前言:上篇介紹了下ko增刪改查的封裝,確實節省了大量的js代碼。博主是一個喜歡偷懶的人,總覺得這些基礎的增刪改查效果能不能通過一個什麼工具直接生成頁面效果,啥代碼都不用寫了,那該多爽。於是研究了下T4的語法,雖然沒有完全掌握,但是算是有了一個大致的了解。於是乎有了今天的這篇文章:通過T4模板快速生成頁面。
KnockoutJS系列文章:
BootstrapTable與KnockoutJS相結合實現增刪改查功能【一】
BootstrapTable與KnockoutJS相結合實現增刪改查功能【二】
BootstrapTable+KnockoutJS相結合實現增刪改查解決方案(三)兩個Viewmodel搞定增刪改查
一、T4的使用介紹
我們知道,MVC裡面在添加視圖的時候可以自動生成增刪改查的頁面效果,那是因為MVC為我們內置了基礎增刪改查的模板,這些模板的語法就是使用T4,那麼這些模板在哪裡呢?找了下相關文章,發現MVC4及以下的版本模板位置和MVC5及以上模板的位置有很大的不同。
•MVC4及以下版本的模板位置:VS的安裝目錄+\ItemTemplates\CSharp\Web\MVC 2\CodeTemplates。比如博主的D:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ItemTemplates\CSharp\Web\MVC 4\CodeTemplates。
找到cshtml對應的模板,裡面就有相應的增刪改查的tt文件
•MVC5及以上版本的模板位置:直接給出博主的模板位置D:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates
知道了這個,那麼接下來就是改造模板,添加自己的生成內容了。可以直接將List和Edit模板拷貝到過來自行改造,但是最後想了想,還是別動MVC內置的東西了,我們自己來建自己的模板不是更好。
在當前Web項目的根目錄下面新建一個文件夾,命名為CodeTemplates,然後將MVC模板裡面的MvcControllerEmpty和MvcView兩個模板文件夾拷貝到CodeTemplates文件夾下面,去掉它裡面的原始模板,然後新建幾個自己的模板,如下圖:
這樣我們在添加新的控制器和新建視圖的時候就可以看到我們自定義的模板了:
二、T4代碼介紹
上面介紹了如何新建自己的模板,模板建好之後就要開始往裡面塞相應的內容了,如果T4的語法展開了說,那一篇是說不完的,有興趣的園友可以去園子裡找找,文章還是挺多的。這裡主要還是來看看幾個模板內容。還有一點需要說明下,貌似從MVC5之後,T4的模板文件後綴全部改成了t4,而之前的模板一直是tt結尾的,沒有細究它們語法的區別,估計應該差別不大。
1、Controller.cs.t4
為什麼要重寫這個空的控制器模板呢?博主覺得增刪改查的好多方法都需要手動去寫好麻煩,寫一個模板直接生成可以省事很多。來看看模板裡面的實現代碼:
<#@ template language="C#" HostSpecific="True" #> <#@ output extension="cs" #> <#@ parameter type="System.String" name="ControllerName" #> <#@ parameter type="System.String" name="ControllerRootName" #> <#@ parameter type="System.String" name="Namespace" #> <#@ parameter type="System.String" name="AreaName" #> <# var index = ControllerName.LastIndexOf("Controller"); var ModelName = ControllerName.Substring(0, index); #> using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using TestKO.Models; namespace <#= Namespace #> { public class <#= ControllerName #> : Controller { public ActionResult Index() { return View(); } public ActionResult Edit(<#= ModelName #> model) { return View(model); } [HttpGet] public JsonResult Get(int limit, int offset) { return Json(new { }, JsonRequestBehavior.AllowGet); } //新增實體 [HttpPost] public JsonResult Add(<#= ModelName #> oData) { <#= ModelName #>Model.Add(oData); return Json(new { }, JsonRequestBehavior.AllowGet); } //更新實體 [HttpPost] public JsonResult Update(<#= ModelName #> oData) { <#= ModelName #>Model.Update(oData); return Json(new { }, JsonRequestBehavior.AllowGet); } //刪除實體 [HttpPost] public JsonResult Delete(List<<#= ModelName #>> oData) { <#= ModelName #>Model.Delete(oData); return Json(new { }, JsonRequestBehavior.AllowGet); } } }
這個內容不難理解,直接查看生成的控制器代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using TestKO.Models; namespace TestKO.Controllers { public class UserController : Controller { public ActionResult Index() { return View(); } public ActionResult Edit(User model) { return View(model); } [HttpGet] public JsonResult Get(int limit, int offset) { return Json(new { }, JsonRequestBehavior.AllowGet); } //新增實體 [HttpPost] public JsonResult Add(User oData) { UserModel.Add(oData); return Json(new { }, JsonRequestBehavior.AllowGet); } //更新實體 [HttpPost] public JsonResult Update(User oData) { UserModel.Update(oData); return Json(new { }, JsonRequestBehavior.AllowGet); } //刪除實體 [HttpPost] public JsonResult Delete(List<User> oData) { UserModel.Delete(oData); return Json(new { }, JsonRequestBehavior.AllowGet); } } }
2、KoIndex.cs.t4
這個模板主要用於生成列表頁面,大致代碼如下:
<#@ template language="C#" HostSpecific="True" #> <#@ output extension=".cshtml" #> <#@ include file="Imports.include.t4" #> <# // The following chained if-statement outputs the file header code and markup for a partial view, a view using a layout page, or a regular view. if(IsPartialView) { #> <# } else if(IsLayoutPageSelected) { #> @{ ViewBag.Title = "<#= ViewName#>"; <# if (!String.IsNullOrEmpty(LayoutPageFile)) { #> Layout = "<#= LayoutPageFile#>"; <# } #> } <# } else { #> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title><#= ViewName #></title> <link href="~/Content/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> <link href="~/Content/bootstrap-table/bootstrap-table.min.css" rel="stylesheet" /> <script src="~/scripts/jquery-1.9.1.min.js"></script> <script src="~/Content/bootstrap/js/bootstrap.min.js"></script> <script src="~/Content/bootstrap-table/bootstrap-table.min.js"></script> <script src="~/Content/bootstrap-table/locale/bootstrap-table-zh-CN.js"></script> <script src="~/scripts/knockout/knockout-3.4.0.min.js"></script> <script src="~/scripts/knockout/extensions/knockout.mapping-latest.js"></script> <script src="~/scripts/extensions/knockout.index.js"></script> <script src="~/scripts/extensions/knockout.bootstraptable.js"></script> <script type="text/javascript"> $(function () { var viewModel = { bindId: "div_index", tableParams : { url : "/<#=ViewDataTypeShortName#>/Get", pageSize : 2, }, urls : { del : "/<#=ViewDataTypeShortName#>/Delete", edit : "/<#=ViewDataTypeShortName#>/Edit", add : "/<#=ViewDataTypeShortName#>/Edit", }, queryCondition : { } }; ko.bindingViewModel(viewModel); }); </script> </head> <body> <# PushIndent(" "); } #> <div id="toolbar"> <button data-bind="click:addClick" type="button" class="btn btn-default"> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>新增 </button> <button data-bind="click:editClick" type="button" class="btn btn-default"> <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>修改 </button> <button data-bind="click:deleteClick" type="button" class="btn btn-default"> <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>刪除 </button> </div> <table data-bind="bootstrapTable:bootstrapTable"> <thead> <tr> <th data-checkbox="true"></th> <# IEnumerable<PropertyMetadata> properties = ModelMetadata.Properties; foreach (PropertyMetadata property in properties) { if (property.Scaffold && !property.IsPrimaryKey && !property.IsForeignKey) { #> <th data-field="<#= GetValueExpression(property) #>"><#= GetValueExpression(property) #></th> <# } }#> </tr> </thead> </table> <# // The following code closes the tag used in the case of a view using a layout page and the body and html tags in the case of a regular view page #> <# if(!IsPartialView && !IsLayoutPageSelected) { ClearIndent(); #> </body> </html> <# } #> <#@ include file="ModelMetadataFunctions.cs.include.t4" #>
添加一個視圖Index,然後選擇這個模板
得到的頁面內容
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> <link href="~/Content/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> <link href="~/Content/bootstrap-table/bootstrap-table.min.css" rel="stylesheet" /> <script src="~/scripts/jquery-1.9.1.min.js"></script> <script src="~/Content/bootstrap/js/bootstrap.min.js"></script> <script src="~/Content/bootstrap-table/bootstrap-table.min.js"></script> <script src="~/Content/bootstrap-table/locale/bootstrap-table-zh-CN.js"></script> <script src="~/scripts/knockout/knockout-3.4.0.min.js"></script> <script src="~/scripts/knockout/extensions/knockout.mapping-latest.js"></script> <script src="~/scripts/extensions/knockout.index.js"></script> <script src="~/scripts/extensions/knockout.bootstraptable.js"></script> <script type="text/javascript"> $(function () { var viewModel = { bindId: "div_index", tableParams : { url : "/User/Get", pageSize : 2, }, urls : { del : "/User/Delete", edit : "/User/Edit", add : "/User/Edit", }, queryCondition : { } }; ko.bindingViewModel(viewModel); }); </script> </head> <body> <div id="toolbar"> <button data-bind="click:addClick" type="button" class="btn btn-default"> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>新增 </button> <button data-bind="click:editClick" type="button" class="btn btn-default"> <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>修改 </button> <button data-bind="click:deleteClick" type="button" class="btn btn-default"> <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>刪除 </button> </div> <table data-bind="bootstrapTable:bootstrapTable"> <thead> <tr> <th data-checkbox="true"></th> <th data-field="Name">Name</th> <th data-field="FullName">FullName</th> <th data-field="Age">Age</th> <th data-field="Des">Des</th> <th data-field="Createtime">Createtime</th> <th data-field="strCreatetime">strCreatetime</th> </tr> </thead> </table> </body> </html> Index.cshtml
我們將上篇說的viewmodel搬到頁面上面來了,這樣每次就不用從controller裡面傳過來了。稍微改一下表格的列名,頁面就可以跑起來了。
這裡有待優化的幾點:
(1)查詢條件沒有生成,如果將T4的語法研究深一點,可以在需要查詢的字段上面添加特性標識哪些字段需要查詢,然後自動生成對應的查詢條件。
(2)表格的列名似乎也可以通過屬性的字段特性來生成。這點和第一點類似,都需要研究T4的語法。
3、KoEdit.cs.t4
第三個模板頁就是編輯的模板了,它的大致代碼如下:
<#@ template language="C#" HostSpecific="True" #> <#@ output extension=".cshtml" #> <#@ include file="Imports.include.t4" #> @model <#= ViewDataTypeName #> <# // "form-control" attribute is only supported for all EditorFor() in System.Web.Mvc 5.1.0.0 or later versions, except for checkbox, which uses a div in Bootstrap string boolType = "System.Boolean"; Version requiredMvcVersion = new Version("5.1.0.0"); bool isControlHtmlAttributesSupported = MvcVersion >= requiredMvcVersion; // The following chained if-statement outputs the file header code and markup for a partial view, a view using a layout page, or a regular view. if(IsPartialView) { #> <# } else if(IsLayoutPageSelected) { #> @{ ViewBag.Title = "<#= ViewName#>"; <# if (!String.IsNullOrEmpty(LayoutPageFile)) { #> Layout = "<#= LayoutPageFile#>"; <# } #> } <h2><#= ViewName#></h2> <# } else { #> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title><#= ViewName #></title> </head> <body> <# PushIndent(" "); } #> <# if (ReferenceScriptLibraries) { #> <# if (!IsLayoutPageSelected && IsBundleConfigPresent) { #> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/jqueryval") <# } #> <# else if (!IsLayoutPageSelected) { #> <script src="~/Scripts/jquery-<#= JQueryVersion #>.min.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <# } #> <# } #> <form id="formEdit" class="form-horizontal"> @Html.HiddenFor(model => model.Id) <div class="modal-body"> <# IEnumerable<PropertyMetadata> properties = ModelMetadata.Properties; foreach (PropertyMetadata property in properties) { if (property.Scaffold && !property.IsPrimaryKey && !property.IsForeignKey) { #> <div class="form-group"> @Html.LabelFor(model => model.<#= GetValueExpression(property) #>, "<#= GetValueExpression(property) #>", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.<#= GetValueExpression(property) #>, new { @class = "form-control", data_bind = "value:editModel.<#= GetValueExpression(property) #>" }) </div> </div> <# } } #> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span>關閉</button> <button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>保存</button> </div> </form> <# var index = ViewDataTypeName.LastIndexOf("."); var ModelName = ViewDataTypeName.Substring(index+1, ViewDataTypeName.Length-index-1); #> <script src="~/Scripts/extensions/knockout.edit.js"></script> <script type="text/javascript"> $(function () { var model = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model)); var viewModel = { formId: "formEdit", editModel : model, urls : { submit : model.id == 0 ? "/<#= ModelName #>/Add" : "/<#= ModelName #>/Update" }, validator:{ fields: { Name: { validators: { notEmpty: { message: '名稱不能為空!' } } } } } }; ko.bindingEditViewModel(viewModel); }); </script> <# if(IsLayoutPageSelected && ReferenceScriptLibraries && IsBundleConfigPresent) { #> @section Scripts { @Scripts.Render("~/bundles/jqueryval") } <# } #> <# else if(IsLayoutPageSelected && ReferenceScriptLibraries) { #> <script src="~/Scripts/jquery-<#= JQueryVersion #>.min.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <# } #> <# // The following code closes the tag used in the case of a view using a layout page and the body and html tags in the case of a regular view page #> <# if(!IsPartialView && !IsLayoutPageSelected) { ClearIndent(); #> </body> </html> <# } #> <#@ include file="ModelMetadataFunctions.cs.include.t4" #>
生成的代碼:
@model TestKO.Models.User <form id="formEdit" class="form-horizontal"> @Html.HiddenFor(model => model.Id) <div class="modal-body"> <div class="form-group"> @Html.LabelFor(model => model.Name, "Name", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.Name, new { @class = "form-control", data_bind = "value:editModel.Name" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.FullName, "FullName", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.FullName, new { @class = "form-control", data_bind = "value:editModel.FullName" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Age, "Age", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.Age, new { @class = "form-control", data_bind = "value:editModel.Age" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Des, "Des", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.Des, new { @class = "form-control", data_bind = "value:editModel.Des" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Createtime, "Createtime", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.Createtime, new { @class = "form-control", data_bind = "value:editModel.Createtime" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.strCreatetime, "strCreatetime", new { @class = "control-label col-xs-2" }) <div class="col-xs-10"> @Html.TextBoxFor(model => model.strCreatetime, new { @class = "form-control", data_bind = "value:editModel.strCreatetime" }) </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span>關閉</button> <button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>保存</button> </div> </form> <script src="~/Scripts/extensions/knockout.edit.js"></script> <script type="text/javascript"> $(function () { var model = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model)); var viewModel = { formId: "formEdit", editModel : model, urls : { submit : model.id == 0 ? "/User/Add" : "/User/Update" }, validator:{ fields: { Name: { validators: { notEmpty: { message: '名稱不能為空!' } } } } } }; ko.bindingEditViewModel(viewModel); }); </script> Edit.cshtml
當然,代碼也需要做稍許修改。通過添加自定義的模板頁,只要後台對應的實體模型建好了,在前端只需要新建兩個自定義視圖,一個簡單的增刪改查即可完成,不用寫一句js代碼。
三、select組件的綁定
上面介紹了下T4封裝增刪改查的語法,頁面所有的組件基本都是文本框,然而,在實際項目中,很多的查詢和編輯頁面都會存在下拉框的展示,對於下拉框,我們該如何處理呢?不賣關子了,直接給出解決方案吧,比如編輯頁面我們可以在後台將下拉框的數據源放在實體裡面。
用戶的實體
[DataContract] public class User { [DataMember] public int id { get; set; } [DataMember] public string Name { get; set; } [DataMember] public string FullName { get; set; } [DataMember] public int Age { get; set; } [DataMember] public string Des { get; set; } [DataMember] public DateTime Createtime { get; set; } [DataMember] public string strCreatetime { get; set; } [DataMember] public string DepartmentId { get; set; } [DataMember] public object Departments { get; set; } }
然後編輯頁面
public ActionResult Edit(User model) { model.Departments = DepartmentModel.GetData(); return View(model); }
然後前端綁定即可。
<div class="form-group"> <label for="txt_des">所屬部門</label> <select id="sel_dept" class="form-control" data-bind="options: editModel.Departments, optionsText: 'Name', optionsValue: 'Id', value:editModel.DepartmentId"></select> </div>
JS代碼不用做任何修改,新增和編輯的時候部門字段就能自動添加到viewmodel裡面去。
當然,我們很多項目使用的下拉框都不是單純的select,因為單純的select樣式實在是難看,於是乎出了很多的select組件,比如博主之前分享的select2、MultiSelect等等。當使用這些組件去初始化select時,審核元素你會發現,這個時候界面上的下拉框已經不是單純的select標簽了,而是由組件自定義的很多其他標簽組成。我們就以select2組件為例來看看直接按照上面的這樣初始化是否可行。
我們將編輯頁面初始化的js代碼增加最後一句:
<script type="text/javascript"> $(function () { var model = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model)); var viewModel = { formId: "formEdit", editModel : model, urls : { submit : model.id == 0 ? "/User/Add" : "/User/Update" }, validator:{ fields: { Name: { validators: { notEmpty: { message: '名稱不能為空!' } } } } } }; ko.bindingEditViewModel(viewModel); $("#sel_dept").select2({}); }); </script>
通過新增和編輯發現,這樣確實可行!分析原因,雖然初始化成select2組件之後,頁面的html發生了變化,但是組件最終還是會將選中值呈現在原始的select控件上面。不知道除了select2,其他select初始化組件會不會這樣,待驗證。但是這裡有一點需要說明下,在初始化select2之前,下拉框的options必須先綁定值,也就是說,組件的初始化必須要放在ko.applyBinding()之後。
四、總結
至此,ko結合bootstrapTable的模板生成以及select控件的使用基本可用,當然,還有待完善。後面如果有時間,博主會整理下其他前端組件和ko的聯合使用,比如我們最常見的日期控件。如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對網站的支持!