1. 程式人生 > >[ASP.NET MVC 小牛之路]09

[ASP.NET MVC 小牛之路]09

我們知道,在 MVC 中每個請求都會提交到 Controller 進行處理。Controller 是和請求密切相關的,它包含了對請求的邏輯處理,能對 Model 進行操作並選擇 View 呈現給使用者,對於業務和資料的邏輯程式碼以及介面和輔助類庫等一般都不放到 Controller 中。

Controller 和 Action 的內容較多,我把它分成了兩篇,也可能會分成三篇。本篇介紹 Controller 的實現、Controller 對狀態資料的獲取、ActionResult 和 Action 的資料傳遞,後續將介紹 Controller 工廠、Action Invoker 和暫時還沒想好或正在學習的一些較高階的特性。

文目錄

繼承 IController 介面

在本系列前面的文章中,我們新增的 Controller 都是一個繼承自抽象類 System.Web.Mvc.Controller 的普通類(請注意:controller(或Controller) 和 Controller 類在本文是兩個意思,請在閱讀本文時根據上下文理解)。Controller 抽象類封裝了很多很實用的功能,讓開發人員不用自己去寫那些重複煩瑣的處理程式碼。

如果不使用封裝的 Controller 抽象類,我們也可以通過實現 IController 介面來建立自己的 controller。IController 介面中只有一個 Exctute 方法:

public interface IController { 
    void Execute(RequestContext requestContext); 
}

IController 介面在 System.Web.Mvc 名稱空間下,一個結構非常簡單的介面。

當請求送到一個實現了 IController 介面的 controller 類時(路由系統通過請求的URL能夠找到controller),Execute 方法被呼叫。

下面我們建立一個空的 MVC 應用程式,在 Controllers 資料夾下新增一個實現了 IController 的類,並做一些簡單的操作,如下:

using System.Web.Mvc;
using System.Web.Routing;

namespace MvcApplication1.Controllers {
    public class BasicController : IController {
        
        public void Execute(RequestContext requestContext) {
            
            string controller = (string)requestContext.RouteData.Values["controller"];
            string action = (string)requestContext.RouteData.Values["action"];
            
            requestContext.HttpContext.Response.Write(
                string.Format("Controller: {0}, Action: {1}", controller, action));
        }
    }
}

執行應用程式,URL 定位到 /Basic/Index(你可以把 Index 改成其他任意片段名稱),結果如下:

實現了 IController 的類,MVC就會辨識它為一個 controller 類,根據 controller 名把對應的請求交給這個類處理。

但對於一個稍稍複雜的應用程式,自己實現 IController 介面是要做很多工作的,我們很少會這麼做。通過這我們更好地理解了 controller 的執行,controller 中一切對請求的處理都是從 Execute 方法開始的。

繼承 Controller 抽象類

MVC 允許我們自由地進行自定義和擴充套件,比如像上面講的你可以實現 IController 介面來建立對各類請求的各種處理並生成結果。不喜歡 Action 方法或不關心 View,那麼你可以自己動手寫一個更好更快更優雅的 controller 來處理請求。但像前面說的,自己實現 IController 介面要做很多工作,最重要的是沒有經過長期實踐測試,程式碼的健壯性得不到保證, 一般不建議你這麼做。MVC 框架的 System.Web.Mvc.Controller 類,提供了足夠實用的特性來方便我們對請求的處理和返回結果。

繼承 Controller 類的 controller 我們已經使用過很多次了,對它已經有一定的瞭解,它提供瞭如下幾個關鍵的特性:

  • Action方法:一個 Controller,它的行為被分為多個方法,通常一個方法對應著一個請求,並且可以通過方法引數來取得請求傳遞過來的資料。
  • ActionResult:可以返回一個描述了 Action 方法執行結果的物件,這樣的好處是想返回什麼結果就指定對應的返回物件就行,不用關心怎麼去執行並生成結果。
  • Filters:通過C#特性,對某一種行為的處理(比如授權和驗證)進行封裝,方便了在多個 Controller 和 Action 方法之間進行重用。

所以,如果你不是因為特殊的需求或閒得蛋疼,建立一個滿足要求的 Controller 最好的途徑是繼承 Controller 抽象類。由於之前我們已經使用過多次,這裡就不再進行具體的演示了,但我們還是來看一下它的程式碼結構。

在 Controllers 資料夾下新增一個 Controller,VS 已經把類的結構幫我們生成好了,如果你喜歡整潔,會習慣性地把不需要用到的引用刪掉,程式碼如下:

using System.Web.Mvc;

namespace MvcApplication1.Controllers {
    
    public class DerivedController : Controller {
        
        public ActionResult Index() {
            ViewBag.Message = "Hello from the DerivedController Index method";
            return View("MyView");
        }
    }
}

