控件封裝的部分說明 css
可能有人以爲應該先後端分離,我也認可這是應該的方向,咱們也在考慮使用ng2等簡化前端。可是,咱們封裝控件仍是由於以下緣由綜合考慮的: html
其實呢,mvc也提供了html.textfor等寫法,其中有的也封裝了js的,甚至校驗也是封裝的js。本節內容進階二,是直接使用cshtml,部分作到了先後端分離。固然了,若是有更好的建議和作法,歡迎提出來。 前端
看本篇以前,建議先看一下上一篇9.2.1 .net framework下的MVC 控件的封裝(上)。 jquery
進階一:For類型控件的作法 數據庫
咱們在上一篇的最開始樣例中,寫了MVC控件的三種寫法。 express
1 @model UserInfo 2 3 4 <input type="text" id="t2" value="t2Value" /> <!—第一種寫法 --> 5 6 @Html.TextBox("t1", "t1value"); <!—第二種寫法 --> 7 8 @Html.TextBoxFor(user => user.EMail) <!—第三種寫法 -->
第一種是html的原始寫法,控件的封裝不會用這種方法(.net core的taghelper就能夠寫成這樣了)。在上一篇的介紹中,咱們講解了按照第二種樣子來作控件的封裝。可是當咱們的控件要綁定cshtml頁面的model屬性時,要寫成第三種方式,也就是寫成相似@Html.TextboxFor(user => user.Email)的For寫法。 後端
這裏的user是指的UserInfo,這個寫法就是在頁面中生成Id爲Email的input元素,值也自動填充UserInfo的實例的電子郵件地址EMail,而且若是UserInfo類的Email屬性上有校驗、顯示名等Attribute時,能夠自動生成校驗腳本和標籤。例以下面就是對UserName和Age兩個屬性增長顯示名稱和校驗。api
1 [Display(Name="用戶名")] 2 [Required(ErrorMessage = "*姓名必填")] 3 public string UserName { get; set; } 4 5 [Display(Name = "年齡")] 6 [Required(ErrorMessage = "*年齡必填")] 7 public int Age { get; set; }
這種寫法,會在最終生成的html代碼中,自動添加用戶名和年齡的標籤和校驗腳本。mvc
既然這種寫法能夠自動綁定model,也能夠自動生成標籤和校驗等,咱們該如何實現For的寫法呢?下面咱們仍舊如下拉多選控件爲例,進行說明。 框架
一樣的,咱們須要寫一個MultiSelectFor的控件,這個For控件是個泛型類
1 public class MultiSelectFor<TModel, TProperty> : MvcControlForBase<TModel, TProperty> 2 { 3 /// <summary> 4 /// 初始化 5 /// </summary> 6 /// <param name="htmlHelper"></param> 7 /// <param name="expression"></param> 8 /// <param name="htmlAttributes"></param> 9 public MultiSelectFor(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) 10 : base(htmlHelper, expression, htmlAttributes) 11 { 12 this.DataSource = new Dictionary<string, string>(); 13 } 14 …… 15 }
繼承泛型的控件基類MvcControlForBase<TModel, TProperty>,同樣的也要有一個泛型的基控件構造類
public abstract class MvcControlForBuilderBase<TModel, TProperty, TMvcControl, TBuilder>
where TMvcControl : MvcControlForBase<TModel, TProperty>
where TBuilder : MvcControlForBuilderBase<TModel, TProperty, TMvcControl, TBuilder>
這兩個類的實現與上一篇很相似。咱們再也不仔細介紹了,惟一須要注意的是控件基類MvcControlForBase<TModel, TProperty>中的Name和Id是從表達式中獲取的,而不是經過構造函數的參數傳入的。所以構造函數傳入的是Expression<Func<TModel, TProperty>>。控件的Name能夠自動從Expression中得到,所以能夠直接改成只讀屬性。
1 protected string Name 2 { 3 get 4 { 5 if (Attributes.ContainsKey("name")) 6 { 7 return Attributes["name"].ToString(); 8 } 9 else 10 { 11 return ExpressionHelper.GetExpressionText(Expression); 12 } 13 } 14 }
MultiSelectFor由於可以自動生成Id、Name、Value、Label等,所以構造函數中也就再也不傳入Label、value等參數,而是傳入表達式Expression,這些數據應該直接從for的表達式中獲取。爲了實現獲取value,咱們須要在render方法中,增長下面的內容
ModelMetadata metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(this.Expression, this.Helper.ViewData);
List<string> value = metadata.Model as List<string>;
以及生成select控件時,寫法也更簡單了,直接從表達式生成
divTag.InnerHtml = Helper.DropDownListFor(Expression, selectList, HtmlAttributes).ToHtmlString();
一樣的,按照上一篇的作法,若是在cshtml頁面中按照以下的方式使用multiselect控件(控件的含義是:選擇一我的的多個職責,Duty是職責,一我的可能有多個職責,例如項目經理、高級開發工程師)
@model UserInfo
@HtmlHelper.MultiSelectFor(p => p.Duty).SetDataSource(…).Render()
HtmlHelper擴展方法也須要增長一個:
1 public static MultiSelectForBuilder<TModel, TProperty> MultiSelectFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes = null) 2 { 3 return new MultiSelectForBuilder<TModel, TProperty>(new MultiSelectFor<TModel, TProperty>(helper, expression, htmlAttributes)); 4 }
進階二:資源性視圖的應用
爲了更簡化WriteHtml和WriteScript的寫法,咱們將腳本和html元素都寫到一個cshtml中,這樣代碼看起來更簡潔,也更易讀,也能夠實現先後端的分離。咱們以日期選擇控件DatePicker爲例,DataPicker.cshtml內容以下:
1 @{ 2 string id = this.ViewData.GetString("Id"); 3 string name = this.ViewData.GetString("Name"); 4 string dateFormat = this.ViewData.GetString("DateFormat"); 5 bool readOnly = this.ViewData.GetBool("ReadOnly"); 6 } 7 @if (!readOnly) 8 { 9 <script> 10 require(['jquery', 'jquery.ui', 'ready!'], function ($) { 11 var options = { 12 changeYear: true, 13 changeMonth: true, 14 dateFormat: "@(dateFormat.ToLower().Replace("yyyy", "yy"))" 15 }; 16 $("#@(id)").datepicker(options); 17 }); 18 </script> 19 }
寫成這樣的好處就是不要在WriteHtml和WriteScript中寫一大堆的腳本、html標籤的拼接程序。
再回到DataPicker控件來,這樣datapicker不須要再寫WriteScript方法,僅僅重寫WriteHtml方法,這個方法主要工做也簡化爲:傳入ViewData,而後調用部分視圖DataPicker.cshtml:
1 ViewDataDictionary vdata = new ViewDataDictionary(); 2 3 vdata["Id"] = this.Id; 4 vdata["Name"] = this.Name; 5 vdata["DateFormat"] = this.DateFormat; 6 vdata["ReadOnly"] = (this.DisplayStatus == FieldDisplayStatus.ReadOnly); 7 8 writer.Write(Helper.Partial("MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker", vdata));
這裏須要注意的就是Helper.Partial方法,咱們這裏調用的是這個重載方法
1 // 2 // 摘要: 3 // 以 HTML 編碼字符串的形式呈現指定的分部視圖。 4 // 5 // 參數: 6 // htmlHelper: 7 // 此方法擴展的 HTML 幫助器實例。 8 // 9 // partialViewName: 10 // 要呈現的分部視圖的名稱。 11 // 12 // viewData: 13 // 用於分部視圖的視圖數據字典。 14 // 15 // 返回結果: 16 // 以 HTML 編碼字符串形式呈現的分部視圖。 17 public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData);
Helper.Partial的參數是分部視圖名稱,可是爲何是MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker這樣的寫法呢?
咱們前面提到,咱們又更近一步作了封裝,將這些控件都放到一個項目中,打包成應用程序集給各個項目使用。控件使用的cshtml也會被打包進應用程序集中,作法就是將cshtml文件屬性設置爲嵌入的資源,編譯時就會以資源的方式直接打包在dll中。固然了,也能夠將控件的cshtml複製到Web項目中,可是這種作法確實有些土,咱們用的是更高大上的作法J
打包進應用程序集的文件的寫法就是這樣的,類名的全名稱,例如這裏的DataPicker.cshtml就是MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker。
可是怎麼經過MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker來定位DataPicker.cshtml呢?這涉及到頁面查找的問題,說來話長了。
通常狀況下,Razor視圖引擎的基類是RazorViewEngine,繼承於抽象類型BuildManagerViewEngine,再繼承自VirtualPathProviderViewEngine。ViewEngine中的方法FindView和FindPartialView是按照以下的目錄進行搜索的:
咱們能夠繼承VirtualPathProviderViewEngine,重寫FindView和FindPartialView修改上面列表,以適應本身系統的目錄搜索要求。
這種方式對於文件系統方式下的目錄搜索是能夠的,可是若是視圖文件存放在數據庫、Dll的資源中,這種方式就很差使了。這時候,應該是使用更底層的VirtualPathProvider。MSDN中的說明是這樣的:VirtualPathProvider提供了一組可以讓 Web 應用程序從虛擬文件系統中檢索資源的方法。咱們這裏就是經過修改它來完成從Dll的資源中獲取視圖文件。
在Global.asax中的寫法以下:
1 var embeddedViewResolver = new EmbeddedViewResolver(); 2 var viewTable = embeddedViewResolver.GetEmbeddedViews(); 3 var embeddedProvider = new EmbeddedViewVirtualPathProvider(viewTable); 4 HostingEnvironment.RegisterVirtualPathProvider(embeddedProvider);
第一行,建立資源性視圖的解析類,這個類初始化時,先找到控件所在的程序集。
1 public class EmbeddedViewResolver : IEmbeddedViewResolver 2 { 3 private IList<Assembly> assemblies; 4 5 public EmbeddedViewResolver() { 6 this.assemblies = new List<Assembly>() { Assembly.GetAssembly(typeof(MvcControlBase)) }; 7 } 8 }
第二行,解析類從控件所在程序集中找到全部的資源行視圖,存放到EmbeddedViewTable中。EmbeddedViewTable至關與一個Dictionary,存放了ViewName和ViewMetaData的鍵值對。EmbeddedViewTable和ViewMetaData相對比較簡單,不作介紹了。
1 public EmbeddedViewTable GetEmbeddedViews() 2 { 3 if (assemblies == null || assemblies.Count == 0) return null; 4 5 var table = new EmbeddedViewTable(); 6 7 foreach (var assembly in assemblies) 8 { 9 var names = GetNamesOfAssemblyResources(assembly); 10 if (names == null || names.Length == 0) continue; 11 12 foreach (var name in names) 13 { 14 var key = name.ToLowerInvariant(); 15 16 if (!key.Contains(".views.")) continue; //要求全部的資源性視圖都應該在views目錄下 17 18 table.AddView(name, assembly.FullName); 19 } 20 } 21 22 return table; 23 } 24 25 private string[] GetNamesOfAssemblyResources(Assembly assembly) 26 { 27 try 28 { 29 return assembly.GetManifestResourceNames(); 30 } 31 catch 32 { 33 return new string[] { }; 34 } 35 }
第三行,進入正題了,建立繼承於VirtualPathProvider的資源性視圖提供器EmbeddedViewVirtualPathProvider。重寫GetFile,若是是資源性視圖,去EmbeddedViewTable中獲取視圖,不然調用Previous.GetFile(virtualPath)繼續系統缺省方式。同時也重寫FileExists和GetCacheDependency方法。
1 public class EmbeddedViewVirtualPathProvider : VirtualPathProvider 2 { 3 private readonly EmbeddedViewTable _embeddedViews; 4 5 public EmbeddedViewVirtualPathProvider(EmbeddedViewTable embeddedViews) 6 { 7 MicroLibraryExceptionHelper.IsNull(embeddedViews, this.GetType().FullName, TraceLogType.Error, "embeddedViews爲空"); 8 9 this._embeddedViews = embeddedViews; 10 } 11 12 private bool IsEmbeddedView(string virtualPath) 13 { 14 if (string.IsNullOrEmpty(virtualPath)) 15 return false; 16 17 string virtualPathAppRelative = VirtualPathUtility.ToAppRelative(virtualPath); 18 if (!virtualPathAppRelative.StartsWith("~/Views/", StringComparison.InvariantCultureIgnoreCase)) 19 return false; 20 21 var fullyQualifiedViewName = virtualPathAppRelative.Substring(virtualPathAppRelative.LastIndexOf("/") + 1, virtualPathAppRelative.Length - 1 - virtualPathAppRelative.LastIndexOf("/")); 22 23 bool isEmbedded = _embeddedViews.ContainsEmbeddedView(fullyQualifiedViewName); 24 return isEmbedded; 25 } 26 27 public override bool FileExists(string virtualPath) 28 { 29 return (IsEmbeddedView(virtualPath) || 30 Previous.FileExists(virtualPath)); 31 } 32 33 public override VirtualFile GetFile(string virtualPath) 34 { 35 if (IsEmbeddedView(virtualPath)) 36 { 37 string virtualPathAppRelative = VirtualPathUtility.ToAppRelative(virtualPath); 38 var fullyQualifiedViewName = virtualPathAppRelative.Substring(virtualPathAppRelative.LastIndexOf("/") + 1, virtualPathAppRelative.Length - 1 - virtualPathAppRelative.LastIndexOf("/")); 39 40 var embeddedViewMetadata = _embeddedViews.FindEmbeddedView(fullyQualifiedViewName); 41 return new EmbeddedResourceVirtualFile(embeddedViewMetadata, virtualPath); 42 } 43 44 return Previous.GetFile(virtualPath); 45 } 46 47 public override CacheDependency GetCacheDependency( 48 string virtualPath, 49 IEnumerable virtualPathDependencies, 50 DateTime utcStart) 51 { 52 return IsEmbeddedView(virtualPath) 53 ? null : Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); 54 } 55 }
你們可能注意到GetFile返回的是EmbeddedResourceVirtualFile這個類,這個類繼承自VirtualFile,主要的方法是Open,就是從應用程序集的資源中返回當前資源性視圖的stream。
1 public class EmbeddedResourceVirtualFile : VirtualFile 2 { 3 private readonly EmbeddedViewMetadata _embeddedViewMetadata; 4 5 public EmbeddedResourceVirtualFile(EmbeddedViewMetadata embeddedViewMetadata, string virtualPath) 6 : base(virtualPath) 7 { 8 MicroLibraryExceptionHelper.IsNull(embeddedViewMetadata, this.GetType().FullName, TraceLogType.Error, "embeddedViewMetadata 爲空"); 9 10 this._embeddedViewMetadata = embeddedViewMetadata; 11 } 12 13 public override Stream Open() 14 { 15 Assembly assembly = GetResourceAssembly(); 16 return assembly == null ? null : assembly.GetManifestResourceStream(_embeddedViewMetadata.Name); 17 } 18 19 private Assembly GetResourceAssembly() 20 { 21 return AppDomain.CurrentDomain.GetAssemblies() 22 .Where(assembly => string.Equals(assembly.FullName, _embeddedViewMetadata.AssemblyFullName, StringComparison.InvariantCultureIgnoreCase)) 23 .FirstOrDefault(); 24 } 25 }
第四行,在系統中註冊咱們的EmbeddedViewVirtualPathProvider。
經過上面的四個步驟,就能夠完成資源性視圖的解析工做了,經過MicroLibrary.Presentation.Web.Controls.CommControls.DatePicker.Views.DatePicker來定位cshtml也得以實現。