背景:
在Asp.net MVC中定義模型的時候,DataType有DataType.ImageUrl這個類型,但htmlhelper卻無法輸出一個img,當用腳手架自動生成一些form或表格的時候,這些Url字段總是需要再手動改一次,特別是我想在img上面包裹一個a標簽。並限定大小,比如:
<a href="url" target="_blank"> <img src="url" style="width: 100px;"/></a>
方法1:分部視圖
在做后台表格的時候經常要修改這樣的問題,於是首先想到的就是做一個分部視圖,叫tableimg。
@model string @if (!string.IsNullOrEmpty(Model)) { <a href="@Model" target="_blank"> <img src="@Model" style="width: 100px;"/></a> }
使用的時候:
@Html.Partial("tableimg",Model.Img)
方便是方便了些,但還是不夠靈活。寬度是寫死的;而且還要記住這個視圖,如果這樣的片段多了都不知道誰是誰了;和腳手架生成的代碼TextBoxFor,DisplayFor等風格也不一樣;如果要增加參數呢,還得去改模型。
方法2:UIHint
這個方法和分部視圖相似,也是使用模板,需要先在shared文件夾下創建一個EditorTemplates文件夾,然后新建一個視圖。這里命名為ImageLink。內容和上面一樣。
@model string @if (!string.IsNullOrEmpty(Model)) { <a href="@Model" target="_blank"> <img src="@Model" style="width: 100px;"/></a> }
只是調用方法不一樣:
[DataType(DataType.ImageUrl)] [UIHint("ImageLink")] public string Img { get; set; }
在視圖里面通過EditorFor調用:
@Html.EditorFor(model => model.Img)
這修改的地方比較多,感覺不太舒服。能不能一勞永逸呢?當然是可以的,這需要自定義一個ModelMetadataProvider,來告訴MVC這個數據類型的屬性就用這個模板顯示。
public class ImageModelMetadataProvider : DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { var meta= base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); if (meta.DataTypeName==DataType.ImageUrl.ToString() && string.IsNullOrEmpty(meta.TemplateHint)) { meta.TemplateHint = "ImageLink"; } return meta; } }
ModelMetadata是用來描述模型數據結構的數據,比如數據類型、約束、顯示名稱等,而ModelMetadataProvider就是用來提供Model的模型元數據的。
然后全局注冊:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ModelMetadataProviders.Current = new ImageModelMetadataProvider(); }
模型的定義里面,不再需要加UiHint了
[DataType(DataType.ImageUrl)] public string Img { get; set; }
視圖里面調用的時候,需要用EditorFor。回頭看一下,這種方式還是不夠靈活,要實現一個效果,首先要增加一個模板,然后注冊模型元數據提供器,然后每一個要顯示計划效果的模型還要強制的使用DataType特性以及Html.EditorFor輸出,這讓人有點束縛的感覺。
可不可以只改一個地方呢?於是想到擴展htmlhelper
方法3:Html.Image
新建一個靜態類,Htmlhelpers,增加一個Image的擴展方法,有url和length兩個參數。用tagbuilder創建標簽,增加屬性。
public static MvcHtmlString Image(this HtmlHelper helper, string url, int length) { var tagA = new TagBuilder("a"); tagA.MergeAttribute("href", url); tagA.MergeAttribute("target", "_blank"); var img = new TagBuilder("img"); img.MergeAttribute("src", url); img.MergeAttribute("style", string.Format("width:{0}px", length)); tagA.InnerHtml = img.ToString(); return MvcHtmlString.Create(tagA.ToString()); }
最后返回MvcHtmlString ,但上面體現不了tagbuilder的好處。如果覺得寫tag比較麻煩,可以這樣:
var str= string.Format("<a href='{0}' target='_blank'> <img src='{0}' style='width:{1}px;'/></a>", url, length); return MvcHtmlString.Create(str);
調用的時候傳入參數:
@Html.Image(Model.Img,100)
結果顯示ok:
但如果要增加寬度以及更多的樣式,想將這個img的id指定為模型屬性的名字呢 ,那就得用ImageFor了。
方法4:Html.ImageFor
開始不會寫,就想到參考MVC源碼,於是用強大的ILSpy(直接把System.Web.MVC.dll拖進來)找到了System.Web.MVC.HTML中的源碼,直接可以看到LabelExtension和DisplayExtension等,常用的TextBoxFor位於InputExtension。
所以這里我借鑒了上面的方法,先產生一個img,在用a表情包裹着。這里如果還用string.Format那就太糟糕了。
internal static MvcHtmlString ImageHelper(HtmlHelper html, ModelMetadata metadata, IDictionary<string, object> htmlAttributes = null) { //屬性值 var value = metadata.Model.ToString(); //屬性名 if (string.IsNullOrEmpty(value)) { return MvcHtmlString.Empty; } var img = new TagBuilder("img"); img.Attributes.Add("src", value); img.Attributes.Add("id", metadata.PropertyName);
img.MergeAttributes(htmlAttributes, true);
var tagA = new TagBuilder("a"); tagA.MergeAttribute("href",value); tagA.MergeAttribute("target", "_blank"); tagA.InnerHtml = img.ToString(); return MvcHtmlString.Create(tagA.ToString()); } public static MvcHtmlString ImageFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) { ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); //var propertyName = ExpressionHelper.GetExpressionText(expression); //也能獲取到屬性名 var htmlAttributes2 = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); return ImageHelper(html, modelMetadata, htmlAttributes2); } public static MvcHtmlString ImageFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) { return ImageFor(html, expression, null); }
ImageHelper方法用來負責生產自己想要的標簽。包含三個參數,htmlhelper、modelmetadata、htmlAttributes。htmlhelper不用多說,頁面上就是用它來生成各種元素,但這里沒有使用它。modelmetadata就是模型元數據,它描述了Model的數據結構,以及Model的每個數據成員的一些特性。正是有了Model元數據的存在,才使模板化HTML的呈現機制成為可能。這里主要用來獲取模型的值,也就是對應的url值。通過斷點我們可以了解到它包含了寫什么:
詳情可以移步Artech大神的博客:ASP.NET MVC Model元數據及其定制: 初識Model元數據 。htmlAttributes就一目了然了。就是樣式字典。但我們在寫的時候,都是傳入的是object,比如:
@Html.ImageFor(n=>Model.Img,new{width = "100px"} )
這后面的new{wdith='100px'}本質上就是一個匿名對象,匿名對象的最大的好處就是屬性可以自定義,想加什么樣式就加什么樣式,然后通過htmlhelper的方法轉換為IDictionary<string, object> htmlAttributes 結構
var htmlAttributes2 = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
在看下這個源碼里面是如何實現的
通過類型解釋器拿到匿名對象的所有屬性的屬性解釋器。再添加到集合里面去。這樣tagbuilder的MergeAttribute方法就好處理這些樣式或者屬性鍵值對了。
而模型元數據通過處理Lambda表達式和得到:
ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
內部是由ModelMetadataProvider實現,ModelMetadataProvider是一個抽象類,提供了三個抽象方法:
public abstract class ModelMetadataProvider { protected 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); }
AssociatedMetadataProvider繼承ModelMetadataProvider,上面用到的DataAnnotationsModelMetadataProvider是繼承AssociatedMetadataProvider。這里artech講的比較多,詳情請移步:ASP.NET MVC的Model元數據提供機制的實現 更多深入知識暫且打住。這個時候我們的ImageFor方法已經可以用了。
@Html.ImageFor(n=>Model.Img) <br/> @Html.ImageFor(n=>Model.Img,new{width = "100px"} ) <br/>
生成的html:
<a href="http://photocdn.sohu.com/20160629/Img456995877.jpeg" target="_blank"><img id="Img" src="http://photocdn.sohu.com/20160629/Img456995877.jpeg"></a> <a href="http://photocdn.sohu.com/20160629/Img456995877.jpeg" target="_blank"><img id="Img" src="http://photocdn.sohu.com/20160629/Img456995877.jpeg" width="100px"></a>
這樣就自在多了。由此我們也可以擴展其他的For方法。
Html.EnumToDropDownList
有了這個思路,順手把枚舉類型的問題也解決下,大家曉得的,給枚舉類型加Display特性形同虛設。我們一般是希望枚舉類型能夠顯示中文,值是枚舉就行。比如有枚舉:
public enum QuestionType { [Display(Name = "單選")] Single,
[Display(Name = "多選")] Multiple, [Display(Name = "判斷")] Jude, [Display(Name = "填空")] Blank, [Display(Name = "問答")] Question }
如果視圖上這樣寫:
@Html.DropDownListFor(n => n.QuestionType, new SelectList(Enum.GetValues(typeof(QuestionType))))
只能得到英文的下拉框:
網上還有用方法二解決枚舉類型顯示問題的例子。其實擴展htmlhelp方法最簡單,定義一個EnumToDropDownList的方法,參數是枚舉和name。
public static MvcHtmlString EnumToDropDownList(this HtmlHelper helper, Enum eEnum,string name) { var selectList = new List<SelectListItem>(); var enumType = eEnum.GetType(); foreach (var value in Enum.GetValues(enumType)) { var field = enumType.GetField(value.ToString()); var option = new SelectListItem() { Value = value.ToString() }; var display = field.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() as DisplayAttribute; option.Text = display != null ? display.Name : value.ToString(); option.Selected = Equals(value, eEnum); selectList.Add(option); } return helper.DropDownList(name, selectList); }
先通過Enum.GetValues方法得到枚舉類型的各個值,然后通過反射得到DisplayAttribute特性。然后將獲取到name作為下拉框option的Text。調用:
@Html.EnumToDropDownList(Model.QuestionType, "QuestionType")
EnumToDropDownListFor實現起來就簡單啦,關鍵是找到類型。
public static MvcHtmlString EnumToDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes=null) { ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); var htmlAttributes2 = AnonymousObjectToHtmlAttributes(htmlAttributes); var enumType = modelMetadata.ModelType; var selectList = new List<SelectListItem>(); foreach (var value in Enum.GetValues(enumType)) { var field = enumType.GetField(value.ToString()); var option = new SelectListItem() { Value = value.ToString() }; var display = field.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() as DisplayAttribute; option.Text = display != null ? display.Name : value.ToString(); option.Selected = Equals(value, modelMetadata.Model); selectList.Add(option); } return html.DropDownList(modelMetadata.PropertyName, selectList,htmlAttributes2);
調用更加簡單:
@Html.EnumToDropDownListFor(model => model.QuestionType)
結果一樣,且可以擴展樣式,匹配選中。如果要再后端顯示枚舉的文字,也很簡單了:
public string GetEnumTxt(Enum eEnum) { var enumType = eEnum.GetType(); var field = enumType.GetField(eEnum.ToString()); var display = field.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() as DisplayAttribute; return display != null ? display.Name : eEnum.ToString(); }
helper代碼

