ASP.NET WebForm最重要的特性之一就是它的界面元素的组件化,简单的输入控件就不必多说,特别是那些类似于Repeater,GridView这样的模板控件,真的给开发人员带来了极大的方便。而在ASP.NET MVC的视图中,虽然技术上我们仍然可以使用这WebForm的Server Control,但是从理念上,我们是必须要完全避免这种情况的发生。很多习惯WebForm开发模式的开发人员,除了不习惯没有Postback外,可能最大的抱怨就是MVC的表单开发方式。在大部分情况,他们需要自己完全去控件HTML标签。在显示数据列表时,需要通过foreach控制数据的输出,当有一些特殊的输出控制时(比如奇偶行不同模板),还要做额外的工作,在界面上定义各种临时变量。这样重复的工作,除了会让开发人员烦躁不说,当一个表单开发下来,充斥着if..else这样的逻辑判断,不规则的“{”“}”,也给我们阅读和日后的修改带来相当大的麻烦。本文的目的就是为解决这些问题提供一些思路。
输入表单
对于输入表单的组件化,我们的解决思路来源于Mvccontrib,Mvccontrib是一个致力于改善和提高开发人员在使用ASP.NET MVC框架开发Web时的开发体验和开发效率的辅助框架。在里面有一个InputBuilder的功能,Mvccontrib首先根据不同的数据类型定义了一些常用的输入,输出模板。在开发人员在设计Model时,预先设置好一些必须的元数据供View使用,这样就可以提高HTML代码的复用性,更多细节请阅读:http://www.lostechies.com/blogs/hex/archive/2009/06/09/opinionated-input-builders-for-asp-net-mvc-using-partials-part-i.aspx。
这种通过在Model添加元数据,来支持View开发的模型在ASP.NET MVC2中得到了极大的应用。下面的代码就是MVC2项目模板的例子:
[PropertiesMustMatch("Password", "ConfirmPassword", ErrorMessage = "The password and confirmation password do not match.")] public class RegisterModel { [Required] [DisplayName("User name")] public string UserName { get; set; } [Required] [DataType(DataType.EmailAddress)] [DisplayName("Email address")] public string Email { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [DisplayName("Password")] public string Password { get; set; } [Required] [DataType(DataType.Password)] [DisplayName("Confirm password")] public string ConfirmPassword { get; set; } }
View是这样的:
<div class="editor-label"> <%: Html.LabelFor(m => m.UserName) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(m => m.UserName) %> <%: Html.ValidationMessageFor(m => m.UserName) %> </div>
以上的LabelFor,就会从UserName这个属性的元数据中去得到[DisplayName("User name")],显示作为label。TextBoxFor会自动生成input标签,并且把UserName的值也赋给标签值。添加<%: Html.ValidationMessageFor(m => m.UserName) %> ,则会把数据验证消息输出到这里。我们会发现这样,虽然已经可以帮我节省了大量了时间。但是你会也发现,每一个字段都复制和拷贝这两个DIV的内容,这部分也是一个相当重复和繁琐的工作。当我们把TextBoxFor替换成EditorFor,就会进一步发现原来每个字段都是这样的结构和内容,我们根本不需要任何修改,那为何还要去复制呢?如果我们能直接使用EditorFor来代替上面的两个Div,根据不同的输入类型,定义不同的输入控件模板。于是,我们的输入表单就变成这样:
<%Html.EnableClientValidation();%> <% using (this.Html.BeginForm()) { %> <%: Html.EditorFor(m=>m.UserName)%> <%: Html.EditorFor(m=>m.Password) %> <%: Html.EditorFor(m=>m.Password)%> <%: Html.EditorFor(m=>m.ConfirmPassword) %> <%: Html.EditorFor(m=>m.Email) %> <input type="submit" value="Submit" /> <%} %>
对于这样一个高度模式化的表单,一行一行去写代码也是相当的讨厌,特别是我可能必须要去检查一下有没有哪一个字段漏掉了。我们还可以进一步简化开发,写一个VS扩展,得到当前强类型模板所使用的Model类型,自动生成所有的字段模板,然后再根据需要手工去调整:
这篇博客就是为了写这个扩展时,得到当前上文Model类型实例而遇到的难题的记录。
列表表单
相对于输入表单,列表表单一般情况都是一行一行的输出数据。在WebForm中,我们可以使用Repeater,GridView这样的模板,给我们提供了非常大的便利。但是在MVC中,目前还没有一个非常好,非常方便的办法让我们方便快速去显示一个列表。在Mvccontrib中,给我们提供了一个强类型的Grid扩展,让我们可以以强类型的方式来输出table,但我并不喜欢那样的做法,要生成一个字段相对多点的列表,那个表达式写起来没有HTML标签来的轻松。
继承元数据和控件模板的做法,我把Model中,要显示在Grid的字段,都加上特定的元数据:
[GridAction(ActionName = "Delete", DisplayName = "Delete", RouteValueProperties = "UserName")] public class RegisterModel { [GridColumn] [Required] [DisplayName("User name")] public string UserName { get; set; } [GridColumn] [Required] [DataType(DataType.EmailAddress)] [DisplayName("Email address")] public string Email { get; set; } [Required] [DataType(DataType.Password)] [DisplayName("Password")] public string Password { get; set; } [Required] [DataType(DataType.Password)] [DisplayName("Confirm password")] public string ConfirmPassword { get; set; } }
以上加GridColumn的两个字段就是将来会被显示在grid的字段。同时Grid中的每一行都有一种操作,Delete。我们在列表中这样来写模板:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<System.Collections.Generic.IEnumerable<MvcFormSample.Models.RegisterModel>>" %> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> List </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2> List</h2> <%: Html.GridForModel() %> </asp:Content>
在Views\Share写一个默认的Grid模板:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Kooboo.Web.Mvc.Grid.GridModel>" %> <div class="table-container"> <table> <thead> <tr> <% foreach (var column in Model.GridColumns) { %> <th> <%:column.GetFormattedHeaderText(ViewContext)%> </th> <%} %> <%if (Model.GridActions.Count() > 0) { %> <th> <%:"Actions" %> </th> <%} %> </tr> </thead> <tbody> <% foreach (var item in Model.GridItems) { %> <tr <% if(item.IsAlternatingItem) {%>class="alternatingItem" <%} %>> <% foreach (var itemValue in item.GetItemValues(ViewContext)) {%> <td> <%: itemValue %> </td> <%} %> <td> <% foreach (var action in item.GetItemActions(ViewContext)) { if (action.Visible) {%> <%: Html.ActionLink(action.DisplayName, action.ActionName, action.RouteValues, new RouteValueDictionary(new { onclick = string.IsNullOrEmpty(action.ConfirmMessage) ? "" : "javascript:return confirm('" + action.ConfirmMessage + "')" }))%> <% } } %> </td> </tr> <% } %> </tbody> </table> </div>
通过以上的封装,我们就可以大大减少在写列表表格时的HTML复制。有时间,字段在列表中的显示并不是简单的把值显示出来,有可能还需要格式化等操作。这时,我们可以通过在GridColumnAttribute添加相应的设置来进行输出的控制。
总之,我们总是希望找到一种就为经济实惠并且可行的表单开发方式。以上的做法,View Model的元数据是基础。而很多时候这些与视图相关的元数据并不会在设计业务模型时被设计好,这篇博客就是针对这种情况扩展。
上文的例子请从这里下载。