我們可以檢視 Controller 抽象類的定義,發現它是繼承 ControllerBase 類的,在 ControllerBase 類中實現了 IController 介面的 Execute 方法,這個方法是MVC對請求進行處理的各個元件的入口,其中包括通過路由系統找到 Action 方法並呼叫。

Controller 類內部使用 Razor 檢視系統來呈現 View,這裡通過 View 方法,指定 View 的名稱引數來告訴 MVC 選擇 MyView 檢視來返回給使用者結果。

在 Controller 中獲取狀態資料

我們經常需要訪問客戶端提交過來的資料,比如 QueryString 值、表單值和通過路由系統來自 URL 的引數值,這些值都可稱為狀態資料。下面是 Controller 中獲取狀態資料的三個主要來源:

  • 一系列的上下文物件。
  • 傳遞給 Action 方法的引數。
  • 顯式的呼叫框架的模型繫結(Model Binding)特性。

從上下文物件中獲取狀態資料

獲取狀態資料最直接的方法就是從上下文物件中提取。當你建立了一個繼承自 Controller 類的 Controller 時,可以通過一系列的屬性可以方便的訪問到和請求相關的資料,這些屬性包括 Request、Response、RouteData、HttpContext 和 Server,每一個都提供了請求相關的不同型別的資訊。下面列出了最常的上下文物件:

在 Action 方法中可以使用任意上下文物件來獲取請求相關的資訊,如下面在 Action 方法中所演示的:

...
public ActionResult RenameProduct() {
    //訪問不同的上下文物件
    string userName = User.Identity.Name;
    string serverName = Server.MachineName;
    string clientIP = Request.UserHostAddress;
    DateTime dateStamp = HttpContext.Timestamp;
    AuditRequest(userName, serverName, clientIP, dateStamp, "Renaming product");
            
    //從POST請求提交的表單中獲取資料
    string oldProductName = Request.Form["OldName"];
    string newProductName = Request.Form["NewName"];
    bool result = AttemptProductRename(oldProductName, newProductName);

    ViewData["RenameResult"] = result;
    return View("ProductRenamed");
}
...

這些上下物件不用特意去記,用的時候,你可以通過VS的智慧提示來了解這些上下文物件。 

使用 Action 方法引數獲取狀態資料

在本系列的前面的文章中,我們已經知識如何通過 Action 引數來接收資料,這種方法和上面的從上下文物件中獲取相比,它更為簡潔明瞭。比如,我們有下面這樣一個使用上下文物件的 Action 方法:

public ActionResult ShowWeatherForecast() {
    string city = (string)RouteData.Values["city"];
    DateTime forDate = DateTime.Parse(Request.Form["forDate"]);
    // do something ... 
    return View();
}

我們可以像下面這樣使用 Action 方法引數來重寫它:

public ActionResult ShowWeatherForecast(string city, DateTime forDate) { 
    // do something ... 
    return View(); 
}

它不僅易讀性強,也方便進行單元測試。

Action 方法的引數不允許使用 ref 和 out 引數,這是沒有意義的。

MVC 框架通過檢查上下文物件來為 Action 方法的引數提供值,它的名稱是不區分大小寫的,比如 Action 方法的 city 引數的值可以是通過 Request.Form["City"] 來獲取的。

理解 Action 方法的引數是如何被賦值的

Controller 類通過 MVC 框架的 value provider 和 model binder 元件來為 Action 方法獲取引數的值。

value provider 提供了一系列Controller中可以訪問到的值,在內部它通過從 Request.Form、Request.QueryString、Request.Files 和 RouteData.Values 等上下文物件中提取資料(鍵值集合),然後把資料傳遞給 model binder,model binder 試圖將這些資料與Action方法的引數進行匹配。預設的 model binder 可以建立和賦值給任何.NET型別物件引數(即 Action 方法的引數),包括集合和自定義的型別。

在這不對 model binder 進行介紹,我將在本系列的後續博文中對其進行專門的介紹。

理解 ActionResult

ActionResult 是描述 Action 方法執行結果的物件,它的好處是想返回什麼結果就指定對應的返回物件就行,不用關心如何使用Response物件來組織和生成結果。ActionResult 是一個命令模式的例子,這種模式通過儲存和傳遞物件來描述操作。

當 MVC 框架從 Action 方法中接收到一個 ActionResult 物件,它呼叫這個物件的 ExecuteResult 方法,其內部是通過 Response 物件來返回我們想要的輸出結果。

為了更好的理解,我們通過繼承 ActionResult 類來自定義一個 ActionResult。在MVC工程中新增一個Infrastructure資料夾,在裡面建立一個名為 CustomRedirectResult 的類檔案,程式碼如下:

using System.Web.Mvc;

namespace MvcApplication1.Infrastructure {
    
    public class CustomRedirectResult : ActionResult {
        
        public string Url { get; set; }

        public override void ExecuteResult(ControllerContext context) {
            string fullUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
            context.HttpContext.Response.Redirect(fullUrl);
        }
    }
}

