zoukankan      html  css  js  c++  java
  • 深入ASP.NET MVC 之八:Model Template与Html.Editor的实现

    这部分的内容和前面的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。

    image

    获得值之后,还需要获得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文件,其他符合规则的地方也可以:

    image

    这个文件的内容如下:

    <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

    image

    内容如下:

    @{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效果就是如下:

    image

  • 相关阅读:
    1.根据文件路径文件或文件夹图标获取 2.根据后缀获取系统图标
    多线程处理一个问题和多个问题
    C#汉字转换拼音技术详解
    委托小实例
    Winform 线程
    word 转换成 pdf
    几道算法题目
    双色球生成器
    c# Invoke和BeginInvoke
    C# 委托和事件2
  • 原文地址:https://www.cnblogs.com/yinzixin/p/2821172.html
Copyright © 2011-2022 走看看