前一章介紹了 Routing,它最重要的工作有兩個:

  • 找到要執行的 Controller
  • 找到那個 Controller 裡的要執行的 Action

一般我們會說是 Controller 與 Model 互動,但更精確來說是 Controller 裡的 Action 與 Model 互動

但有些情況下,Action 又不一定會直接跟 Model 互動

因此,無論有與 Model 互動與否,最終 Controller 都會將結果回傳給 View

Controller 的角色

  • 檔案放置與 Controllers 資料匣底下
  • 習慣上,檔案名稱結尾都會加上 Controller 字樣
  • 以關注點分離的概念,將職責分類並交由各個 Controller 去執行
  • Controller 底下的各個 Action,理想狀況下,盡量都只專注做一件事,向單一指責原則的理想趨近

運作流程

基本架構:

public class EmptyController: Controller
{
  // action
  public ActionResult Index()
  {
    return View();
  }
}

EmptyController 繼承自 System.Web.Mvc.Controller 抽象類別

這個 Index action 要回傳的 View,會對應到 View 資料匣的檔案,檔名會是 Index.cshtml

EmptyController 位於 Controllers/EmptyController.cs

因此,這個 Index.cshtml 就會放在 Views/Empty/Index.cshtml

預設的 View 會長這樣:

@{ ViewBag.Title = "Index"; }

<h2>Index</h2>

View 繼承 System.Web.Mvc.ViewPage 類別

ViewPage 類別提供一些屬性及方法,在 View 裡面使用,例如:ViewDataViewBagTempData⋯⋯ 等

Controller 與 View 的資料傳遞

View 傳遞資料至 Controller 的主要媒介是 <form> 表單,在 ASP.NET MVC 中的技術是 Model Binding

Controller 傳資料給 View 的方式,像是透過 model 傳給 RazorViewEngine 並交給 View Engine 去渲染成 HTML

另外,View 也可以透過 ViewDataViewBagTempData 這幾種屬性取得渲染時需要的資料

ViewData 屬性

ViewData 屬性屬於 ViewDataDictionary 型別,而 ViewDataDictionary 型別的定義如下:

public class ViewDataDictionary: IDictionary<string, object>{}

