asp.net mvc源码分析-EditorFor
在我们的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方法有个了解了吧,感觉很是复杂。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构