本文目的
我們來看一個小例子,在一個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綁定與驗證,敬請期待!
希望新手看的明白,老手多提意見。如果你覺得對你有幫助,請讓更多人知道它:)

