AspNet MVC與T4,我定制的視圖模板


一. 遇到的問題

文章開頭部分想先說一下自己的困惑,在用AspNet MVC時,完成Action的編寫,然后添加一個視圖,這個時候彈出一個添加視圖的選項窗口,如下:

  很熟悉吧,繼續上面說的,我添加一個視圖,強類型的、繼承母版頁的視圖,點擊確定,mvc會為我們添加一些自動生成的代碼,感覺很方便。呵呵,剛開始的時候還真方便一些,但也僅僅只是方便一些而已。當遇到以下情景的時候,可能我們就不覺得了:

  程序中都要對N個實體類進行CRUD,就只說添加的功能,生成一個強類型的Create視圖,但是這個自帶的Create視圖的布局可能並不能符合我們界面的要求,沒關系啊,這個改改界面就ok了,這個是個不錯的方法。但是有N個實體要進行CRUD的時候工作量就是time*N了,而且因為要求頁面風格一致,我們幾乎在做一樣的工作,有必要嗎?

  當然沒必要了,應該把重復的工作交給程序去做,省點時間去……

  舉個例子吧,自帶的Create視圖使用的是div進行排版的,而我們需要的是table來進行排版,這個改起來不難,但蠻麻煩的。為什么我就不能定制Create視圖的模板,讓它生成我想要的布局呢?這個可以有。

二. 解決問題

  經過多方搜羅,終於找到了AspNet MVC中用於生成這些視圖的東東:T4(Text Template Transformation Toolkit)文本模板轉換工具箱。在MVC項目中正是使用了T4來生成視圖模板的,它藏在哪呢?是在你VS安裝目錄下:...\Microsoft Visual Studio 10.0\Common7\IDE\ItemTemplates\CSharp\Web,在這個文件夾下面有MVC2、MVC3和MVC4的模板,創建什么項目就會用到對應的模板。這里只演示MVC3 生成Razor的,用2和4的同學就自己摸索了,都差不多的。在MVC3文件夾里面的CodeTemplates文件夾中包含了生成Controller和View兩個文件夾。這里只說視圖的,Controller里面的東西也可以去試試。在...\MVC 3\CodeTemplates\AddView\CSHTML,在這個文件夾下我們可以看到:

  這些正好是在創建強類型視圖時系統自帶的模板。好了,源頭找到了,也應該進行修改了吧。不急,還有一點東西要了解,T4的基本編寫語法:

T4基本語法

T4包括三個部分:

Directives(指令) 元素,用於控制模板如何被處理
Texts blocks(文本塊) 用於直接復制到輸出文件
Control blocks(控制塊) 編程代碼,用於控制變量顯示文本

1)指令

   語法:

    <#@ DirectiveName [AttributeName = "AttributeValue"] … #>

     常用的指令

    <#@ template [language="C#"] [hostspecific="true"] [debug="true"] [inherits="templateBaseClass"] [culture="code"] [complierOption="options"] #>

    <#@ parameter type="Full.TypeName" name="ParameterName" #>

    <#@ output extension=".fileNameExtension" [encoding="encoding"] #>

    <#@ assembly name="[assembly strong name| assembly file name]" #>

    <#@ import namespace="namespace" #>

    <#@ include file="filepath" #>

2)文本塊

  只需要輸入文本就可以了

3)控制塊
  <# #> 代碼表達式
  <#= #> 顯示表達式值
  <#+ #> 聲明定義方法、變量
 T4和MVC2中的界面編碼非常類似,這就不用多說了吧。

  其實不僅僅有頁面布局的問題,還有數據顯示的問題,驗證的問題等等,只要是界面上需要重復編寫的東西都可以使用T4來減少工作量。

  新建一個視圖模板

  將CodeTemplates文件夾拷貝到項目程序的根目錄下,覆蓋默認視圖模板。可以在MVC自帶視圖模板基礎上進行修改,也可以自己新創建一個。建議做法是新建一個視圖模板,是Text Template文件.tt后綴的,然后將要改動的系統視圖代碼復制過來,再進行修改。在此之前,選中所有CodeTemplates文件夾中的tt文件,右鍵屬性,將Custome Tool項默認值清掉,原因還不清楚,知道的大蝦指點下啊。(不去掉的話編譯不通過,缺少了xxx程序集;也可以添加xxx程序集到項目中,不過沒這個必要)

  如本文修改Create模板,新建一個newCreate.tt文件,與Create.tt文件在同一文件夾內,將Create.tt內容復制到newCreate.tt中,在此基礎上進行修改。

  瀏覽一下newCreate.tt的代碼

  包括設定輸出文件格式,引入程序集和命名空間。這些和我們編寫cs時差不多,不多講了。

  最關鍵的是MvcTextTemplateHost這個類,這個類存儲着視圖信息,如視圖名、視圖類型(部分視圖、強類型視圖)、是否繼承母版頁等,具體的內容是有文章開篇的那一張圖中設定的內容,即添加視圖窗口的信息將會保存到MvcTextTemplateHost這個類實例去。好,那么這個類藏在哪呢?上網搜了一下,VS2008 sp1是在...\Microsoft Visual Studio 9.0\Common7\IDE\Microsoft.VisualStudio.Web.Extensions.dll這個程序集中定義的。不過我找了好久都沒找到,可能是因為我用的是VS2010吧。還好最終還是找到了,是在...\Microsoft Visual Studio 10.0\Common7\IDE\Microsoft.VisualStudio.Web.Mvc.2.0.dll這個程序集中定義的,在Microsoft.VisualStudio.Web.Mvc命名空間內。具體的內容用Reflector反編譯看下就知道了。

  啰嗦了好多東西,現在也該進入正題了。