從定義可以看出,它就是一個 dictionary 的 key-value 結構:key 的型別為 string,且 value 可以儲存任何型別的資料(定義為很寬鬆的 object

另外,ViewData 與 ViewBag 都有一個特性,就是無法跨 Action 方法存取,只限於此次的 HTTP request

在 Action 裡面儲存一個 Name 欄位的資料:

// in Action
public ActionResult DemoViewData()
{
  ViewData["Name"] = "Bruce";

  return View();
}

然後在 View 這裡接收並顯示:

// in View @{ ViewBag.Title = "DemoViewData"; }
<h2>DemoViewData</h2>
@ViewData["Name"]

ViewBag 屬性

ViewBag 屬於 dynamic 型別

一般的型別是靜態型別,在編譯期就已經確定,執行期不能再做修改

dynamic 型別也是靜態型別(不要因為叫做 dynamic 而混淆),但不同的是,它是動態化的靜態型別

意思就是,dynamic 型別的物件會略過編譯期的型別檢查,在執行期進行型別檢查

雖然型別不同,但 ViewBag 的特性跟 ViewData 一模一樣,而寫法上還是有一點差異

來看一下書中的範例:

// in Action
public ActionResult DemoViewBag()
{
  ViewData["Name"] = "Bruce"; // ViewData 的寫法
  ViewBag.Name = "Bruce";

  return View();
}
// in View @{ ViewBag.Title = "DemoViewBag"; }
<h2>DemoViewBag</h2>
@ViewBag.Name

由上述範例可以看出,比起 ViewData,ViewBag 的寫法又更簡潔了一點

ViewData.Model 屬性

利用 Model 這個屬性,在 View 的寫法會更為簡單

// in Action
public ActionResult DemoViewDataModel()
{
  var product = db.Products.ToList();
  ViewData.Model = product;
  return View();
}

此時,我們可以直接在 View 中,用 Model 這個字取得資料:

// in View
<ul>
  @foreach(var item in Model) {
  <li>@item.ProductName</li>
  }
</ul>

此時的 Model 雖然可以拿到 Action 那裡 product 的資料,但它還是弱型別的

要使 Model 具有型別檢查的能力,就要使用 @model 這個關鍵字

@model 可以定義這個 model 的型別,通常寫在 View 的最上面:

// in View @model INumerable<Product>
  // <-- 定義 model 的型別
  <ul>
    @foreach(var item in Model) {
    <li>@item.ProductName</li>
    }
  </ul></Product
>

PS. 這裡 Product 型別不用寫完整的 namespace,是因為已經在 /View/Web.Config 檔案裡加入其 namespace

多 Model 與多物件傳遞

有時候,View 所要呈現的資料不只限於單一 Model 的資料,然而在 View 裡面,@model 只允許定義一個型別,無法同時定義兩個 model

此時,我們熟悉的 ViewModel 就派上用場了

書中關於 ViewModel 的一段描述:

ViewModel 是一個專門給 View 使用的 Model 物件資料,透過 ViewModel 將所需的多個物件透過一層 Class 進行屬性封裝,然後就可以透過傳遞 ViewModel 物件至 View 來達到強型別開發的目的

ViewModel 類別

  • 習慣放置在 Models/ViewModels 目錄下(但公司專案是跟 Models 同在根目錄下)
  • 命名習慣上,會以 ViewModel 結尾
  • 依照 View 畫面所需定義欄位

有了畫面專屬的 ViewModel 之後,我們就可以將 model 定義成 @model CustomViewModel 了!

Model Binding

Client 端與 Server 端如何溝通,這時就要透過 Model Binding

Request 物件

常用的 QueryString 就是來自於 Request 物件中,可以從網址上面 ?name1=value1&name2=value2&... 中取得對應的值

我們來看一下書中的範例:

// in Action
public ActionResult DemoQueryString()
{
  ViewBag.id = int.Parse(Request.QueryString["id"]);
  return View();
}

若使用表單 <form> 的話,就可以用 Request.Form["name"] 取得資料

然而,若 QueryString 太過冗長,則會造成 URL 的難以閱讀

簡單 Model Binding

// in Action
public ActionResult BasicModelBinding(string name)
{
  ViewBag.Name = name;
  return View();
}
// in View
<div>
  @using(Html.BeginForm()) {
  <p>
    姓名:<input type="text" name="name" />
    <input type="submit" value="送出" />
  </p>
  }
</div>

在這裡,Razor 的 Html.BeginForm() 會轉換成 html 的 form 表單格式:

<form action="/VtoC/BasicModelBinding" method="post">
  <p>
    ID: <input name="name" type="text" />
    <input type="submit" value="送出" />
  </p>
</form>

使用 FormCollection 類別

// in Action
public ActionResult DemoFormCollection(FormCollection form)
{
  ViewBag.Name = form["name"];
  ViewBag.Age = form["age"];
  return View();
}
// in View
<div>
  @using(Html.BeginForm()) {
  <p>
    姓名:<input type="text" name="name" /><br />
    年紀:<input type="text" name="age" />
    <input type="submit" value="送出" />
  </p>
  }
</div>
<p>
  Your Name: @ViewBag.Name <br />
  Your Age: @ViewBag.Age
</p>

ActionResult

每個 controller 的 Action 最終要回傳一個實作 ActionResult 抽象類別的型別,例如:return View(),實際上是回傳一個 ViewResult 型別

ASP.NET MVC 5 實作了 9 種繼承自 ActionResult 的型別:

繼承自 ActionResult 型別Controller 類別方法描述
ContentResult
- FileContentResult
- FileStreamResult
- FilePathResult
Content()回傳文字內容
FileResultFile()輸出檔案內容
HttpNotFoundResultHttpNotFound()回應 HTTP 狀態碼
JavaScriptResultJavaScript()輸出 JavaScript 內容
JsonResultJson()輸出 JSON 內容
ViewResultView()輸出 HTML 內容
PartialViewResultPartialView()輸出部分 HTML 內容
RedirectResultRedirect()
RedirectPermanent()
進行 URL 重新導向
RedirectToRouteResultRedirectToAction()
RedirectToActionPermanent()
RedirectToRoute()
RedirectToRoutePermanent()
使用路由系統,進行 URL 重新導向

EmptyResult

  • EmptyResult 類別的 ExecuteResult 方法沒有實作任何程式碼
  • 為了遵循 OOP 的 Null Object Pattern(Null 物件模式),應該要回傳一個空物件,而非 null

ContentResult

Content 方法共有三個多載型別:

// 1.
Content(string content)
// 2.
Content(string content, string contentType)
// 3.
Content(string content, string contentType, Encoding contentEncoding)

其中,各參數代表的意義:

  • content:要 return 的內容,我們可以回傳純文字、HTML、excel 檔或是 CSV 檔 ⋯⋯ 等各種格式的 content
  • contentType:內容類型(MIME type),Mime type 的種類,像是常見的 text/html、text/csv ⋯⋯ 等
  • contentEncoding:編碼方式(可參考 System.Text.Encoding 類別),編碼方式,例如常見的 UTF8、Big5 之類的

JavaScriptResult

JavaScriptResult 本質上跟 ContentResult 一樣,但是它多設置了 ContentType

它用於動態產生 JavaScript 在 View 執行的情境,例如:當 JavaScript 的內容需要 Model 提供的時候

書中的範例如下:

// in Action
public ActionResult OnlineGame()
{
  return View();
}

public ActionResult NextTime()
{
  StringBuilder sb = new StringBuilder();
  sb.AppendFormat("var nextTime = '{0}'; \r\n", DateTime.UtcNow);
  return JavaScript(sb.ToString());
}
<!-- in View -->
<head>
  <script src='@Url.Action("NextTime", "MvcType")'></script>
</head>
<body>
  <div>
    <script>
      alert("伺服器時間: " + nextTime);
    </script>
  </div>
</body>

在 View 裡,@Url.Action 裡的參數分別為 ActionName、ControllerName

JsonResult

JSON 格式是目前前端接收 Web API 資料最常見的格式

ASP.NET MVC 提供將物件、Model 的資料轉換成 JSON 格式輸出,或是讀取 JSON 格式的資料

  • JsonResult 使用 JavaScriptSerializer 類別進行序列化工作
  • ContentType 為 application/json
  • 為避免 JSON Hijacking 攻擊,預設不允許接受 HTTP GET request
public ActionResult DemoJson()
{
  var person = new
  {
    Name = "Bruce",
    Age = 18,
    Birthday = new DateTime(2099, 9, 9)
  };

  return Json(person);
}

回傳的 JSON 資料如下:

{ "Name": "Bruce", "Age": 18, "Birthday": "/Date(4092566400000)/" }

HttpStatusCodeResult

HttpStatusCodeResult 有兩個子類別:HttpNotFound 與 HttpUnauthorizedResult

  • HttpNotFoundResult 用於「找不到」的情況
  • HttpUnauthorizedResult 用於「無存取權」的情況

System.Net.HttpStatusCode 這個 enum 提供很多種狀態,可以參考微軟官網

HttpNotFoundResult 會提供 404 狀態碼,HttpUnauthorizedResult 則會提供 401 狀態碼

書中對於 status code 的一段敘述:

當 client 端進行操作符合某一種狀態時, 我們應該把應用程式設計為回應一個狀態而不是回應錯誤拋出例外

RedirectResult & RedirectToRouteResult

重新導向(或稱為轉址)分為兩種:

  • 301 轉址:永久轉址(Permanent Redirect)
  • 302 轉址:暫時轉址(Temporary Redirect)

兩者的差異只有在 SEO 上,301 轉址會將舊網址的權重移轉至新網址上,302 則否

而 RedirectResult 與 RedirectToRouteResult 類別都提供 301 或 302 轉址

差異在於 RedirectResult 採用 URL 的指定方式進行轉址,而 RedirectToRouteResult 則是指定路由(Routing)來取得最後轉址的 URL

RedirectResult

RedirectResult 有兩種封裝的方法:

  • Redirect
  • RedirectPermanent

書中範例:

public ActionResult DemoRedirect(string param)
{
  if(!String.IsNullOrEmpty(param))
  {
    string baseUrl = "http://mvcbook.net/";
    Uri url = new Uri(baseUrl + param);
    return Redirect(url.ToString()); // 302 redirect
    // return RedirectPermanent(url.ToString()); // 301 redirect
  }
  else
  {
    return Content("error");
  }
}

RedirectToRouteResult

有四種方法:

  • RedirectToAction
  • RedirectToActionPermanent
  • RedirectToRoute
  • RedirectToRoutePermanent

RedirectResult 使用的是轉址至一個存在的 URL,通常是網站之外的 URL

而 RedirectToRouteResult 則使用內部的轉址,導至其他的 Action、或是其他 Controller 的 Action

我們當然也可以用 RedirectResult 轉址到其他的 Action

然而,利用字串去做轉址,當 routing 名稱改變的時候,

這個轉址也就自然會壞掉,而 IDE 也不會告訴你這裡需要修改,

因為對它而言只是一個字串而已,這也是為何內部轉址會用 RedirectToRouteResult 的原因

FileResult

FileResult 代表一個可下載的檔案,它有三個子類別:

  • FileContentResult:來源為 Byte[] 陣列
  • FilePathResult:來源為實體檔案路徑
  • FileStreamResult:來源為 Stream 類別

可下載的檔案要提供三個資訊,前兩個為必要資訊:

  1. 要下載的檔案內容:由剛才說的三個子類別提供
  2. Content Type:它是一種網路媒體類型(Internet Media Type),進行下載時,必須提供檔案類型的資訊,全球已註冊的 Media Type 可於 IANA(Internet Assigned Numbers Authority)機構查詢
  3. 指定下載檔案的名稱:此為選擇性資訊。FileResult 在指定下載檔案名稱時,採用的編碼是 RFC 2231;而目前瀏覽器均已支援 RFC 2231(UTF-8),所以中文檔名也不是問題

另外,關於 RFC 2231 編碼,作者這裡有一個 memo:

Internet Explorer 8(含)以前不支援 RFC 2231 標準, 也就是說,如果使用非英文與數字指定檔案名稱,將會以亂碼來呈現。 (微軟已於 2014/4/8 正式公告 WindowsXP 支援終止, 希望各位開發者未來的路會好走些。)

這段話看起來怎麼有點似曾相識?

近幾年,IE 仍然是前端開發界一個很大的痛點

而在今年(2022 年),微軟宣布終止支援 IE11,全面改用 Chromium 核心的 Edge,想必前端開發人員都很振奮

微軟總是每隔一段時間,很貼心地送給世人一個大禮物 🙃

ViewResult

前面的範例程式不斷出現的,在 Action 裡面 return View();

就是所謂的 ViewResult

View Engine 將 View 轉譯成 HTML 之後,再丟給瀏覽器去渲染

而 View Engine 可以分為兩種:WebFormViewEngine 及 RazorViewEngine

WebFormViewEngine 是最原始 ASP.NET MVC 的 View Engine,而後來引進的 RazorViewEngine 寫法上更威簡潔,兩者的語法如下:

ASPX 語法

<% forEach(var p in model) {%>
  <li><%=p.Name%></li>
<%}>

Razor 語法

@{
  forEach(var p in model) {
    <li>@p.Name</li>
  }
}

View Engine 轉譯的步驟

View Engine 轉譯大致分為三個動作:

  1. 取得對應的 View 檔案內容(檔案存取)
  2. 填入關聯的 Model 資料
  3. 轉譯取得 HTML,寫入資料流(資料流輸出)

Action Filters

從前一章 Routing 的章節我們知道,Routing 會用 URL 導向至對應的 Controller、對應的 Action

然而,進入 Action 之前之後,其實另外有好幾層處理

Action Filter 的職責就是事件監聽器,分別有五個種類:

  • Authentication Filter:驗證過濾器
  • Authorization Filter:授權過濾器
  • Action Filter:動作過濾器
  • Result Filter:結果過濾器
  • Exception Filter:例外過濾器

借用一下 dotnettricks 網站的圖來解釋這幾個 Action Filter 與 Action 的先後順序:

asp.net mvc pipeline

上面提到的每種 Action Filter 都能有三種運作層級:Action、Controller、Global

  • Action 層級:設置在某個 Action 上,此 Action Filter 就僅限這個 Action 之前或之後執行
  • Controller 層級:將 Action Filter 設置到 Controller,那麼 Controller 裡面所有的 Action 都會受影響
  • Global 層級:透過註冊,將 Action Filter 註冊到 GlobalFilterCollection 類別中,就會影響整個 APP

此外,若 Action 或 Controller 層級設置了多個相同的 Filter,則預設由上而下執行,也可以使用 Order 屬性調整順序,例如:

[AF1(order = 3)]
[AF2(order = 2)]
[AF3(order = 1)]
public ActionResult ActionOrder(){}

Authentication Filter

資訊安全上,AAA 是一種標準做法,它分別代表「Authentication(驗證)」、「Authorization(授權)」、「Accounting(帳戶)」

  • Authentication:確認「你」的唯一身分
  • Authorization:你能做什麼或你不能做什麼

Authorization Filter

Action Filter

IActionFilter interface 提供兩個方法,分別在 Action 執行的前後觸發執行:

public interface IActionFilter
{
  void OnActionExecuting(ActionExecutingContext filterContext);
  void OnActionExecuted(actionExecutedContext filterContext);
}

AsyncTimeout 屬性

可以設定 Action 的 timeout 時間(毫秒,ms):

public class TestAsyncController: AsyncController
{
  [AsyncTimeout(10000)]
  public void DownloadAsync(string url)
  {
    // ...
  }

  public ActionResult DownloadCompleted(string content)
  {
    return Content(content);
  }
}

NoAsyncTimeout 屬性

也可以讓 Action 沒有 timeout 時間,也就是無限等待:

[NoAsyncTimeout]
public void DownloadAsync(string url) {
  // ...
}

Result Filter

IResultFilter interface 提供兩個方法,分別在進行 View 處理之前之後觸發:

public interface IResultFilter
{
  void OnResultExecuting(ResultExecutingContext filterContext);
  void OnResultExecuted(ResultExecutedContext filterContext);
}

我們在上面的關於 Action Filter 的流程圖中可以看到, Result Filter 在整個 Result Execution 當中的開始與結束都會遇到, 其順序為:

  • OnResultExecuting()
  • InvokeActionResult()
  • OnResultExecuted()

而 Result Filter 最典型的應用就是 OutputCache 機制, 也就是將執行結果快取起來,節省節省預算資源、加快回應速度

[OutputCache(Duration = 10)]
public ActionResult GetCacheTime()
{
  ViewBag.Time = DateTime.now;
  return View();
}

duration 代表快取的間隔時間,在這段時間內,除了第一次的請求外,都是從快取取得資料

除了 duration 之外,還有其他參數可以設定,在此就不多作介紹

Exception Filter

整個 Action Filters 拋出的例外,都將由 Exception Filter 進一步來處理,簡單的範例如下:

[HandleError(View = "Error", ExceptionType = typeof(Exception))]
public ActionResult Index()
{
  throw new Exception("測試 Error 頁面");
  return View();
}

預設的 Error View Page 會在 /Views/Shared/Error.cshtml 下:

@model System.Web.Mvc.HandleErrorINfo
@{
  ViewBag.Title = "錯誤";
}
<h1 class="text-danger">錯誤。</h1>
<h2 class="text-danger">處理您的要求時發生錯誤。</h2>

當然,如果是以 SPA 的架構下,就不需要這個由 server 提供的 error page 了

Action Filters 介面與擴充

我們當然也可以自行撰寫一些客製化的 Action Filter,原生的 Action Filters 都實作 ASP.NET MVC 提供的 interface

因此,若要客製化自己的 Action Filter,也要從這些 interface 中挑選需要的來實作

ASP.NET MVC 的 interface 有:IAuthenticationIAuthorizationIActionFilterIResultFilterIExceptionFilter

參考資料