public static class HtmlHelpers { public static MvcHtmlString Image(this HtmlHelper helper, string url, int length) { var tagA = new TagBuilder("a"); tagA.MergeAttribute("href", url); tagA.MergeAttribute("target", "_blank"); var img = new TagBuilder("img"); img.MergeAttribute("src", url); img.MergeAttribute("style", string.Format("width:{0}px", length)); tagA.InnerHtml = img.ToString(); //return string.Format("<a href='{0}' target='_blank'> <img src='{0}' style='width:{1}px;'/></a>", url, length); return MvcHtmlString.Create(tagA.ToString()); } public static MvcHtmlString EnumToDropDownList(this HtmlHelper helper, Enum eEnum,string name) { var selectList = new List<SelectListItem>(); var enumType = eEnum.GetType(); foreach (var value in Enum.GetValues(enumType)) { var field = enumType.GetField(value.ToString()); var option = new SelectListItem() { Value = value.ToString() }; var display = field.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() as DisplayAttribute; option.Text = display != null ? display.Name : value.ToString(); option.Selected = Equals(value, eEnum); selectList.Add(option); } return helper.DropDownList(name, selectList); } public static MvcHtmlString EnumToDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes=null) { ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); var htmlAttributes2 = AnonymousObjectToHtmlAttributes(htmlAttributes); var enumType = modelMetadata.ModelType; var selectList = new List<SelectListItem>(); foreach (var value in Enum.GetValues(enumType)) { var field = enumType.GetField(value.ToString()); var option = new SelectListItem() { Value = value.ToString() }; var display = field.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() as DisplayAttribute; option.Text = display != null ? display.Name : value.ToString(); option.Selected = Equals(value, modelMetadata.Model); selectList.Add(option); } return html.DropDownList(modelMetadata.PropertyName, selectList,htmlAttributes2); } public static MvcHtmlString A(this HtmlHelper helper, string text, string url, int id) { var tagA = new TagBuilder("a"); tagA.MergeAttribute("href", url); tagA.MergeAttribute("data-id", id.ToString()); tagA.InnerHtml = text; return MvcHtmlString.Create(tagA.ToString()); } internal static MvcHtmlString ImageHelper(HtmlHelper html, ModelMetadata metadata, IDictionary<string, object> htmlAttributes = null) { //屬性值 var value = metadata.Model.ToString(); if (string.IsNullOrEmpty(value)) { return MvcHtmlString.Empty; } var img = new TagBuilder("img"); img.Attributes.Add("src", value); //屬性名 img.Attributes.Add("id", metadata.PropertyName); img.MergeAttributes(htmlAttributes, true); var tagA = new TagBuilder("a"); tagA.MergeAttribute("href",value); tagA.MergeAttribute("target", "_blank"); tagA.InnerHtml = img.ToString(); return MvcHtmlString.Create(tagA.ToString()); } public static MvcHtmlString ImageFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) { ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); // var propertyname = ExpressionHelper.GetExpressionText(expression); var htmlAttributes2 = AnonymousObjectToHtmlAttributes(htmlAttributes); return ImageHelper(html, modelMetadata , htmlAttributes2); } public static MvcHtmlString ImageFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) { return ImageFor(html, expression, null); } private static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) { RouteValueDictionary routeValueDictionary = new RouteValueDictionary(); if (htmlAttributes != null) { foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(htmlAttributes)) { routeValueDictionary.Add(propertyDescriptor.Name.Replace('_', '-'), propertyDescriptor.GetValue(htmlAttributes)); } } return routeValueDictionary; } } public class ImageModelMetadataProvider : DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { var meta= base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); if (meta.DataTypeName==DataType.ImageUrl.ToString() && string.IsNullOrEmpty(meta.TemplateHint)) { meta.TemplateHint = "ImageLink"; } return meta; } }
代碼已更新到:https://github.com/stoneniqiu/Portal.MVC
小結:回顧這四種方法,分部視圖最直接,但不夠靈活,ImageFor調用很簡單,也最靈活,實現復雜點但可用來去擴展更多方法。如果要實現一個功能,需要強制性改動幾個地方,依賴多個地方,自然就失去了靈活性,最后實現了EnumToDropDownList的方法還是很方便的,不需要依賴於什么模板,也不需要再自定義什么特性。 最后希望對你有幫助。tks!