1. 程式人生 > >從零開始學習 ASP.NET MVC 1.0 (四) View/Model 全解

從零開始學習 ASP.NET MVC 1.0 (四) View/Model 全解

《從零開始學習ASP.NET MVC 1.0》 文章導航

一.摘要

本文講解在Action中向View傳遞Model的幾種方式.以及View獲取Model以後如何編寫顯示邏輯.還詳細的介紹了ASP.NET MVC框架提供的Html Helper類的使用及如何為Html Helper類新增自定義擴充套件方法.

二.承上啟下

上一篇文章中我們學習了Controller處理一次請求的全過程.在Controller的Action中, 會傳遞資料給View,還會通知View物件開始顯示.所以Model是在Action中獲取的, 並由Action傳遞給View. View物件接到Action通知後會使用自己的顯示邏輯展示頁面.

image

下面首先讓我們學習如何將Model傳遞給View物件.

三.傳遞資料給View

在MVC中,Model物件是指包含了資料的模型. Controller將Model傳遞給View以後, View物件中不應該做任何的業務邏輯處理, 僅僅根據Model物件做一些顯示邏輯的處理.

傳遞Model物件時, 我們有兩種選擇:

1.傳遞一個弱型別的集合, 即成員為object型別的集合,  在View中需要將每個成員轉換成我們需要的型別,比如int, string,自定義型別等.

2.傳遞強型別物件, 這些型別是我們自定義的. 在View中直接使用我們傳遞的強型別物件, 不需要再轉換型別.

如果讓我們自己設計一個MVC框架, 我們也會想到上面兩種實現方式,接下來看看在ASP.NET MVC中的實現.

1.傳遞弱型別的集合

(1) 如何傳遞

ASP.NET MVC框架定義了ViewContext類, 直譯後是"View上下文", 其中儲存和View有關的所有資料, 其中Model物件也封裝在了此型別中.

ViewContext物件包含三個屬性:

  • IView View
  • ViewDataDictionary ViewData
  • TempDataDictionary TempData

其中ViewData集合和TempData集合都是用來儲存Model物件的.在一個Controller的Action中, 我們可以用如下方式為這兩個集合賦值:

        /// <summary>
        /// 傳遞弱型別Model的Action示例
        /// </summary>
        /// <returns>ViewResult</returns>
        public ActionResult WeakTypedDemo()
        {
            ViewData["model"] = "Weak Type Data in ViewData";
            TempData["model"] = "Weak Type Data in TempData";
            return View("WeakTypedDemo");
        }

在頁面中, 是用如下方式使用這兩個集合:

    <div>
        <% = ViewData["model"] %><br />
        <% = TempData["model"] %><br />
    </div>

(2) 傳遞過程

請注意Action中的賦值語句實際上操作的是Controller類的ViewData和TempData屬性, 此時並沒有任何的資料傳遞.上一篇文章中我們已經學到, return View語句會返回一個ViewResult物件, 並且接下來要執行ViewResult的Executeresult方法. Controller的View方法會將Controller類的ViewData和TempData屬性值傳遞給ViewResult,程式碼如下:

        protected internal virtual ViewResult View(IView view, object model) {
            if (model != null) {
                ViewData.Model = model;
            }

            return new ViewResult {
                View = view,
                ViewData = ViewData,
                TempData = TempData
            };
        }

然後在ViewResult中根據ViewData和TempData構建ViewContext物件:

        public override void ExecuteResult(ControllerContext context) {
          //...
            ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
            View.Render(viewContext, context.HttpContext.Response.Output);
          //...
        }

ViewContext物件最終會傳遞給ViewPage, 也就是說ViewData和TempData集合傳遞到了ViewPage. 我這裡簡化了最後的傳遞流程, 實際上ViewData物件並不是通過ViewContext傳遞到ViewPage中的, ViewPage上的ViewData是一個單獨的屬性, 並沒有像TempData一樣其實訪問的是ViewContext.TempData. 這麼做容易產生奇異, 本類ViewContext是一個很好理解職責十分清晰的類. 作為使用者的我們暫時可以忽略這點不足, 因為如此實現ViewData完全是為了下面使用強型別物件.

(3)ViewData和TempData的區別

雖然ViewData和TempData都可以傳遞弱型別資料,但是兩者的使用是有區別的:

  • ViewData的生命週期和View相同, 只對當前View有效.
  • TempData儲存在Session中, Controller每次執行請求的時候會從Session中獲取TempData並刪除Session, 獲取完TempData資料後雖然儲存在內部的字典物件中,但是TempData集合的每個條目訪問一次後就從字典表中刪除. 也就是說TempData的資料至多隻能經過一次Controller傳遞.