當我們建立一個 CustomRedirectResult 類的例項時,我們可以傳遞想要跳轉的 URL。當 Action 方法執行結束時,MVC 框架呼叫 ExecuteResult 方法,ExecuteResult 方法通過 ControllerContext 物件獲得 Response 物件,然後呼叫 Redirect 方法。

下面我們在 Controller 中使用自定義的 CustomRedirectResult:

public class DerivedController : Controller {
    ...
    public ActionResult ProduceOutput() {
        if (Server.MachineName == "WL-PC") {
            return new CustomRedirectResult { Url = "/Basic/Index" };
        }
        else {
            Response.Write("Controller: Derived, Action: ProduceOutput");
            return null;
        }
    }
}

執行後我們看到如下結果:

當執行在本機(WL-PC)時直接重定向到了指定的/Basic/Index。

上面我們通過自定義 CustomRedirectResult 來實現重定向,我們可以用 MVC 框架提供的方法,如下:

... 
public ActionResult ProduceOutput() { 
    return new RedirectResult("/Basic/Index"); 
} 

為了使用方便,Controller 類中為大部分型別的 ActionResult 提供簡便的方法,如上面的可像下面這樣簡寫:

... 
public ActionResult ProduceOutput() { 
    return Redirect("/Basic/Index"); 
}

 MVC框架包含了許多 ActionResult 型別,這些型別都繼承自 ActionResult 類,大部分在 Controller 類中都有簡便的方法,下面列舉了一些:

除了該表列出來的,還有ContentResultFileResultJsonResult JavaScriptResult。具體每種ActionResult型別的用法這裡就不講了,大家可以看看蔣老師的瞭解ASP.NET MVC幾種ActionResult的本質系列的文章。

幾種從 Action 傳遞資料到 View 的方式

我們經常需要在 Action 方法中傳遞資料到一個 View 中,MVC 框架為此提供了一些很方便的操作。下面簡單簡介幾種常用的方式。

View Model 物件

通過 View Model 物件傳遞資料給View,這是最常用的一種,在 Acton 方法執行結束時通過 View 方法傳遞 View Model 物件給 View,如下程式碼所示:

... 
public ViewResult Index() { 
    DateTime date = DateTime.Now; 
    return View(date); 
} 

在 View 中我們通過 Model 屬性來使用傳遞過來的 View Model 物件,如下:

@model DateTime 

@{ 
    ViewBag.Title = "Index"; 
}

<h2>Index</h2> 
The day is: @Model.DayOfWeek

在 Razor 檢視引擎中,@model 的作用是宣告 odel 屬性的型別,省去了型別轉換的麻煩,而 @Model 是V iew Model 物件的引用。

ViewBag、ViewData 和 TempData 屬性

ViewBag、ViewData 和 TempData 都是 Controller 和 View 中能訪問到的屬性,都是用來儲存小量的資料,他們的區別如下:

  • ViewBag,是一個動態(dynamic)的弱型別,在程式執行的時候解析,是 MVC3 中新增的特性,只在當前View有效。
  • ViewData,是一個字典集合,也是隻在當前View有效,效能比 ViewBag 高,但是使用的時候需要型別轉換。
  • TempData,也是字典集合,一般用於兩個請求之間臨時快取內容或頁面間傳遞訊息,儲存在 Session 中,使用完以後則從 Session 中被清除。

下面是三者使用的例子,先在 Controller 中分別用三者儲存小資料:

public class DerivedController : Controller {

    public ActionResult Index() {
        ViewBag.DayOfWeek = DateTime.Now.DayOfWeek;
        ViewData["DayOfMonth"] = DateTime.Now.Day;
        return View();
    }

    public ActionResult ProduceOutput() {
        TempData["Message"] = "Warning message from Derived Controller.";
        return Redirect("/Home/Index");
    }
}

在 Views/Derived 目錄下的 Index.cshtml 中,取出 ViewBag 和 ViewData 中的儲存的資料:

...
Day of week from ViewBag: @ViewBag.DayOfWeek
<p /> 
Day of month from ViewData: @ViewData["DayOfMonth"] 

在 Views/Home 目錄下的 Index.cshtml 中,取 TempData 中的資料如下:

...
@TempData["Message"]

當請求 /Derived/ProduceOutput 時,ProduceOutput 方法將一條訊息存到 TempData 中,並跳轉到 /Home/Index。

下面是分別是將URL定位到 /Derived/Index 和 /Derived/ProduceOutput 時的結果:

  

一般在當前 View 中使用 ViewBag 或 ViewData,在兩個請求之間傳遞臨時資料用 TempData。由於 TempData 被使用後即被釋放,所以如果要二次使用 TempData 中的資料就需要將其存到其他變數中。

參考:Pro ASP.NET MVC 4 4th Edition