文章目錄
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的中繼資料是基礎。而很多時候這些與視圖相關的中繼資料並不會在設計業務模型時被設計好,這篇部落格就是針對這種情況擴充。
上文的例子請從這裡下載。