(4) TempData的實現

TempData的型別是TempDataDictionary, 和一般的字典表沒有明顯的不同, TempData的生命週期是由Controll決定的.

在所有Controll的基類ControllerBase中, 建立了型別為TempDataDictionary的TempData屬性.

在ControllerBase的派生類Controller中, 重寫了ExecuteCore()方法:

        protected override void ExecuteCore() {
            TempData.Load(ControllerContext, TempDataProvider);

            try {
                string actionName = RouteData.GetRequiredString("action");
                if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
                    HandleUnknownAction(actionName);
                }
            }
            finally {
                TempData.Save(ControllerContext, TempDataProvider);
            }
        }

注意其中的TempData.Load和TempData.Save語句.

TempDataDictionary.Load用來從ControllerContext總讀取TempData的資料.

TempDataDictionary.Save方法將發生了變化的TempData資料儲存到ControllerContext中.

這兩個方法都需要傳遞ITempDataProvider例項負責具體的讀取和儲存操作. 在Controll中預設的TempDataProvider是SessionStateTempDataProvider, 也就是說讀取和儲存都使用Session. 我們也可以擴充套件自己的TempDataProvider, 可以將臨時資料儲存在任何地方.比如製作AspNetCacheTempDataProvider使用機器本地快取來儲存TempData.

為何TempData只能夠在Controll中傳遞一次? 因為SessionStateTempDataProvider.LoadTempData方法(在TempDataDictionary.Load中呼叫)在從ControllerContext的Session中讀取了TempData資料後, 會清空Session:

        public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) {
            HttpContextBase httpContext = controllerContext.HttpContext;
            
            if (httpContext.Session == null) {
                throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled);
            }

            Dictionary<string, object> tempDataDictionary = httpContext.Session[TempDataSessionStateKey] as Dictionary<string, object>;

            if (tempDataDictionary != null) {
                // If we got it from Session, remove it so that no other request gets it
                httpContext.Session.Remove(TempDataSessionStateKey);
                return tempDataDictionary;
            }
            else {
                return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            }
        }

注意上面加粗的部分. 一旦讀取成功, 就會刪除TempData的Session.

再回憶一下Controll的ExecuteCore方法,  是在Controll的Execute方法中呼叫的, 是每一次Controll一定會執行的方法, 所以我們得出了"只能在Controll之間傳遞一次"的結論.

2.傳遞強型別物件

我在系統中建立了一個模型類:StrongTypedDemoDTO

從名字可以看出, 這個模型類是資料傳輸時使用的(Data Transfer Object).而且是我為一個View單獨建立的.

新增一個傳遞強型別Model的Action,使用如下程式碼:

        public ActionResult StrongTypedDemo()
        {
            StrongTypedDemoDTO model = new StrongTypedDemoDTO() { UserName="ziqiu.zhang", UserPassword="123456" };
            return View(model);
        }

使用了Controller.View()方法的一個過載, 將model物件傳遞給View物件.下面省略此物件的傳輸過程, 先讓我們來看看如何在View中使用此物件.

在建立一個View時, 會彈出下面的彈出框:

image

勾選"Create a strongly-typed view"即表示要建立一個強型別的View, 在"View data class"中選擇我們的資料模型類.

在"view content"中如下選項:

image

這裡是選擇我們的View的"模板", 不同的模板會生成不同的View頁面程式碼. 雖然這些程式碼不一定滿足我們需要, 但是有時候的確能節省一些時間,尤其是在寫文章做Demo的時候. 比如我們的View是新增資料使用的,那就選擇"Create".如果是顯示一條資料用的, 就選擇"Detail".

以選擇Detail為例, 自動生成了下列程式碼:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<DemoRC.Models.DTO.TransferModelController.StrongTypedDemoDTO>" %>
...
<body>
    <fieldset>
        <legend>Fields</legend>
        <p>
            UserName: 
            <%= Html.Encode(Model.UserName) %>
        </p>
        <p>
            UserPassword: 
            <%= Html.Encode(Model.UserPassword) %>
        </p>
    </fieldset>
    <p>
        <%=Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> |
        <%=Html.ActionLink("Back to List", "Index") %>
    </p>
</body>
...

頁面的Model屬性就是一個強型別物件, 在這裡就是StrongTypedDemoDTO類例項.page頁面指令可以看出, 這裡的頁面繼承自ViewPage<T>類, 而不是ViewPage, 用T已經確定為StrongTypedDemoDTO型別, 所以Model的型別就是StrongTypedDemoDTO.

3.傳遞資料的思考