修改視圖生成界面

  將默認視圖中div排版改成table排版,修改里面的back to list、Create等英文單詞為中文。這個是最簡單的修改了,只需修改tt文件的文本塊內容就可以了。直接上效果圖了:

  添加視圖,這時我們自己新定義的模板已經在列表框中了:

  

  兩個模板的效果,左邊是由newCreate模板生成的,右邊是由Create模板生成

  

  這個只是做了一下簡單的頁面修改,想怎么改就看具體要求了。

與Jquery聯用,自動添加時間插件

  如果僅僅只是修改一下界面,那真的沒必要這么大費周章的,我們還可以再進一步改進。在出生日期這一編輯框中,我們往往都會使用一個jquery時間插件來美化。那我們可不可以讓newCreate.tt文本模板在檢測到DateTime類型時自動添加js代碼,答案是可以的。

  在開始之前肯定是要先下載需要的js文件,布置到項目中。這里就只貼出關鍵部分的代碼和效果圖:

  

View Code
復制代碼
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content( "~/Content/jquery-ui/jquery-ui-1.8.12.custom.min.js")" type="text/javascript"></script>
<script src="@Url.Content( "~/Content/jquery-ui/jquery-ui-i18n.min.js")" type="text/javascript"></script>
<script src="@Url.Content( "~/Content/jquery-ui-timepicker-addon.js")" type="text/javascript"></script>
<link href="@Url.Content( "~/Content/jquery-ui/redmond/jquery-ui-1.8.5.custom.css")" rel="stylesheet" type="text/css" />
<script type="text/javascript">
$(document).ready(function () {
// 自動綁定時間插件
<#
foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) {
if (property.UnderlyingType == typeof(DateTime)) {
#>
$('#<#= property.Name #>').datetimepicker({
currentText: '當前時間',
closeText: '完成',
timeText: '時間',
hourText: '小時',
minuteText: '分鍾',
dateFormat: 'yy年mm月dd日',
});
<#
}
}
#>
});
</script>
復制代碼

 

  

  這樣只要程序中有用到時間的地方就會自動生成js代碼然后就方便很多了。當然可以寫個MVC的html擴展方法來實現,方便的程度差不多吧。

數據驗證

  還可以再改進么?必然可以,只要你想得到。數據驗證,這個在添加編輯數據的地方都會用到。這回不說能夠全自動吧,起碼也是半自動。其實在MVC中已經有通過后台編寫Metadata來進行數據驗證了,不過那個是在后台。這回要做的是在前端頁面進行半自動添加js驗證代碼。要想做得方便的話就得自己編寫一些js代碼,這個肯定要的,方便的前提是先需要復雜一段時間(不過也沒多復雜)。

  在做驗證的時候,我們無非要驗證不可空、驗證數字、手機號碼、郵件地址等這些東西。還有一點是js代碼是通過文本模板生成的,這就要求我們需要創建一個通用的驗證函數。這個函數怎么設計呢?想想,每一個驗證都會對應一個form表單、需要驗證的格式、驗證不通過時的提示信息。也就是說這個js函數有三個參數:

  1 被驗證的表單的id:這個可以在文本模板中獲得

  2 驗證的格式:這個編寫一個js的枚舉類型吧,把要用到的所有格式的正則表達式寫好。

  3 提示信息:這個總不能也要自動生成吧

  說了思路我就直接貼代碼和效果圖了,想去了解的可以下載程序來看一下。

  看一下這個自動生成的js代碼:

  

  第一個formValidatorRegex.js文件存儲的就是各種格式的正則表達式。

  第二個formValidatorUI.js文件是checkValue這個驗證函數的定義。

  自動生成之后就可以做一些小的修改來達到我們需要的驗證功能了,兩個地方要修改的,第一個就是設置驗證格式,第二個是填寫提示信息。

  現在把newCreate.tt的全部代碼獻上:

  

