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方法有个了解了吧,感觉很是复杂。

  • 相关阅读:
    LeetCode 345. Reverse Vowels of a String 题解
    LeetCode 344. Reverse String 题解
    LeetCode 27. Remove Element 题解
    LeetCode 61. Rotate List 题解
    LeetCode 19.Remove Nth Node From End of List 题解
    Android耗电量
    Android 使用adb查看和修改电池信息
    Android AOP AspectJ 插桩
    Flask相关用法
    Monkey日志信息的11种Event percentage
  • 原文地址:https://www.cnblogs.com/majiang/p/2779324.html
Copyright © 2011-2022 走看看