使用強型別資料要優於弱型別資料, 老趙也曾提出過. 強型別有太多的好處, 智慧提示, 語意明確, 提高效能,編譯時發現錯誤等等.

所以在實際應用中, 我們應該傳遞強型別的資料.

目前ASP.NET MVC還存在一些不足. 將頁面型別化,  導致了只能傳遞一種資料型別例項到頁面上. 而且內部程式碼的實現上並不十分完美.尤其是雖然我們已經知道傳遞的是StrongTypedDemoDTO型別, 頁面的Model屬性也是StrongTypedDemoDTO型別, 但是仍然需要進行強制型別轉換, 原因是Controller的View(object model)方法過載接收的是一個object型別.

還有, 為每個View建立一個模型類, 也是一件繁瑣的工作. 也許我們的業務模型中的兩個類組合在一起就是View頁面需要的資料, 但是卻不得不建立一個類將其封裝起來.模型類的膨脹也是需要控制一個事情. 尤其是對於互諒網應用而非企業內部的系統, 頁面往往會有成百上千個.而且複用較少.

當然目前來說既然要使用強型別的Model, 我提出一些組織Model型別的實踐方法.下面是我專案中的Model型別組織結構:

image

這裡Model是一個資料夾, 稍大一些的系統可以建立一個Model專案. 另外我們需要建立一個DTO資料夾, 用來區分Model的型別. MVC中的Model應該對應DTO資料夾中的類.在DTO中按照Controller再建立資料夾, 因為Action和View大部分都是按照Controller組織的, 所以Model也應該按照Controller組織.

在Controller資料夾裡放置具體的Model類. 其實兩個Controller資料夾中可以同相同的類名稱, 我們通過名稱空間區分同名的Model類:

namespace DemoRC.Models.DTO.TransferModelController
{
    /// <summary>
    /// Action為StrongTypedDemo的資料傳輸模型
    /// </summary>
    public class StrongTypedDemoDTO
    {
        /// <summary>
        /// 使用者名稱
        /// </summary>
        public string UserName
        {
            get;
            set;
        }

        /// <summary>
        /// 使用者密碼
        /// </summary>
        public string UserPassword
        {
            get;
            set;
        }
    }
}

使用時也要通過帶有Controller的名稱空間使用比如DTO.TransferModelController.StrongTypedDemoDTO, 或者建立一些自己的約定.

四.使用Model輸出頁面

View物件獲取了Model以後, 我們可以通過兩種方式使用資料:

1.使用內嵌程式碼

熟悉ASP,PHP等頁面語言的人都會很熟悉這種直接在頁面上書寫程式碼的方式.但是在View中應該只書寫和顯示邏輯有關的程式碼,而不要增加任何的業務邏輯程式碼.

假設我們建立了下面這個Action,為ViewData添加了三條記錄:

        /// <summary>
        /// Action示例:使用內嵌程式碼輸出ViewData
        /// </summary>
        /// <returns></returns>
        public ActionResult ShowModelWithInsideCodeDemo()
        {
            ViewData["k1"] = @"<script type=""text/javascript"">";
            ViewData["k2"] = @"alert(""Hello ASP.NET MVC !"");";
            ViewData["k3"] = @"</script>";
            return View("ShowModelWithInsideCode");
        }

在ShowModelWithInsideCode中, 我們可以通過內嵌程式碼的方式, 遍歷ViewData集合:

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>使用內嵌程式碼輸出ViewData</title>
    <% foreach(KeyValuePair<string, object> item in ViewData )
       {%>
            <% = item.Value %>
    <% } %>    
</head>
<body>
    <div>
    
        <div>此頁面執行的指令碼程式碼為:</div>
        <fieldset>       
        <% foreach(KeyValuePair<string, object> item in ViewData )
           {%>
               <% = Html.Encode(item.Value) %> <br />
        <%  } %>
        </fieldset> 
    </div>
</body>
</html>

頁面上遍歷了兩遍ViewData,第一次是作為指令碼輸出的, 第二次由於進行了HTML編碼,所以將作為文字顯示在頁面上.

使用這種方式, 我們可以美工做好的HTML頁面的動態部分, 使用<% %>的形式轉化為編碼區域, 通過程式控制輸出.由於只剩下顯示邏輯要處理,所以這種開發方式往往要比CodeBehind的編碼方式更有效率, 維護起來一目瞭然.

最讓我高興是使用這種方式後,我們終於可以只使用HTML控制元件了.雖然ASP.NET WebFrom程式設計模型中自帶了很多伺服器控制元件, 功能很好很強大, 但是那終究是別人開發的控制元件, 這些控制元件是可以隨意變化的, 而且實現原理也對使用者封閉. 使用原始的頁面模型和HTML控制元件將使我們真正的做程式的主人.而且不會因為明天伺服器控制元件換了個用法就要更新知識, 要知道幾乎所有的HTML控制元件幾乎是被所有瀏覽器支援且不會輕易改變的.