View Code
復制代碼
<#@ template language="C#" HostSpecific="True" #>
<#@ output extension=".cshtml" #>
<#@ 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" #>
<#
MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host); // 關鍵類,其實例是通過Add View窗口所做的設定獲取的
#>
@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#>"; @*ViewName視圖名,第一張圖中的View name 中的值*@
<#
if (!String.IsNullOrEmpty(mvcHost.MasterPageFile)) { // 母版頁
#>
Layout = "<#= mvcHost.MasterPageFile#>"; @*MasterPageFile母版頁路徑*@
<#
}
#>
}

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

<#
} else {
#>

@{
Layout = null;
}

<!DOCTYPE html>

<html>
<head>
<title><#= mvcHost.ViewName #></title>
</head>
<body>
<#
PushIndent("");
}
#>
<#
if (mvcHost.ReferenceScriptLibraries) { // ReferenceScriptLibraries是否勾取了引入javascript腳本庫
#>
<#
if (!mvcHost.IsContentPage) {
#>
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<#
}
#>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<#
}
#>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend><#= mvcHost.ViewDataType.Name #></legend>
<table class="cls">
<#
foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) {
if (!property.IsPrimaryKey && !property.IsReadOnly) {
#>
<tr>
<td>
@Html.LabelFor(model => model.<#= property.Name #>)
</td>
<td>
@Html.EditorFor(model => model.<#= property.Name #>)
@Html.ValidationMessageFor(model => model.<#= property.Name #>)
</td>
</tr>
<#
}
}
#>
<tr>
<td></td>
<td><input type="submit" id="submit1" value="創建" /></td>
</tr>
</table>
</fieldset>
}

<div>
@Html.ActionLink("返回列表", "Index")
</div>
<div id="alertDialog" title="消息提示"></div>

<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content( "~/Content/jquery-ui/jquery-ui-1.8.12.custom.min.js")" type="text/javascript"></script>
<script src="@Url.Content( "~/Content/jquery-ui/jquery-ui-i18n.min.js")" type="text/javascript"></script>
<script src="@Url.Content( "~/Content/jquery-ui-timepicker-addon.js")" type="text/javascript"></script>
<link href="@Url.Content( "~/Content/jquery-ui/redmond/jquery-ui-1.8.5.custom.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Content/formValidatorRegex.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Content/formValidatorUI.js")" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function () {
// 添加驗證代碼
$("#submit1").click(function () {
if (1==2 //初始化,驗證默認不通過(驗證時將其刪除)
<#
foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) {
#>
&& checkValue($("#<#= property.Name #>").val(), "", "")
<#
}
#>
) { //checkValue($("#").val(), regexEnum, "")
//第1個參數是需要驗證的值
//第2個參數為正則表達式
//第3個參數是驗證不通過的提示信息
return true;
}
return false;
});

// 自動綁定時間插件
<#
foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) {
if (property.UnderlyingType == typeof(DateTime)) {
#>
$('#<#= property.Name #>').datetimepicker({
currentText: '當前時間',
closeText: '完成',
timeText: '時間',
hourText: '小時',
minuteText: '分鍾',
dateFormat: 'yy年mm月dd日',
});
<#
}
}
#>
});
</script>


<#
// 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 ValueExpression { get; set; }
public Type UnderlyingType { get; set; }
public bool IsPrimaryKey { get; set; }
public bool IsReadOnly { 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.ValueExpression = "String.Format(\"{0:F}\", " + prop.ValueExpression + ")";
}
else if (prop.UnderlyingType == typeof(DateTime)) {
prop.ValueExpression = "String.Format(\"{0:g}\", " + prop.ValueExpression + ")";
}
}

return results;
}

// 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);
}

// 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; // 去掉可空之后的類型
// GetGetMethod()獲取公共get訪問器(就是屬性定義中的get方法)、GetIndexParameters()獲取索引器
// 這個判斷條件就是屬性必須有get方法,且不能為索引器類型,再一個返回類型必須是基元類型或者[String、Guid、DateTime、TimeSpan、decimal、TimeSpan、DateTimeOffset]這些類型之一
if (prop.GetGetMethod() != null && prop.GetIndexParameters().Length == 0 && IsBindableType(underlyingType)) {
results.Add(new ModelProperty { // 添加到ModelProperty
Name = prop.Name,
ValueExpression = "Model." + prop.Name,
UnderlyingType = underlyingType,
IsPrimaryKey = IsPrimaryKey(prop),
IsReadOnly = prop.GetSetMethod() == null
});
}
}

return results;
}

// Helper
bool IsBindableType(Type type) {
return type.IsPrimitive || bindableNonPrimitiveTypes.Contains(type);
}

#>
復制代碼

 

  好了,寫得差不多了。其實還好很多可以改進的地方,感興趣的同學可以再去修改一下默認的controller模板,然后再結合自己自定的視圖模板,完成一個實體的簡單增刪改查應該用10分鍾就可以搞定了,只是簡單的,不要想太多了。

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM