﻿# ASP.NET MVC 開發心得分享 (25)：ModelBinder 與 ValueProvider 的用途

使用 ASP.NET MVC 的人應該知道 **模型繫結** (Model Binding) 是個功能十分強大的設計，早在 ASP.NET MVC 1 就有了 Model Binder 的設計，不過從 ASP.NET MVC 2 開始新增了一個 Value Provider 設計，這部分一直都不太有人提及，今天我就來說說這兩者之間的差異與實際的運作方式。

提到 ASP.NET MVC 的執行生命週期，一個最簡單的解釋為：

1.  由 **路由** (Routing) 解析網址路徑
2.  執行 **模型繫結** (Model Binding) 將瀏覽器傳入的資訊綁定到 **控制器** (Controller) **動作方法** (Action Method) 的參數列中
3.  執行控制器動作方法，並回傳 **動作結果** (ActionResult)
4.  執行 **動作結果** (如果為 ViewResult 就會去執行 View )

上述第 2 步驟的「**模型繫結**」其實在內部做了很多事情，最主要的功能就是將瀏覽器傳到 **控制器** 的各種資料 (表單資料、JSON、路由參數、查詢字串、…) 轉換成 **強型別** 物件。

例如說，我們有個 ViewModel 類別如下：

```
public class Test2ViewModel
{
    public int id { get; set; }
    public string Name { get; set; }
}
```

然後我們有個控制器的動作方法如下：

```
public ActionResult Test2(Test2ViewModel data)
{
    return View(data);
}
```

當瀏覽器送出 HTTP 要求到上述的 Test2 動作方法 (Action method) 時，由於該動作方法所宣告的 Test2ViewModel 是一個強型別模型，因此 MvcHandler 在執行這個動作方法前，就會設法執行「模型繫結」動作，試圖建立一個 Test2ViewModel 型別的 data 物件 (其 data 為該動作方法的參數名稱)。

我想大部分的 ASP.NET MVC 開發人員多少都可以說出「只要傳入的表單欄位名稱等於 Test2ViewModel 的其中一個屬性名稱，該屬性的值就會自動被填入」這樣的說法。這樣的說法是沒有錯，但我今天要在講得深入一點。

事實上 ASP.NET MVC 在執行「模型繫結」動作的時候，其實是先透過內建的 DefaultModelBinder 來將**瀏覽器傳來的資料**轉換為**強型別物件**。在我們上述這個簡單的例子，就是要盡可能的滿足 Test2 動作方法的 data 參數要求 (將資料填入)，該參數為 Test2ViewModel 型別，因此 DefaultModelBinder 會試圖找出所有可以填滿 Test2ViewModel 類別中所有的屬性，但重點是，這些資料要怎麼取得呢？是的，取得資料的動作，就是靠 ValueProvider 來處理。