2.使用伺服器控制元件

注意雖然我們同樣可以在ASP.NET MVC中使用伺服器端控制元件, 但是在MVC中這並不是一個好的使用方式.建議不要使用.

要使用伺服器端控制元件, 我們就需要在後臺程式碼中為控制元件繫結資料. ASP.NET MVC框架提供的新增一個View物件的方法已經不能建立後臺程式碼, 也就是說已經摒棄了這種方式.但是我們仍然可以自己新增.

首先建立一個帶有後臺程式碼的(.cs檔案),一般的Web Form頁面(aspx頁面),然後修改頁面的繼承關係, 改為繼承自ViewPage:

public partial class ShowModelWithControl : System.Web.Mvc.ViewPage

在頁面上放置一個Repeater控制元件用來顯示資料:

<body>
    <form id="form1" runat="server">
    <div>
        <asp:Repeater ID="rptView" runat="server">
            <ItemTemplate>
                <%# ((KeyValuePair<string, object>)Container.DataItem).Value  %><br />
            </ItemTemplate>
        </asp:Repeater>
    </div>
    </form>
</body>

在Page_Load方法中, 為Repeater繫結資料:

    public partial class ShowModelWithControl : System.Web.Mvc.ViewPage
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            rptView.DataSource = ViewData;
            rptView.DataBind();
        }
    }

在Controller中建立Action, 為ViewData賦值:

        /// <summary>
        /// Action示例:使用伺服器控制元件輸出ViewData
        /// </summary>
        /// <returns></returns>
        public ActionResult ShowModelWithControlDemo()
        {
            ViewData["k1"] = @"This";
            ViewData["k2"] = @"is";
            ViewData["k3"] = @"a";
            ViewData["k4"] = @"page";
            return View("ShowModelWithControl");
        }

執行結果:

image

再次強調,  在ASP.NET MVC中我們應該儘量避免使用這種方式.

3.使用 HTML Helper 類生成HTML控制元件

HTML Helper類是ASP.NET MVC框架中提供的生成HTML控制元件程式碼的類. 本質上與第一種方式一樣, 只是我們可以不必手工書寫HTML程式碼,而是藉助HTML Helper類中的方法幫助我們生成HTML控制元件程式碼.

同時,我們也可以使用擴充套件方法為HTML Helper類新增自定義的生成控制元件的方法.

HTML Helper類的大部分方法都是返回一個HTML控制元件的完整字串, 所以可以直接在需要呼叫的地方使用<% =Html.ActionLink() %>的形式輸出字串.

(1)ASP.NET MVC中的HtmlHelper類

在ViewPage中提供了Html屬性, 這就是一個HtmlHelper類的例項. ASP.NET MVC框架自帶了下面這些方法:

  • Html.ActionLink()
  • Html.BeginForm()
  • Html.CheckBox()
  • Html.DropDownList()
  • Html.EndForm()
  • Html.Hidden()
  • Html.ListBox()
  • Html.Password()
  • Html.RadioButton()
  • Html.TextArea()
  • Html.TextBox()

上面列舉的常用的HtmlHelper類的方法,並不是完整列表.

下面的例子演示如何使用HtmlHelper類:

    <div>
        <% using (Html.BeginForm())
           { %>
           <label style="width:60px;display:inline-block;">使用者名稱:</label>
           <% =Html.TextBox("UserName", "ziqiu.zhang", new { style="width:200px;" })%>
           <br /><br /> 
           <label style="width:60px;display:inline-block;">密碼:</label>
           <% =Html.Password("Psssword", "123456", new { style = "width:200px;" })%>                     
        <% }%>
    </div>

上面的程式碼使用Html.BeginForm輸出一個表單物件, 並在表單物件中添加了兩個Input, 一個使用Html.TextBox輸出, 另一個使用Html.Password輸出,區別是Html.Password輸出的input控制元件的type型別為password.效果如圖:

image

(2)擴充套件Html Helper類

我們可以自己擴充套件HtmlHelper類, 為HtmlHelper類新增新的擴充套件方法, 從而實現更多的功能.

在專案中建立Extensions資料夾, 在其中建立SpanExtensions.cs檔案.原始碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;


namespace System.Web.Mvc
{
    public static class SpanExtensions
    {
        public static string Span(this HtmlHelper helper,string id, string text)
        {
            return String.Format(@"<span id=""{0}"">{1}</span>", id, text);
        }
    }
}

上面的程式碼我們為HtmlHelper類添加了一個Span()方法, 能夠返回一個Span的完整HTML字串.

因為名稱空間是System.Web.Mvc,所以頁面使用的時候不需要再做修改,Visual Studio會自動識別出來:

image

請大家一定要注意名稱空間, 如果不使用System.Web.Mvc名稱空間, 那麼一定要在頁面上引用你的擴充套件方法所在的名稱空間, 否則我們的擴充套件方法將不會被識別.

接下來在頁面上可以使用我們的擴充套件方法:

    <div>
        <!-- 使用自定義的Span方法擴充套件HTML Helper -->
        <% =Html.Span("textSpan", "使用自定義的Span方法擴充套件HtmlHelper類生成的Span") %>
    </div>

(3) 使用TagBuilder類建立擴充套件方法

上面自定義的Span()方法十分簡單, 但是有時候我們要構造具有複雜結構的Html元素, 如果用字串拼接的方法就有些笨拙.

ASP.NET MVC框架提供了一個幫助我們構造Html元素的類:TagBuilder

TagBuilder類有如下方法幫助我們構建Html控制元件字串:

方法名稱 用途
AddCssClass() 新增class=””屬性
GenerateId() 新增Id,  會將Id名稱中的"."替換為IdAttributeDotReplacement 屬性值的字元.預設替換成"_"
MergeAttribute() 新增一個屬性,有多種過載方法.
SetInnerText() 設定標籤內容, 如果標籤中沒有再巢狀標籤,則與設定InnerHTML 屬性獲得的效果相同.
ToString() 輸出Html標籤的字串, 帶有一個引數的過載可以設定標籤的輸出形式為以下列舉值:
  • TagRenderMode.Normal -- 有開始和結束標籤
  • TagRenderMode.StartTag -- 只有開始標籤
  • TagRenderMode.EndTag -- 只有結尾標籤
  • TagRenderMode.SelfClosing -- 單標籤形式,如<br/>

同時一個TagBuilder還有下列關鍵屬性:

屬性名稱 用途
Attributes Tag的所有屬性
IdAttributeDotReplacement 新增Id時替換"."的目標字元
InnerHTML Tag的內部HTML內容
TagName Html標籤名, TagBuilder只有帶一個引數-TagName的建構函式.所以TagName是必填屬性


下面在新增一個自定義的HtmlHelper類擴充套件方法,同樣是輸出一個<Span>標籤:

        public static string Span(this HtmlHelper helper, string id, string text, string css, object htmlAttributes)
        {
            //創意某一個Tag的TagBuilder
            var builder = new TagBuilder("span");

            //建立Id,注意要先設定IdAttributeDotReplacement屬性後再執行GenerateId方法.
            builder.IdAttributeDotReplacement = "-";
            builder.GenerateId(id);
            

            //新增屬性            
            builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));

            //新增樣式
            builder.AddCssClass(css);
            //或者用下面這句的形式也可以: builder.MergeAttribute("class", css);

            //新增內容,以下兩種方式均可
            //builder.InnerHtml = text;
            builder.SetInnerText(text);

            //輸出控制元件
            return builder.ToString(TagRenderMode.Normal);

        }

在頁面上,呼叫這個方法:

<% =Html.Span("span.test", "使用TagBuilder幫助構建擴充套件方法", "ColorRed", new { style="font-size:15px;" })%>

生成的Html程式碼為:

<span id="span-test" class="ColorRed" style="font-size: 15px;">使用TagBuilder幫助構建擴充套件方法</span>


注意已經將id中的"."替換為了"-"字元.

五.總結

本來打算在本文中直接將ViewEngine的使用也加進來, 但是感覺本文已經寫了很長的內容, (雖然不多,但是很長......)所以將ViewEngine作為下一章單獨介紹.

前些天 Scott Guthrie's的部落格上放出了"ASP.NET MVC免費教程", 裡面介紹了建立一名為"NerdDinner"專案的全過程, 使用LINQ+ASP.NET MVC, 但是其中對於技術細節沒有詳細介紹(和本系列文章比較一下就能明顯感覺出來), 下面提供本書的pdf檔案下載地址以及原始碼下載地址:

image

  • 免費下載PDF版本
  • 下載應用原始碼 + 單元測試

    原始碼是英文版本,  其實我最近有做一箇中文的"Nerd Dinner"的想法, 但是因為要寫文章而且還要工作已經讓我焦頭爛額, 寫文章真的是一件體力活.如果有人同樣有興趣翻譯程式碼, 我可以提供域名和伺服器空間.

    差點提供忘記本篇文章的示例原始碼下載: