Asp.Net MVC 擴展 Html.ImageFor 方法詳解


背景:

在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;
        }
    }
View Code

代碼已更新到:https://github.com/stoneniqiu/Portal.MVC

小結:回顧這四種方法,分部視圖最直接,但不夠靈活,ImageFor調用很簡單,也最靈活,實現復雜點但可用來去擴展更多方法。如果要實現一個功能,需要強制性改動幾個地方,依賴多個地方,自然就失去了靈活性,最后實現了EnumToDropDownList的方法還是很方便的,不需要依賴於什么模板,也不需要再自定義什么特性。 最后希望對你有幫助。tks!


免責聲明!

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



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