你可以查看 ASP.NET MVC 開源專案的 [ValueProviderFactories.cs](https://github.com/aspnet/AspNetWebStack/blob/main/src/System.Web.Mvc/ValueProviderFactories.cs) 原始碼內容，這裡定義了一個 ValueProviderFactoryCollection 預設包含了 6 個不同的 ValueProviderFactory，分別用來取得各自不同的「**資料來源**」：

```
namespace System.Web.Mvc
{
    public static class ValueProviderFactories
    {
        private static readonly ValueProviderFactoryCollection _factories = new ValueProviderFactoryCollection()
        {
            new ChildActionValueProviderFactory(),
            new FormValueProviderFactory(),
            new JsonValueProviderFactory(),
            new RouteDataValueProviderFactory(),
            new QueryStringValueProviderFactory(),
            new HttpFileCollectionValueProviderFactory(),
        };

        public static ValueProviderFactoryCollection Factories
        {
            get { return _factories; }
        }
    }
}
```

由於從瀏覽器傳來的資料可能有很多種格式，有從表單 HTTP POST 過來的、有從 AJAX 發送 **JSON** 資料過來的、有從 **路由資料** (Route Data) 過來的、有從 **查詢字串** (Query String) 來的，所以 ASP.NET MVC 建立了一個公開的 [ValueProviderFactories](https://github.com/aspnet/AspNetWebStack/blob/main/src/System.Web.Mvc/ValueProviderFactories.cs) 靜態類別，預設定義了以下資料來源的解析類別，這些類別將**依照順序**的解析各種資料：

1.  ChildActionValueProviderFactory 取得從另一個 View 透過呼叫 @Html.Action 傳來的資料
2.  FormValueProviderFactory 取得從瀏覽器表單 HTTP POST 過來的所有欄位資料
3.  JsonValueProviderFactory 取得從瀏覽器的 JavaScript 透過 XHR 傳過來的 JSON 資料
4.  RouteDataValueProviderFactory 取得從網址路徑取得的到的路由參數值
5.  QueryStringValueProviderFactory 取得從瀏覽器網址列上的 **查詢字串** (Query String) 傳來的值
6.  HttpFileCollectionValueProviderFactory 取得從瀏覽器表單透過檔案上傳功能傳來的檔案

這裡要注意的是，上述這 6 個不同的 ValueProvider 是有順序性的，會由上而下逐一取得資料，當 DefaultModelBinder 試圖取得某一個欄位的資料時，就會依序呼叫不同的 ValueProvider 來取得其值，如果第一個沒取得需要的值，就會換用下一個來取得資料，如果你在 FormValueProviderFactory 這個階段就取得需要的值的時候，就不會再往下嘗試。

我舉一個實際的例子：

-   假設有個網頁透過 JavaScript 發出一個 AJAX 要求，並傳遞一個 JSON 資料過來
-   這時 Test2 動作方法準備執行，由於 Test2 動作需要傳入一個 data 參數，且該參數型別為 Test2ViewModel
-   此時 MvcHandler 便呼叫 DefaultModelBinder 設法將可能的資料讀入，並將讀入的資料轉型成 Test2ViewModel 型別中的兩個屬性，也就是 int id 與 string Name 這兩個屬性
-   由於 DefaultModelBinder 必須一個一個解析每個屬性，所以他會先嘗試取得 int id 的資料

-   由於 DefaultModelBinder 只負責取得資料並將資料轉型成必要的型別，不負責解析瀏覽器傳來的資料，所以這次 DefaultModelBinder 便呼叫 ValueProvider 出來幫忙解析與取得資料
-   由於 DefaultModelBinder 不會知道資料到底在哪裡，所以 ValueProvider 就會開始依序叫用不同的 ValueProviderFactory 出來解析各種可能的資料來源
-   依照 ValueProviderFactories 註冊的順序，他先透過 ChildActionValueProviderFactory 查看是否有從 ChildAction 來的 **id** 資料？結果沒有！
-   再來透過 FormValueProviderFactory 嘗試解析是否有上一頁表單傳來的 **id** 欄位，由於我們這次是由 AJAX 發出的 JSON 資料，所以一樣沒找到資料。
-   接著透過 JsonValueProviderFactory 嘗試解析是否有上一頁傳來的 JSON 資料，結果這次解析成功，所以 JsonValueProviderFactory 將把屬性名稱為 **id** 的資料解析後回傳給 DefaultModelBinder
-   DefaultModelBinder 得到 **id** 屬性的資料後，便將資料轉型為 **int** 型別，並將其值寫入到 data 物件的 id 屬性中
-   取得屬性值後，會先找出該 **id** 屬性是否有相對應的 **驗證屬性** (ValidationAttribute) 需要進行 **輸入驗證** (Input Validation)，例如 \[Required\] 之類的驗證屬性。如果有，就會檢查該屬性是否符合必要的輸入驗證，如果驗證失敗，就會修改 **ModelState.IsValid** 的值為 **false**。

-   接著 DefaultModelBinder 要再解析下一個屬性，所以他會接著嘗試取得 string Name 這個屬性的資料

-   由於 DefaultModelBinder 只負責取得資料並將資料轉型成必要的型別，不負責解析瀏覽器傳來的資料，所以這次 DefaultModelBinder 便呼叫 ValueProvider 出來幫忙解析與取得資料
-   由於 DefaultModelBinder 不會知道資料到底在哪裡，所以 ValueProvider 就會開始依序叫用不同的 ValueProviderFactory 出來解析各種可能的資料來源
-   依照 ValueProviderFactories 註冊的順序，他先透過 ChildActionValueProviderFactory 查看是否有從 ChildAction 來的 **Name** 資料？結果沒有！
-   再來透過 FormValueProviderFactory 嘗試解析是否有上一頁表單傳來的 **Name** 欄位，由於我們這次是由 AJAX 發出的 JSON 資料，所以一樣沒找到資料。
-   接著透過 JsonValueProviderFactory 嘗試解析是否有上一頁傳來的 JSON 資料，結果這次解析成功，所以 JsonValueProviderFactory 將把屬性名稱為 **Name** 的資料解析後回傳給 DefaultModelBinder
-   DefaultModelBinder 得到 **Name** 屬性的資料後，便將資料轉型為 **string** 型別 (其實本來就是 string 型別了)，並將其值寫入到 data 物件的 **Name** 屬性中
-   取得屬性值後，會先找出該 **Name** 屬性是否有相對應的 **驗證屬性** (ValidationAttribute) 需要進行 **輸入驗證** (Input Validation)，例如 \[Required\] 或 \[StringLength(10)\] 之類的驗證屬性。如果有，就會檢查該屬性是否符合必要的輸入驗證，如果驗證失敗，就會修改 **ModelState.IsValid** 的值為 **false**。

-   當 DefaultModelBinder 已經完整取得所有可能的屬性，所有找不到的屬性都會自動變成該屬性的預設值 [default(T)](https://msdn.microsoft.com/en-us/library/xwth0h0d.aspx)
-   最後，還會檢查 Test2ViewModel 是否有實作 IValidatableObject 介面，如果有的話，就還會最後做一次 **模型驗證** (Model Validation)，如果驗證失敗，一樣會修改 **ModelState.IsValid** 的值為 **false**。
-   上述「模型繫結」工作結束後，就會交由 MvcHandler 正是呼叫 Test2 動作方法，並將取得後的 Test2ViewModel data 參數值傳入。

相信透過上述的說明，可以讓大家徹底了解 ModelBinder 與 ValueProvider 的真正用途與分工！ (如果真的有看懂的話啦) XD

其實這個主題很有趣，也很重要，因為我們使用 ASP.NET MVC 開發網站偶爾還是會遇到鬼打牆的情況，例如我們在特定一個動作方法綁定一個 int id 參數

-   **public ActionResult Test(int id) {  
      // …  
    }**

到底這個 id 參數傳入的值為何呢？假設你的網址長這樣，同時有路由參數 id 又有查詢字串的 id：

-   **http://localhost/Home/Test/1?id=2**

請問你可否在開發時期確定 public ActionResult Test(int id) 所得到的 id 值到底是 1 還是 2 呢？

如果上述網站，再加上前一頁的表單 POST 過來的欄位也包含一個 id 欄位為 **3** 呢？請問你可否在開發時期確定 public ActionResult Test(int id) 所得到的 id 值到底是 1 是 2 還是 **3** 呢？

如果你知道 [ValueProviderFactories](https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/ValueProviderFactories.cs#L10-L16) 的設定順序，我想這個答案你就會了然於心，下次用起模型繫結也不會太擔心想錯了。 ;-)

**相關連結**

-   [What’s the Difference Between a Value Provider and Model Binder? - You've Been Haacked](http://haacked.com/archive/2011/06/30/whatrsquos-the-difference-between-a-value-provider-and-model-binder.aspx/)
-   [default Keyword in Generic Code (C# Programming Guide)](https://msdn.microsoft.com/en-us/library/xwth0h0d.aspx)
-   [The Will Will Web | ASP.NET MVC 開發心得分享 (6)：小心使用 FormCollection](/post/2009/06/15/ASPNET-MVC-Developer-Note-Part-6-Model-Binder-and-FormCollection.aspx)
