本文目的
我們來看一個小例子,在一個ASP.NET MVC項目中創建一個控制器Home,只有一個Index:
public class HomeController : Controller { public ActionResult Index() { var model = new DemoModel {Email = "test@test.com"}; return View(model); } } public class DemoModel { [DataType(DataType.EmailAddress)] public string Email { get; set; } }
創建對應的強類型視圖
@model TestMvc.Controllers.DemoModel <fieldset> <legend>DemoModel</legend> <div > @Html.DisplayFor(model => model.Email) </div> </fieldset>
運行一下,如果你的RP不是非常不好的情況下,會出現下面的結果:
生成的是一個Email的鏈接。看一下email部分對應的html源文件:
<div > <a href="mailto:test@test.com">test@test.com</a> </div>
對於不求甚解的人來說,這很正常啊,正常的就像1+1=2一樣。但對於勤學好問的同學來說,問題來了。
為什么生成的是一個email鏈接,不是一個純文本?
因為我用DataType指明了它是email!
那DataType是如何指導Model生成html的?
如果你對這些問題感興趣,請你看下去。
Model的兄弟--ModelMetadata
在介紹Model如何能在View中正常顯示之前,不得不提一下他的兄弟ModelMetadata這個類。ModelMetadata實現對Model的一些特征進行描述(如Model的類型、Model的值,Model要顯示的模板 ),並且實現了對Model的所有屬性值進行描述(遞歸結構)。有了這個兄弟,Model才能知道如何在View中正常顯示【注意:這里特指的是用HtmlHelper的系列擴展方法(如Display/DiaplayFor/EditorFor等)方法在View中顯示。如果你是在View中直接取Model的值用html顯示,你可以暫不用關心ModelMetadata。但!!不要覺得這樣你就不用了解ModelMetadata了,因為MVC中還有一個核心與它息息相關---Model綁定與驗證,所以我建議你還是先認識一下這位好兄弟】,讓我們來看看這個兄弟長什么樣:
public class ModelMetadata { private readonly Type _modelType; //Model的類型 private readonly string _propertyName; //屬性名稱 private object _model; //Model的值 private Func<object> _modelAccessor; //為了用Lambd給Model傳值(這是潮流) private IEnumerable<ModelMetadata> _properties; //Model屬性值的ModelMetadata集合 //通過子類的ComputeXX系列函數將Model的注解特性賦值到下面幾個屬性 public virtual string DataTypeName { get; set; } public virtual string Description { get; set; } public virtual string DisplayFormatString { get; set; } public virtual string DisplayName { get; set; } (其它略,屬性太多了,影響閱讀...)
public object Model { get { if (_modelAccessor != null) { _model = _modelAccessor(); _modelAccessor = null; } return _model; } } public virtual IEnumerable<ModelMetadata> Properties { get { if (_properties == null) { IEnumerable<ModelMetadata> originalProperties = Provider.GetMetadataForProperties(Model, RealModelType); _propertiesInternal = SortProperties(originalProperties.AsArray()); _properties = new ReadOnlyCollection<ModelMetadata>(_propertiesInternal); } return _properties; } } protected ModelMetadataProvider Provider { get; set; } //ModelMetadata的創建者(后面會有介紹) public virtual string TemplateHint { get; set; } //Model顯示用的模板名稱 }
然而在MVC中默認使用的卻不是這個類,而是它的子類CachedDataAnnotationsModelMetadata(其實中間還隔着一個CachedModelMetadata類,因為該類只是做了簡單的封裝,為了方便讀者理解,在此省略)。CachedDataAnnotationsModelMetadata做的最主要的一件事情就是通過一系列的Compute函數得到Model上注解特性的值,並把這些值賦值到對應的屬性。如下:
public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes> { public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, IEnumerable<Attribute> attributes) : base(provider, containerType, modelType, propertyName, new CachedDataAnnotationsMetadataAttributes(attributes.ToArray())) {
PrototypeCache = new CachedDataAnnotationMetadataAttributes(attributes.ToArray()); //這個賦值應該在它的父類CachedModelMetadata中執行的,這里簡化一下,方便閱讀 } protected override string ComputeDataTypeName() { if (PrototypeCache.DataType != null) { return PrototypeCache.DataType.ToDataTypeName(); }return base.ComputeDataTypeName(); }
public override string DataTypeName{ get{ return ComputeDataTypeName();}} }
注意代碼中兩個紅色的部分,把Model的注解特性存在PrototypeCache里面,這樣就可以通過反射得到注解特性的值了。比如文章開始處的DemoModel的屬性Email對應的CachedDataAnnotationsModelMetadata對象中 DataTypeName最終被賦值為“EmailAddress”,等介紹完下一位重量級人物后,會詳解具體調用過程。
ModelMetadata的創建者ModelMetadataProvider
我們還要來認識一位重量級的人物:ModelMetadata的創建者:
public abstract class ModelMetadataProvider { public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType); public abstract ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName); public abstract ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType); }
它是一個純抽象類。該類的第一級子類非常重要,看一下代碼:
public abstract class AssociatedMetadataProvider : ModelMetadataProvider { private static void ApplyMetadataAwareAttributes(IEnumerable<Attribute> attributes, ModelMetadata result) { foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>()) { awareAttribute.OnMetadataCreated(result); } } protected abstract ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName); public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType) {
//得到Model上上面的Attribute AttributeList attributes = new AttributeList(GetTypeDescriptor(modelType).GetAttributes());
//創建ModelMetadata ModelMetadata result = CreateMetadata(attributes, null /* containerType */, modelAccessor, modelType, null /* propertyName */); //創建ModelMetadata后,用所有實現IMetadataAware接口的Attribute對ModelMetadata進行改造 ApplyMetadataAwareAttributes(attributes, result); return result; } protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type) { return TypeDescriptorHelper.Get(type); } }
注意紅色的部分,正是因為有這樣一個處理,用戶就可以通過自已自定義擴展實現IMetadataAware接口,對ModelMetadata進行再加工處理。
根據上面ModelMetadata講解,我們知道,MVC中默認使用的是CachedDataAnnotationsModelMetadata這個類,於是對應ModelMetadataProvider的最終子類CachedDataAnnotationsModelMetadataProvider。
當外部想通過GetMetadataForType來得到ModelMetadata時,內部會調用CreateMetadata,再根據原型模式調用CreateMetadataPrototype或CraeteMetadataFromPrototype實現最終創建過程:
public class CachedDataAnnotationsModelMetadataProvider : CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata> { protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName) { return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes); } protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor) { return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor); } }
同MVC的路由表RouteTable一樣,ModelMetadataProvider也有一個靜態的ModelMetadataProviders變量Current來提供默認的ModelMetadataProvider。簡化代碼如下:
public class ModelMetadataProviders
{
private static ModelMetadataProviders _instance = new ModelMetadataProviders();
private ModelMetadataProvider _currentProvider;
internal ModelMetadataProviders()
{
_currentProvider=new CachedDataAnnotationsModelMetadataProvider();
}
public static ModelMetadataProvider Current
{
get { return _instance._currentProvider; }
set { _instance._currentProvider = value; }
}
}
終極解密
知道了這兩位重量級的大員后,現在我們回到文章開始的代碼:
Html.DisplayFor(model => model.Email)
當View中執行這句時,執行HtmlHelper的擴展函數,正式進入漫長的生成MvcString旅程:
【注意,這個圖中代碼是經過N重簡化而來,實際調用流程復雜無比(全是在TemplateHelpers中跳轉),函數參數多如牛毛,實在不忍讓大家看的痛苦】
紅色線是程序執行的主流程,后來的執行主場景都是在TemplateHelpers里面完成。
從黃色線可以看出ModelMetadata是通過調用ModelMetadata本身的靜態函數FromLambdExpression--->GetMetadataFromProvider,最終由ModelMetadataProviders完成創建。
再來看看桔黃色部分。MVC中有一個DefaultDisplayTemplates的類,存儲了諸如Boolean/String/Html/Email等等數據類型的模板,MVC最終根據ViewData中的ModelMetadata.DataType的值找到對應的模板,最終完成HTML字符串的輸出,作為WebViewPage.ExecutePageHierarchy的一部分(WebViewPage請參看:僅此一文讓你明白ASP.NET MVC 之View的顯示(僅此一文系列二))。
結語
本文只是用了ModelMetadata的一個DataType屬性做為例子,讓你了解在Model中添加注解特性是如何被ModelMetadata使用的。還有其它很多Model注解特性(如ReadOnlyAttribute/RequiredAttribute/UIHintAttribute等等)都和該原理類似。
其實ModelMetadata在view顯示中的作用是有限的,因為很多開發人員都不喜歡用這種前台顯示方式,尤其隨着json的大量使用,與JQUERY等前台的完美結合。但它有屬於它的舞台----Model綁定與驗證,敬請期待!
希望新手看的明白,老手多提意見。如果你覺得對你有幫助,請讓更多人知道它:)