ASP.NET MVC3のScaffoldingで使用するテンプレートファイル

Railsで感心するほど便利だと感じたScaffolding機能ですが、ASP.NET MVC3でも利用できます。こちらはRailsと違って作っては削除する問いことが直感的にできますね。

さて、Railsで開発時にScaffoldingを作成する際には、Scaffoldingのテンプレートを修正して利用していたのですが、ASP.NET MVCでもきっと同様のことができるはずと意気込んで調べてみたところ、やはりありますねぇ~♪以前Railsで大量のマスタメンテナンス画面を作成したときにはScaffoldingで画面が生成されることに喜びを感じて、その後は生成されたファイルを手でごそごそと直し、なおし終わった頃にtemplate機能があることに気づいたなんてことがありましたので今回は慎重に進めていますw

で、保存先ですが "[VisualStudioのインストールディレクトリ]\Common7\IDE\ItemTemplates\CSharp\Web\MVC 3\CodeTemplates\AddView\CSHTML" に保存されています。このフォルダには拡張子がttのファイルが存在しますが、それらが該当するファイルとなっています。

(そのまま修正すると、何かと困るかと思いますので必ずバックアップをとってから修正した方が良さそうです。)

この拡張子がttのファイルですが、中を見れば何となくわかると思います。試しにサンプルを作成してテンプレートを適用してみたのですが、ばっちりでした! (少しわかりにくいですが、元々divタグで作成されている項目を複数の項目をテーブルにマッピングするように修正しています。)

対応するテンプレートファイルはこんな感じです。

[sourcecode language="html"] <#@ template language="C#" HostSpecific="True" #> <#@ output extension=".cshtml" #> <#@ import namespace="Microsoft.VisualStudio.Web.Mvc.Scaffolding.BuiltIn" #> <#@ assembly name="System.ComponentModel.DataAnnotations" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Data.Entity" #> <#@ assembly name="System.Data.Linq" #> <#@ import namespace="System" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.ComponentModel.DataAnnotations" #> <#@ import namespace="System.Data.Linq.Mapping" #> <#@ import namespace="System.Data.Objects.DataClasses" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Reflection" #> <#@ import namespace="Microsoft.VisualStudio.Web.Mvc.Scaffolding.BuiltIn" #> <# MvcTextTemplateHost mvcHost = MvcTemplateHost;

>

@model <#= mvcHost.ViewDataTypeName #> <# // The following chained if-statement outputs the file header code and markup for a partial view, a content page, or a regular view. if(mvcHost.IsPartialView) {

>

<# } else if(mvcHost.IsContentPage) {

>

@{ ViewBag.Title = "<#= mvcHost.ViewName#>"; <# if (!String.IsNullOrEmpty(mvcHost.MasterPageFile)) {

>

Layout = "<#= mvcHost.MasterPageFile#>"; <# }

>

}

<h3><#= mvcHost.ViewName#></h3>

<# } else {

>

@{ Layout = null; }

<!DOCTYPE html>

<html> <head> <title><#= mvcHost.ViewName #></title> </head> <body> <# PushIndent("    "); }

>

<div id="data_area"> ここに説明を記述する<br /> <span class="must_input"> 注意書きを記述</span> <table> <thead></thead> <tbody>

<# foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) { if (!property.IsPrimaryKey && property.Scaffold) {

>

&lt;tr&gt;

<td width="200px"><#= property.AssociationName #> </td> <td width="200px"> @Html.DisplayFor(model => model.<#= property.ValueExpression #>) </td> </tr> <# } }

>

</tbody> </table> </div> @using (Html.BeginForm()) { <p> <input type="submit" value="実行" /> | @Html.ActionLink("一覧画面へ", "Index") </p> } <# // The following code closes the asp:Content tag used in the case of a master page and the body and html tags in the case of a regular view page

>

<# if(!mvcHost.IsPartialView && !mvcHost.IsContentPage) { ClearIndent();

>

</body> </html> <# }

>

<#+ // Describes the information about a property on the model class ModelProperty { public string Name { get; set; } public string AssociationName { get; set; } public string ValueExpression { get; set; } public string ModelValueExpression { get; set; } public string ItemValueExpression { get; set; } public Type UnderlyingType { get; set; } public bool IsPrimaryKey { get; set; } public bool IsForeignKey { get; set; } public bool IsReadOnly { get; set; } public bool Scaffold { get; set; } } // Change this list to include any non-primitive types you think should be eligible for display/edit static Type bindableNonPrimitiveTypes = new { typeof(string), typeof(decimal), typeof(Guid), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), }; // Call this to get the list of properties in the model. Change this to modify or add your // own default formatting for display values. List<ModelProperty> GetModelProperties(Type type) { List<ModelProperty> results = GetEligibleProperties(type);

