9.2.1 .net framework下的MVC 控件的封装(上)
在写.net core下mvc控件的编写之前,我先说一下.net framework下我们MVC控件的做法。
MVC下控件的写法,主要有如下三种,最后一种是泛型的写法,mvc提供的控件都是基本控件。
1 @model UserInfo 2 3 <input type="text" id="t2" value="t2Value" /> <!—第一种写法 --> 4 @Html.TextBox("t1", "t1value"); <!—第二种写法 --> 5 @Html.TextBoxFor(user => user.EMail) <!—第三种写法 -->
但是我们在写大型系统的时候,像自动完成autocomplete、下拉多选multiselect、附件accessory、富文本编辑htmleditor、人员机构选择oguinput等控件会在许多地方都会用到,这些控件也都包含一部分html标签和script脚本。
就以下拉多选列表为例,其实这个控件包含了一个标签(例如"请选择人员:",不过我后来发现截图没截上),一个用于展示的select控件,一个用于存放实际选择值的隐藏控件,以及一个JQuery的MultiSelect.js脚本,和调用MultiSelect.js的脚本。
如果在各个使用到MultiSelect的cshtml页面中写上这么一堆重复性的html标签和对应的脚本总不是一个好的技术人员该做的事情,封装成一个MultiSelect控件是一个比较好的选择。
既然做控件的封装,首先就是抽象,将一个个的控件抽象、提取形成控件基类。这个基类,我们叫MvcControlBase。
但是,只是这样的话,如果在页面中使用,例如MultiSelect就得写成这样:
1 @{ 2 MultiSelect control = new MultiSelect(…..); //创建实例 3 control.DataSource = …; //设置各种属性,这里需要设置数据源等属性 4 Html.Raw(control.Render().ToString(); //呈现 5 }
这种写法在cs程序中是没有问题的,但是在cshtml中显得有些土的掉渣了。html内的写法尽量简洁,像如下这样连缀的写法:
@Html.MultiSelect(…).SetDataSource(…).SetShowItemsCount(5).Render()
这就涉及到另外一个封装基类,我们叫控件构建器,MvcControlBuilderBase。
下面就具体介绍下这两个基类。
基控件类MvcControlBase是所有MVC控件的抽象,那么应该包含名称Name,Id(通过Name自动生成的),控件的标签名称,值,属性集合,标签单元格数,控件单元格数,显示状态等,这些属性应该是所有控件都会用到的,以及一个前台呈现(Render)方法。
对于选择日期控件 ,Name和Id都是theDate,标签名称是"日期:",值是"2016/10/17",属性集合包含了cssclass、Style等,显示状态有普通、只读、隐藏三个状态。对于单元格数,我们使用了BootStrap作为css的框架集,如果设置了单元格数是3,那么会自动生成col-md-3\col-xs-3\col-sm-3等cssclass。
由上引申出基控件的构造函数定义如下:
protected MvcControlBase(HtmlHelper helper, string name, object value, string label, object attributes)
因为显示状态、单元格数等都有缺省值,我们在构造函数中没有对应的参数。
基控件类MvcControlBase最重要的方法是Render方法,就是在页面中呈现控件。该方法,首先调用WriteHtml方法,生成Html标签,然后再调用WriteScript方法,生成Script脚本,最终将标签和脚本的内容生成MvcHtmlString返回。
1 public IHtmlString Render() 2 { 3 MvcHtmlString result = null; 4 5 using (HtmlTextWriter htmlWriter = new HtmlTextWriter(new StringWriter())) 6 { 7 WriteHtml(htmlWriter); 8 9 htmlWriter.WriteLine(); 10 11 using (HtmlTextWriter scriptWriter = new HtmlTextWriter(new StringWriter())) 12 { 13 WriteScript(scriptWriter); 14 15 string scriptString = scriptWriter.InnerWriter.ToString(); 16 17 if (!string.IsNullOrWhiteSpace(scriptString)) 18 { 19 htmlWriter.RenderBeginTag(HtmlTextWriterTag.Script); 20 htmlWriter.Write(scriptString); 21 htmlWriter.RenderEndTag(); 22 } 23 } 24 25 result = new MvcHtmlString(htmlWriter.InnerWriter.ToString()); 26 } 27 28 return result; 29 }
WriteHtml方法是个抽象方法,各个子控件必须予以重写,生成控件的html元素; WriteScript方法,是个虚方法,如果子控件不需要脚本可以不重写。
protected virtual void WriteScript(HtmlTextWriter writer) { }
protected abstract void WriteHtml(HtmlTextWriter writer);
我们继续以多选下拉框控件MultiSelect为例说明如何继承基控件。MultiSelect控件比缺省控件会多两个属性,一个是数据源DataSource,就是下拉列表的所有数据,还有一个是ShowItemsCount,就是下拉列表显示的行数。此外,Value的类型也应该是List<string>,代表哪个数据被选择,就是下拉列表的选中内容。
上图的例子中,数据源是所有人List<UserInfo>,Value是Aaron和Adair,ShowItemsCount是9。
因为要控制select元素的展示内容和方式,因此MultiSelect前台需要部分脚本,我们选择一个JQuery的开源的multiselect脚本。像图中显示的全选、查找等,是在multiselect.js脚本中实现的,我们初始化时,将该脚本的是否显示全选、查找的选项设置为true,没有通过我们封装的控件暴露出来。
具体做法是,重写WriteScript方法,编写调用脚本的$(function() { … })的脚本,调用的脚本比较简单,就不再详细介绍了。然后重写WriteHtml脚本,生成标签和一个div。div内包含两个控件,一个是显示在页面的select,一个是隐藏的hidden,用于存放选中值的。select控件的options就是从DataSource中获取所有数据生成的,属于Value的数据则设置选中状态。
1 public class MultiSelect : MvcControlBase 2 { 3 /// <summary> 4 /// 初始化 5 /// </summary> 6 /// <param name="htmlHelper"></param> 7 /// <param name="expression"></param> 8 /// <param name="htmlAttributes"></param> 9 public MultiSelect(HtmlHelper htmlHelper, string name, List<string> value, string labelName, object htmlAttributes) 10 : base(htmlHelper, name, value, labelName, htmlAttributes) 11 { 12 this.DataSource = new Dictionary<string, string>(); 13 this.ShowItemsCount = 5; 14 } 15 16 #region 属性 17 /// <summary> 18 /// 列表值 19 /// </summary> 20 internal Dictionary<string, string> DataSource { get; set; } 21 /// <summary> 22 /// 显示项目的数量 23 /// </summary> 24 internal int ShowItemsCount { get; set; } 25 #endregion 26 27 /// <summary> 28 /// 输出控件JS代码 29 /// </summary> 30 /// <param name="writer"></param> 31 protected override void WriteScript(HtmlTextWriter writer) 32 { 33 string classes = string.Empty; 34 if (this.DisplayStatus == FieldDisplayStatus.ReadOnly) 35 { 36 classes = "ui-state-disabled"; 37 } 38 else if (this.DisplayStatus == FieldDisplayStatus.Hidden) 39 { 40 classes = " hidden"; 41 } 42 43 44 string script = string.Format(@" 45 $(function(){{ 46 $(""#{0}"").multiselect({{ 47 noneSelectedText: ""请选择"", 48 checkAllText: ""全选"", 49 uncheckAllText: ""全不选"", 50 selectedList: {1}, 51 classes: ""{2}"" 52 }}); 53 }});", "select-" + this.Id, this.ShowItemsCount.ToString(), classes); 54 55 writer.Write(script); 56 57 } 58 59 /// <summary> 60 /// 输出控件Html代码 61 /// </summary> 62 /// <param name="writer">HtmlTextWriter</param> 63 protected override void WriteHtml(HtmlTextWriter writer) 64 { 65 List<string> value = this.Value as List<string>; 66 67 if (this.DisplayStatus == FieldDisplayStatus.Hidden || this.LabelCellNumber <= 0) 68 { 69 writer.Write(Helper.Label(LabelName, new RouteValueDictionary { { "class", string.Format("col-sm-{0} control-label hidden", LabelCellNumber) } }).ToHtmlString()); 70 } 71 else 72 { 73 writer.Write(Helper.Label(LabelName, new RouteValueDictionary { { "class", string.Format("col-sm-{0} control-label", LabelCellNumber) } }).ToHtmlString()); 74 } 75 76 TagBuilder divTag = new TagBuilder("div"); 77 divTag.AddCssClass(string.Format("col-sm-{0}", ControlCellNumber)); 78 79 List<SelectListItem> selectList = new List<SelectListItem>(); 80 foreach (KeyValuePair<string, string> kvp in this.DataSource) 81 { 82 SelectListItem item = new SelectListItem(); 83 item.Text = kvp.Value; 84 item.Value = kvp.Key; 85 if (value != null && value.Contains(kvp.Key)) 86 { 87 item.Selected = true; 88 } 89 selectList.Add(item); 90 } 91 92 //select 标签的名字 93 IDictionary<string, object> HtmlAttributesForSelect = new RouteValueDictionary(); 94 HtmlAttributesForSelect.Add("id", "select-" + this.Id); 95 HtmlAttributesForSelect.Add("multiple", "multiple"); 96 97 divTag.InnerHtml = Helper.DropDownList(Name, selectList, HtmlAttributesForSelect).ToHtmlString(); 98 writer.Write(divTag.ToString()); 99 writer.Write("<input type=\"hidden\" id=\"" + this.Id + "\" />"); 100 } 101 }
按照上面的方法,MultiSelect控件基本就完成了。为了实现@Html.MultiSelect(…).SetDataSource(…).SetShowItemsCount(5).Render() 这样的写法,我们还得写控件构建器。一样的,有控件基类MvcControlBase,也得有控件基构建器类MvcControlBuilderBase。控件构造器主要作用是,传入控件,根据控件的属性和方法,生成一个个的连缀方法。
这个构建器类MvcControlBuilderBase我只写了一部分内容,类定义的写法比较绕一些,也算是一种泛型的设计模式吧,也确实是解决问题的一个程序写法,方法呢也要返回构建器自身。
1 public abstract class MvcControlBuilderBase<TMvcControl, TBuilder> 2 where TMvcControl : MvcControlBase 3 where TBuilder : MvcControlBuilderBase<TMvcControl, TBuilder> 4 { 5 /// <summary> 6 /// 构造函数 7 /// </summary> 8 /// <param name="control">当前Control的实例</param> 9 protected MvcControlBuilderBase(TMvcControl control) 10 { 11 this.Control = control; 12 } 13 14 /// <summary> 15 /// 要生成的控件 16 /// </summary> 17 public TMvcControl Control { get; private set; } 18 19 /// <summary> 20 /// 设置Class名称 21 /// </summary> 22 /// <param name="className"></param> 23 /// <returns></returns> 24 public virtual TBuilder CssClass(string className) 25 { 26 this.Control.Attributes.Merge(new { @class = className }); 27 return this as TBuilder; 28 } 29 30 /// <summary> 31 /// 设置style内容 32 /// </summary> 33 /// <param name="styleValue"></param> 34 /// <returns></returns> 35 public virtual TBuilder CssStyle(string styleValue) 36 { 37 this.Control.Attributes.Merge(new { style = styleValue }); 38 return this as TBuilder; 39 } 40 41 /// <summary> 42 /// 以匿名方式设置控件html属性 43 /// </summary> 44 /// <param name="attributes">html属性集合</param> 45 public virtual TBuilder HtmlAttributes(IDictionary<string, object> attributes) 46 { 47 Control.Attributes.Clear(); 48 Control.Attributes.Merge(attributes); 49 return this as TBuilder; 50 } 51 52 /// <summary> 53 /// 以Html方式输出控件代码 54 /// </summary> 55 public virtual IHtmlString Render() 56 { 57 return Control.Render(); 58 } 59 60 /// <summary> 61 /// 重写tostring方法,返回html代码 62 /// </summary> 63 /// <returns></returns> 64 public override string ToString() 65 { 66 return Render().ToString(); 67 } 68 }
我们还是以MultiSelect控件的构建器MultiSelectBuilder为例子,写法就比较简单,主要实现SetDataSource和SetShowItemsCount就行了。
1 public class MultiSelectBuilder : MvcControlBuilderBase<MultiSelect, MultiSelectBuilder> 2 { 3 /// <summary> 4 /// 构造函数 5 /// </summary> 6 /// <param name="control">控件</param> 7 public MultiSelectBuilder(MultiSelect control) 8 : base(control) 9 { 10 } 11 12 /// <summary> 13 /// 设置List,根据传入的Dictionary 14 /// </summary> 15 /// <param name="func"></param> 16 /// <returns></returns> 17 public MultiSelectBuilder SetDataSource(Dictionary<string, string> list) 18 { 19 Control.DataSource.Merge(list); 20 return this; 21 } 22 /// <summary> 23 /// 设置显示项目的数量 24 /// </summary> 25 /// <param name="func"></param> 26 /// <returns></returns> 27 public MultiSelectBuilder SetShowItemsCount(int iCount) 28 { 29 Control.ShowItemsCount = iCount; 30 return this; 31 } 32 }
控件和控件的构建器完成后,如何在cshtml中像@Html.TextBox()一样,只要写成@Html.MultiSelect(…).SetDataSource(…).SetShowItemsCount(5).Render()就实现在cshtml页面中呈现多选下拉控件呢?
这里的Html是HtmlHelper,顾名思义HtmlHelper是html的帮助类,在这里的主要功能是协助在cshtml页面中呈现Html控件。我们再来看一下cshtml页面,每个cshtml最终在运行时是一个类,这个类继承了WebViewPage类,这个类有一个属性就是public HtmlHelper Html { get; set; }。这里的Html就是这个属性。
我们应该写一个HtmlHelper的扩展方法,来实现Html.MultiSelect()的写法。这个方法,返回值不是控件,而是控件的构建器,也就是MultiSelectBuilder。
1 public static MultiSelectBuilder MultiSelect(this HtmlHelper helper, string name, List<string> value, string labelName, object htmlAttributes = null) 2 { 3 return new MultiSelectBuilder(new MultiSelect(helper, name, value, labelName, htmlAttributes)); 4 }
到此为止,一个基本的控件就完成,使用控件的cshtml页面中也只需要写上类似@Html.MultiSelect(…).SetDataSource(…).SetShowItemsCount(5).Render()的语句就搞定了。