深入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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM