zoukankan      html  css  js  c++  java
  • 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方法有个了解了吧,感觉很是复杂。

  • 相关阅读:
    Thinkphp各种方法知识图谱
    Yii2.0学习笔记:创建登录表单
    Yii2.0学习笔记:第一个页面Saying Hello
    4.1
    4
    flask(3.0)
    flask(2.0)
    flask(1.1)装饰器装饰多个视图函数出现的问题
    flask(1.0)
    Flask之上下文管理机制
  • 原文地址:https://www.cnblogs.com/majiang/p/2779324.html
Copyright © 2011-2022 走看看