在我們的mvc日常開發會經常遇到什么LabelFor、EditorFor、Editor等等,這個擴展方法有很多是相似的。這里我們以EditorFor來說說吧,我覺得這個相對要復雜一點。
首先我們來看看EditorFor的定義:
public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object additionalViewData) {
return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData);
}
雖然EditorFor有很多定義,但是實際上都是調用 TemplateHelpers.TemplateFor方法。
internal static MvcHtmlString TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
string templateName, string htmlFieldName, DataBoundControlMode mode,
object additionalViewData) {
return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
}
現在大家應該知道TemplateFor方法的主要參數都有哪些了吧,但是在實際開發中我們的templateName、htmlFieldName、additionalViewData通常都是null,mode是DataBoundControlMode.Edit
我們還是舉一個例子來說說吧:
public class UserInfo
{
[StringLength(100, MinimumLength = 10)]
[Required]
public string UserName { set; get; }
}
@Html.EditorFor(model => model.UserName)
這個代碼是不是很簡單。
現在我們來看看TemplateFor的實現
return templateHelper(html,
ModelMetadata.FromLambdaExpression(expression, html.ViewData),
htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
templateName,
mode,
additionalViewData);
首先我們來看看 ModelMetadata.FromLambdaExpression(expression, html.ViewData)這句是如何獲取ModelMetadata的,具體實現如下:
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); }
如果我們調用的是Editor那么之力調用就是FromStringExpression方法而不是FromLambdaExpression方法,這2個方法相差不大。我們還是來看看FromLambdaExpression這個方法吧:
看了這張圖 那么 return GetMetadataFromProvider(modelAccessor, typeof(TValue), propertyName, containerType);這句代碼里面的參數相信大家都應該是到了吧。
propertyName=“UserName” containerType=MvcApp.Controllers.UserInfo,modelAccessor就是創建一個實例,實例的創建是通過 model => model.UserName這句。至於GetMetadataFromProvider這個方法就沒什么好講的了,前面的文章已經講過了,它實際是創建 了一個DataAnnotationsModelMetadata實例。
至於ExpressionHelper.GetExpressionText(expression)這句說白了默認就是返回一個屬性名稱,具體實現:
public static string GetExpressionText(LambdaExpression expression) { // Split apart the expression string for property/field accessors to create its name Stack<string> nameParts = new Stack<string>(); Expression part = expression.Body; while (part != null) { if (part.NodeType == ExpressionType.Call) { MethodCallExpression methodExpression = (MethodCallExpression)part; if (!IsSingleArgumentIndexer(methodExpression)) { break; } nameParts.Push( GetIndexerInvocation( methodExpression.Arguments.Single(), expression.Parameters.ToArray() ) ); part = methodExpression.Object; } else if (part.NodeType == ExpressionType.ArrayIndex) { BinaryExpression binaryExpression = (BinaryExpression)part; nameParts.Push( GetIndexerInvocation( binaryExpression.Right, expression.Parameters.ToArray() ) ); part = binaryExpression.Left; } else if (part.NodeType == ExpressionType.MemberAccess) { MemberExpression memberExpressionPart = (MemberExpression)part; nameParts.Push("." + memberExpressionPart.Member.Name); part = memberExpressionPart.Expression; } else if (part.NodeType == ExpressionType.Parameter) { // Dev10 Bug #907611 // When the expression is parameter based (m => m.Something...), we'll push an empty // string onto the stack and stop evaluating. The extra empty string makes sure that // we don't accidentally cut off too much of m => m.Model. nameParts.Push(String.Empty); part = null; } else { break; } } // If it starts with "model", then strip that away if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase)) { nameParts.Pop(); } if (nameParts.Count > 0) { return nameParts.Aggregate((left, right) => left + right).TrimStart('.'); } return String.Empty; }
這里的循環執行兩次,第一次是執行
else if (part.NodeType == ExpressionType.MemberAccess) {
MemberExpression memberExpressionPart = (MemberExpression)part;
nameParts.Push("." + memberExpressionPart.Member.Name);
part = memberExpressionPart.Expression;
}
第二次執行
else if (part.NodeType == ExpressionType.Parameter) {
// Dev10 Bug #907611
// When the expression is parameter based (m => m.Something...), we'll push an empty
// string onto the stack and stop evaluating. The extra empty string makes sure that
// we don't accidentally cut off too much of m => m.Model.
nameParts.Push(String.Empty);
part = null;
}
當然這個方法默認的返回結果這里就是UserName了,它默認就是生成html是的id和name屬性的值。
現在我們再來看看TemplateHelper方法了
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; } string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString; if (metadata.Model != null && !String.IsNullOrEmpty(formatString)) { formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model); } // Normally this shouldn't happen, unless someone writes their own custom Object templates which // don't check to make sure that the object hasn't already been displayed object visitedObjectsKey = metadata.Model ?? metadata.RealModelType; if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey)) { // DDB #224750 return String.Empty; } 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); }
這個方法其實也很簡單,獲取當前model的值,以及呈現html的format格式,最后這里從新創建了一個ViewDataDictionary實例 viewData,並且把參數中的additionalViewData也合並到這個viewData中來,把當前的值 (visitedObjectsKey也就是最后呈現給textbox的value)給添加到viewData的VisitedObjects屬性中。最 后再調用
return executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions);方法。
那么現在我們應該看看ExecuteTemplate方法了:
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 ) ); }
這里的GetActionCache方法很簡單就是從當前的context.Items中獲取一個字典數據,如果沒有就實例化一個然后加入到context.Items中。GetDefaultActions方法也很簡單
internal static Dictionary<string, Func<HtmlHelper, string>> GetDefaultActions(DataBoundControlMode mode) {
return mode == DataBoundControlMode.ReadOnly ? defaultDisplayActions : defaultEditorActions;
}
其中GetViewNames就是獲取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; } } }
默認返回參數順序是templateHints中的view,其次就是根據參數 數據類型返回相應的默認view。我這里返回的是String。
那么現在我們回到ExecuteTemplate方法中來,
string fullViewName = modeViewPath + "/" + viewName;這句就是已經找到我們的view了,如果我們先前actionCache中包含該key就直接執行該view並返回,其次通過 ViewEngines.Engines.FindPartialView來找該view,如果找到則輸出該view並返回。否則調用默認的處理方式
if (defaultActions.TryGetValue(viewName, out defaultAction)) {
actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction };
return defaultAction(MakeHtmlHelper(html, viewData));
}
這里的defaultAction對應則DefaultDisplayTemplates.StringTemplate,因為我的 viewName是String,這里的MakeHtmlHelper方法是根據當前的ViewContext和viewData從新實例化一個 HtmlHelper。
DefaultEditorTemplates.StringTemplate方法非常簡單:
return html.TextBox(String.Empty,
html.ViewContext.ViewData.TemplateInfo.FormattedModelValue,
CreateHtmlAttributes("text-box single-line")).ToHtmlString();
它里面主要是調用TextBox方法:
public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes) {
return InputHelper(htmlHelper, InputType.Text, null, name, value, (value == null) /* useViewData */, false /* isChecked */, true /* setId */, true /* isExplicitValue */, htmlAttributes);
}
private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, IDictionary<string, object> htmlAttributes) { string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); if (String.IsNullOrEmpty(fullName)) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name"); } TagBuilder tagBuilder = new TagBuilder("input"); tagBuilder.MergeAttributes(htmlAttributes); tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType)); tagBuilder.MergeAttribute("name", fullName, true); string valueParameter = Convert.ToString(value, CultureInfo.CurrentCulture); bool usedModelState = false; switch (inputType) { case InputType.CheckBox: bool? modelStateWasChecked = htmlHelper.GetModelStateValue(fullName, typeof(bool)) as bool?; if (modelStateWasChecked.HasValue) { isChecked = modelStateWasChecked.Value; usedModelState = true; } goto case InputType.Radio; case InputType.Radio: if (!usedModelState) { string modelStateValue = htmlHelper.GetModelStateValue(fullName, typeof(string)) as string; if (modelStateValue != null) { isChecked = String.Equals(modelStateValue, valueParameter, StringComparison.Ordinal); usedModelState = true; } } if (!usedModelState && useViewData) { isChecked = htmlHelper.EvalBoolean(fullName); } if (isChecked) { tagBuilder.MergeAttribute("checked", "checked"); } tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue); break; case InputType.Password: if (value != null) { tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue); } break; default: string attemptedValue = (string)htmlHelper.GetModelStateValue(fullName, typeof(string)); tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName) : valueParameter), isExplicitValue); break; } if (setId) { tagBuilder.GenerateId(fullName); } // If there are any errors for a named field, we add the css attribute. ModelState modelState; if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState)) { if (modelState.Errors.Count > 0) { tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); } } tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); if (inputType == InputType.CheckBox) { // Render an additional <input type="hidden".../> for checkboxes. This // addresses scenarios where unchecked checkboxes are not sent in the request. // Sending a hidden input makes it possible to know that the checkbox was present // on the page when the request was submitted. StringBuilder inputItemBuilder = new StringBuilder(); inputItemBuilder.Append(tagBuilder.ToString(TagRenderMode.SelfClosing)); TagBuilder hiddenInput = new TagBuilder("input"); hiddenInput.MergeAttribute("type", HtmlHelper.GetInputTypeString(InputType.Hidden)); hiddenInput.MergeAttribute("name", fullName); hiddenInput.MergeAttribute("value", "false"); inputItemBuilder.Append(hiddenInput.ToString(TagRenderMode.SelfClosing)); return MvcHtmlString.Create(inputItemBuilder.ToString()); } return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing); }
這里的InputHelper是真正生成html字符串的地方。這個方法整體比較好理解,不過要注意這個方法里面有這么一句
tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
這句就是處理對象上面的那些驗證屬性,效果如圖:
我想大家到這里應該對EditorFor方法有個了解了吧,感覺很是復雜。