foreach (ModelProperty prop in results) { if (prop.UnderlyingType == typeof(double) || prop.UnderlyingType == typeof(decimal)) { prop.ModelValueExpression = "String.Format(&quot;{0:F}&quot;, " + prop.ModelValueExpression + ")"; } else if (prop.UnderlyingType == typeof(DateTime)) { prop.ModelValueExpression = "String.Format(&quot;{0:g}&quot;, " + prop.ModelValueExpression + ")"; } } return results; } // Call this to determine if property has scaffolding enabled bool Scaffold(PropertyInfo property) { foreach (object attribute in property.GetCustomAttributes(true)) { var scaffoldColumn = attribute as ScaffoldColumnAttribute; if (scaffoldColumn != null && !scaffoldColumn.Scaffold) { return false; } } return true; } // Call this to determine if the property represents a primary key. Change the // code to change the definition of primary key. bool IsPrimaryKey(PropertyInfo property) { if (string.Equals(property.Name, "id", StringComparison.OrdinalIgnoreCase)) {  // EF Code First convention return true; }

if (string.Equals(property.Name, property.DeclaringType.Name + "id", StringComparison.OrdinalIgnoreCase)) {  // EF Code First convention return true; } foreach (object attribute in property.GetCustomAttributes(true)) { if (attribute is KeyAttribute) {  // WCF RIA Services and EF Code First explicit return true; }

var edmScalar = attribute as EdmScalarPropertyAttribute; if (edmScalar != null && edmScalar.EntityKeyProperty) {  // EF traditional return true; } var column = attribute as ColumnAttribute; if (column != null && column.IsPrimaryKey) {  // LINQ to SQL return true; } }

return false; } // This will return the primary key property name, if and only if there is exactly // one primary key. Returns null if there is no PK, or the PK is composite. string GetPrimaryKeyName(Type type) { IEnumerable<string> pkNames = GetPrimaryKeyNames(type); return pkNames.Count() == 1 ? pkNames.First() : null; } // This will return all the primary key names. Will return an empty list if there are none. IEnumerable<string> GetPrimaryKeyNames(Type type) { return GetEligibleProperties(type).Where(mp => mp.IsPrimaryKey).Select(mp => mp.Name); }

// Call this to determine if the property represents a foreign key. bool IsForeignKey(PropertyInfo property) { return MvcTemplateHost.RelatedProperties.ContainsKey(property.Name); }

// A foreign key, e.g. CategoryID, will have a value expression of Category.CategoryID string GetValueExpressionSuffix(PropertyInfo property) { RelatedModel propertyModel; MvcTemplateHost.RelatedProperties.TryGetValue(property.Name, out propertyModel);

return propertyModel != null ? propertyModel.PropertyName + "." + propertyModel.DisplayPropertyName : property.Name; }

// A foreign key, e.g. CategoryID, will have an association name of Category string GetAssociationName(PropertyInfo property) { RelatedModel propertyModel; MvcTemplateHost.RelatedProperties.TryGetValue(property.Name, out propertyModel); return propertyModel != null ? propertyModel.PropertyName : property.Name; }

// Helper List<ModelProperty> GetEligibleProperties(Type type) { List<ModelProperty> results = new List<ModelProperty>(); foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { Type underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; if (prop.GetGetMethod() != null && prop.GetIndexParameters().Length == 0 && IsBindableType(underlyingType)) { string valueExpression = GetValueExpressionSuffix(prop); results.Add(new ModelProperty { Name = prop.Name, AssociationName = GetAssociationName(prop), ValueExpression = valueExpression, ModelValueExpression = "Model." + valueExpression, ItemValueExpression = "item." + valueExpression, UnderlyingType = underlyingType, IsPrimaryKey = IsPrimaryKey(prop), IsForeignKey = IsForeignKey(prop), IsReadOnly = prop.GetSetMethod() == null, Scaffold = Scaffold(prop) }); } } return results; } // Helper bool IsBindableType(Type type) { return type.IsPrimitive || bindableNonPrimitiveTypes.Contains(type); } MvcTextTemplateHost MvcTemplateHost { get { return (MvcTextTemplateHost)Host; } }

>

[/sourcecode]

少し長いですが、基本的には上の方しか修正していません。下の方は何が書いているのかもいまいち理解できていない…。ここまでできるとほぼたいていのことができてきそうな気になりますねw