這部分的內容和前面的MVC頁面的生命周期關系不是太緊密,但在開發中也是十分重要的部分,它可以幫助方便生成合適的html,包括自動填充model的值到表單中,這可以使得通過表單提交的數據在提交頁面之后不會丟失,這在asp.net web form中是通過viewstate來實現的,asp.net mvc采用了完全不同的方式,個人認為mvc的方式更加好一些。本文將以Html.Editor,EditorFor為例分析其實現。ASP.NET MVC的Editor,Text,Display等這一系列的helper方法的擴展性都是非常好的,支持自定義顯示的template,但是它也有默認的實現。 Editor和EditorFor是很類似的,只是一個接受字符串作為參數,一個用強類型的Lambda表達式。先看Editor:
public static MvcHtmlString Editor(this HtmlHelper html, string expression) { return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */); }
類似Editor這一類的helper最終都是靠TemplateHelper這個類的方法來完成的,Editor方法只是把最基本的參數傳給合適的方法,跳過一個TemplateHelper的簡單重載的方法,直接到最核心的地方:
internal static string Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData, TemplateHelperDelegate templateHelper) { return templateHelper(html, ModelMetadata.FromStringExpression(expression, html.ViewData), htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, mode, additionalViewData); }
這里調用了兩個重要的方法,一個是獲得ModelMeta的,Editor方法會根據Metadata顯示合適的label等:
public static ModelMetadata FromStringExpression(string expression, ViewDataDictionary viewData) { if (expression.Length == 0) { // Empty string really means "model metadata for the current model" return FromModel(viewData); } ViewDataInfo vdi = viewData.GetViewDataInfo(expression); Type containerType = null; Type modelType = null; Func<object> modelAccessor = null; string propertyName = null; if (vdi != null) { if (vdi.Container != null) { containerType = vdi.Container.GetType(); } modelAccessor = () => vdi.Value; if (vdi.PropertyDescriptor != null) { propertyName = vdi.PropertyDescriptor.Name; modelType = vdi.PropertyDescriptor.PropertyType; } else if (vdi.Value != null) { // We only need to delay accessing properties (for LINQ to SQL) modelType = vdi.Value.GetType(); } } // Try getting a property from ModelMetadata if we couldn't find an answer in ViewData else if (viewData.ModelMetadata != null) { ModelMetadata propertyMetadata = viewData.ModelMetadata.Properties.Where(p => p.PropertyName == expression).FirstOrDefault(); if (propertyMetadata != null) { return propertyMetadata; } } return GetMetadataFromProvider(modelAccessor, modelType ?? typeof(string), propertyName, containerType); }
首先調用ViewDataDictionary的GetViewDataInfo方法來獲得viewData中expression所代表的值。在這個方法內部,調用了一個幫助類ViewDataEvaluator的方法:
public static ViewDataInfo Eval(ViewDataDictionary vdd, string expression) { //Given an expression "foo.bar.baz" we look up the following (pseudocode): // this["foo.bar.baz.quux"] // this["foo.bar.baz"]["quux"] // this["foo.bar"]["baz.quux] // this["foo.bar"]["baz"]["quux"] // this["foo"]["bar.baz.quux"] // this["foo"]["bar.baz"]["quux"] // this["foo"]["bar"]["baz.quux"] // this["foo"]["bar"]["baz"]["quux"] ViewDataInfo evaluated = EvalComplexExpression(vdd, expression); return evaluated; }
這里的注釋說明了一個expression是如何被解析的。
private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression) { foreach (ExpressionPair expressionPair in GetRightToLeftExpressions(expression)) { string subExpression = expressionPair.Left; string postExpression = expressionPair.Right; ViewDataInfo subTargetInfo = GetPropertyValue(indexableObject, subExpression); if (subTargetInfo != null) { if (String.IsNullOrEmpty(postExpression)) { return subTargetInfo; } if (subTargetInfo.Value != null) { ViewDataInfo potential = EvalComplexExpression(subTargetInfo.Value, postExpression); if (potential != null) { return potential; } } } } return null; }
GetRightToLeftExpressions會將一個expression拆成左右兩對,例如 foo.bar.baz會拆成
foo.bar.baz “”
foo.bar baz
foo. bar.baz
首先利用GetProperty方法獲得左側expression的值,如果右側expression不為空串,則在右側取得的值的基礎上對右側的expression再重復這個過程,就是:
ViewDataInfo potential = EvalComplexExpression(subTargetInfo.Value, postExpression);
這樣最終實現的效果就是上文注釋所描述的。
下面看下針對一個expression,它是如何獲得他的值的,
private static ViewDataInfo GetPropertyValue(object container, string propertyName) { // This method handles one "segment" of a complex property expression // First, we try to evaluate the property based on its indexer ViewDataInfo value = GetIndexedPropertyValue(container, propertyName); if (value != null) { return value; } // If the indexer didn't return anything useful, continue... // If the container is a ViewDataDictionary then treat its Model property // as the container instead of the ViewDataDictionary itself. ViewDataDictionary vdd = container as ViewDataDictionary; if (vdd != null) { container = vdd.Model; } // If the container is null, we're out of options if (container == null) { return null; } // Second, we try to use PropertyDescriptors and treat the expression as a property name PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propertyName, true); if (descriptor == null) { return null; } return new ViewDataInfo(() => descriptor.GetValue(container)) { Container = container, PropertyDescriptor = descriptor }; }
這里的注釋寫的很清楚,首先這個container可能就是一個IDictionary對象,那么就把這個expression來做為key來取得它的值,或者它是一個對象,那么就把這個expression當作它的一個property name去取值。這兩種情況放在GetIndexedProperty方法里面實現了,這個方法思路很簡單,實際上還是比較繁瑣的,用到了很多反射的技巧,這里不展開。如果通過這兩種方案沒有獲得到值,那么很有可能這個container是一個ViewDataDictionary,真正的值在它的Model屬性中,因此將他的Model取出來,expression作為property name,獲得它的值。
綜上分析,Editor方法在獲取值的時候有兩個來源,一是ViewData中的數據,二是ViewData的Model中的數據,而且前者是優先的。例如有如下的action方法:
public ActionResult Index2() { var p = new Person { Name = "abc", Add = new Address { City = "city", Street = "st" } }; ViewBag.Name = "view Bag"; return View(p); }
在View中,有如下方法:
@Html.Editor("Name");
那么頁面上顯示的是什么?沒錯,是view Bag。
獲得值之后,還需要獲得ModelMetaData,這是在最后return的時候調用了GetMetadataFromProvider方法:
private static ModelMetadata GetMetadataFromProvider(Func<object> modelAccessor, Type modelType, string propertyName, Type containerType) { if (containerType != null && !String.IsNullOrEmpty(propertyName)) { return ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor, containerType, propertyName); } return ModelMetadataProviders.Current.GetMetadataForType(modelAccessor, modelType); }
這里先補充下ModelMetaData的基本知識,有一篇博客很好的介紹了ModelMetaData類中的各個屬性的含義。ModelMetaData被View,具體的說,是各個html helper方法用來控制生成的html。這些信息是通過ModelMetadataProvider對象來獲得的,asp.net mvc默認的ModelMetadataProvider是DataAnnotationsModelMetadataProvider,這個provider是通過讀取model類上的attribute來獲得信息的。DataAnnotationsModelMetadataProvider的實現本文不多做分析,以后另文介紹。
回到最早的Template方法中,第三個參數是,htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), 不是很明白這個參數的含義,通常情況下htmlFieldName都是null,暫時不管它,后一個方法可以獲得表單name屬性的值。看這個Template方法,經過幾個重載以后(這里的方法重載很多,有點繞),真正執行的是(有刪節):
internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate) { // TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because // the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically // decorates it with a [ReadOnly] attribute... if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model)) { metadata.Model = null; } object formattedModelValue = metadata.Model; if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly) { formattedModelValue = metadata.NullDisplayText; } ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData) { Model = metadata.Model, ModelMetadata = metadata, TemplateInfo = new TemplateInfo { FormattedModelValue = formattedModelValue, HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName), VisitedObjects = new HashSet<object>(html.ViewContext.ViewData.TemplateInfo.VisitedObjects), // DDB #224750 } }; if (additionalViewData != null) { foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData)) { viewData[kvp.Key] = kvp.Value; } } viewData.TemplateInfo.VisitedObjects.Add(visitedObjectsKey); // DDB #224750 return executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions); }
這個方法主要是做一些准備工作,將數據准備好,傳給executeTemplate委托去執行,這個委托其實就是在同一個類中定義的一個方法,這里的代碼大量使用了各種委托,看起來比較費勁,這個方法中的GetViewName,GetDefaultActions也都是委托。
下面是核心方法,此時各種數據都已經准備好,將生成html:
internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions) { Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html); Dictionary<string, Func<HtmlHelper, string>> defaultActions = getDefaultActions(mode); string modeViewPath = modeViewPaths[mode]; foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName)) { string fullViewName = modeViewPath + "/" + viewName; ActionCacheItem cacheItem; if (actionCache.TryGetValue(fullViewName, out cacheItem)) { if (cacheItem != null) { return cacheItem.Execute(html, viewData); } } else { ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName); if (viewEngineResult.View != null) { actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName }; using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) { viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
return writer.ToString(); } } Func<HtmlHelper, string> defaultAction; if (defaultActions.TryGetValue(viewName, out defaultAction)) { actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction }; return defaultAction(MakeHtmlHelper(html, viewData)); } actionCache[fullViewName] = null; } } throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, MvcResources.TemplateHelpers_NoTemplate, viewData.ModelMetadata.RealModelType.FullName ) ); }
先忽略緩存的部分,它首先去找viewName,可以認為在調用Editor,或者EditorFor顯示的這些html,其實也是一個Partial View,它和View初始化的時候一樣,有一套尋找view文件的規則。看其查找View的代碼:
internal static IEnumerable<string> GetViewNames(ModelMetadata metadata, params string[] templateHints) { foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s))) { yield return templateHint; } // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>) Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType; // TODO: Make better string names for generic types yield return fieldType.Name; if (!metadata.IsComplexType) { yield return "String"; } else if (fieldType.IsInterface) { if (typeof(IEnumerable).IsAssignableFrom(fieldType)) { yield return "Collection"; } yield return "Object"; } else { bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType); while (true) { fieldType = fieldType.BaseType; if (fieldType == null) break; if (isEnumerable && fieldType == typeof(Object)) { yield return "Collection"; } yield return fieldType.Name; } } }
從上面代碼可以看到, ASP.NET MVC按照以下順序去找template 的 viewName
1. 通過直接指定文件名.
2.Model的類型名
3. 默認的類型,比如string,object和collection。
得到了ViewName之后,通過
string fullViewName = modeViewPath + "/" + viewName;
獲得完整的ViewName,看下這里的modeViewPath:
static readonly Dictionary<DataBoundControlMode, string> modeViewPaths = new Dictionary<DataBoundControlMode, string> { { DataBoundControlMode.ReadOnly, "DisplayTemplates" }, { DataBoundControlMode.Edit, "EditorTemplates" } };
得到完整的ViewName之后,如果對應view文件存在,就會調用ViewEngine的FindPartialView方法,這種情況下這個editor方法就相當於是一個partial view。舉例說明下,根據前文的分析,如果要讓asp.net找到自定義的模版,需要在類似這樣的地方放置partial view文件,其他符合規則的地方也可以:
這個文件的內容如下:
<p>Hello @ViewData.ModelMetadata.SimpleDisplayText</p>
頁面view文件:
@Html.Editor("Add","MyTemplate");
輸出的結果自然是:
Hello city
自定義模版還有其他方法,過會兒再說,先看另一種情況,就是這個對應的view文件不存在,也就是默認的情形下,它是從defaultAction中找到合適的action,然后執行。defaultAction是這樣定義的,這是Editor的,Display也有相似的一系列:
static readonly Dictionary<string, Func<HtmlHelper, string>> defaultEditorActions = new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) { { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate }, { "MultilineText", DefaultEditorTemplates.MultilineTextTemplate }, { "Password", DefaultEditorTemplates.PasswordTemplate }, { "Text", DefaultEditorTemplates.StringTemplate }, { "Collection", DefaultEditorTemplates.CollectionTemplate }, { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate }, { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate }, { typeof(string).Name, DefaultEditorTemplates.StringTemplate }, { typeof(object).Name, DefaultEditorTemplates.ObjectTemplate }, };
這里簡單看下最簡單的StringTemplate,其他的就不展開了,並不是很難,但是細節很多:
internal static string StringTemplate(HtmlHelper html) { return html.TextBox(String.Empty, html.ViewContext.ViewData.TemplateInfo.FormattedModelValue, CreateHtmlAttributes("text-box single-line")).ToHtmlString(); }
這個TextBox實際上是調用了一個InputExtension類中的InputHelper方法生成了一個input,這里不再贅述。
下面再看下EditorFor,EditorFor和Editor的主要區別就是接受的表達式不一樣。前者接受lambda表達式。因此,這兩個方法的實現的不同也僅在很少的地方,前者在獲得ModelMetadata的時候調用的是ModelMetaData.FromLambdaExpression方法:
public static ModelMetadata FromLambdaExpression<TParameter, TValue>(Expression<Func<TParameter, TValue>> expression, ViewDataDictionary<TParameter> viewData) { if (expression == null) { throw new ArgumentNullException("expression"); } if (viewData == null) { throw new ArgumentNullException("viewData"); } string propertyName = null; Type containerType = null; bool legalExpression = false; // Need to verify the expression is valid; it needs to at least end in something // that we can convert to a meaningful string for model binding purposes switch (expression.Body.NodeType) { // ArrayIndex always means a single-dimensional indexer; multi-dimensional indexer is a method call to Get() case ExpressionType.ArrayIndex: legalExpression = true; break; // Only legal method call is a single argument indexer/DefaultMember call case ExpressionType.Call: legalExpression = ExpressionHelper.IsSingleArgumentIndexer(expression.Body); break; // Property/field access is always legal case ExpressionType.MemberAccess: MemberExpression memberExpression = (MemberExpression)expression.Body; propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null; containerType = memberExpression.Expression.Type; legalExpression = true; break; // Parameter expression means "model => model", so we delegate to FromModel case ExpressionType.Parameter: return FromModel(viewData); } if (!legalExpression) { throw new InvalidOperationException(MvcResources.TemplateHelpers_TemplateLimitations); } TParameter container = viewData.Model; Func<object> modelAccessor = () => { try { return CachedExpressionCompiler.Process(expression)(container); } catch (NullReferenceException) { return null; } }; return GetMetadataFromProvider(modelAccessor, typeof(TValue), propertyName, containerType); }
這里的注釋也寫的比較清楚,主要是獲得modelAccessor,也就是將lambda表達式轉換成一個delegate的過程,這里出於效率的考慮,用了一個CachedExpressionCompiler 這個類來專門處理,這里用到的技術太技術性了,不在此分析,有時間再深入研究下。如果不考慮性能,這里其實並不復雜,最兩段代碼等效於:
modelAccessor = ()=>expression.Compile()(container); return GetMetadataFromProvider(modelAccessor, typeof(TValue), propertyName, containerType);
最后,結合上面的分析,看下自定義template的一些方法。出於某些原因,我們可能需要重新生成所有asp.net mvc默認的html代碼,例如,如果我們采用bootstrap作為我的前端樣式,需要在生成的html中加上bootstrap規定的class,我們可以覆蓋string的默認模版。如果DefaultTemplates可以重寫那就最好了,不過至少目前他不是public的,此路不通,但是注意到GetViewNames的時候,它首先會返回類型名String,因此,我們可以創建一個名字為String的partial view,放在合適的路徑下,就可以覆蓋其默認的模版了。例如,在如下路徑建一個String.cshtml
內容如下:
@{var display = ViewData.ModelMetadata.DisplayName ?? ViewData.ModelMetadata.PropertyName;} <div class="control-group"> <label class="control-label">@display</label> <div class="controls"> <input type="text" value="@ViewData.ModelMetadata.SimpleDisplayText" > </div> </div>
那么,view中的
<form class="form-horizontal"> @Html.EditorFor(p => p.Name) </form>
就會顯示成bootstrap默認的html樣式。當然大多數情況下,直接用html標簽來覆蓋基礎類型String,Object並不是一個好的選擇,因為它默認的實現比較復雜和全面,自己的實現往往會失去一些默認的功能,這里僅是舉例。在mvc自帶的helper方法的基礎上,重新實現類似Editor之類的方法是更好的選擇。
下面再舉一個例子,利用自定義的template實現對Enum的顯示。在Shared->EditorTemplates新建一個Enum.cshtml的partial view, 內容如下:
@model Enum @using System.Linq; @Html.DropDownListFor(m=>m,Enum.GetValues(Model.GetType()).Cast<Enum>() .Select(m=>{ string val=Enum.GetName(Model.GetType(),m); return new SelectListItem(){ Selected=(Model.ToString()==val), Text=val, Value=val };}) )
假如有如下Enum:
public enum AddressType { BUSSINESS, HOME } public class Address { [Required] [Display(Name="City Name")] public string City { get; set; } public string Street { get; set; } [UIHint("Enum")] public AddressType AddressType { get; set; } }
在model上通過UIHint指定所要使用的template,那么@Html.EditorFor(p => p.Add.AddressType)生成的html效果就是如下: