在 ASP.NET MVC 的 模型繫結 (Model Binding) 完成之後,我們可以在 Controller / Action 中取得 ModelState 物件,一般來說我們都會用 ModelState.IsValid 來檢查在「模型繫結」的過程中所做的 輸入驗證 (Input Validation) 與 模型驗證 (Model Validation) 是否成功。不過,這個 ModelState 物件的用途很廣,裡面存有非常多模型繫結過程的狀態資訊,不但在 Controller 中能用,在 View 裡面也能使用,用的好的話,可以讓你的 Controller 更輕、View 也更乾淨,本篇文章將分享幾個 ModelState 的使用技巧。
本篇文章的範例程式我已經發佈到 GitHub,請從以下網址取得程式原始碼:
https://github.com/doggy8088/UnderstandingModelState
我的這個範例程式非常簡單,在預設的 ASP.NET MVC 5 專案範本建立好之後,我只建立了一組 /Home/Login 頁面,其 Action 程式碼如下圖示:

至於 View 的部分,我也是只是透過 Add View (新增檢視) 建立起一個預設的檢視頁面而已:

這個頁面的輸出畫面如下圖示:

首先,我們就先來看看 ModelState 最常見的用法:ModelState.IsValid
以下這段程式碼範例,是一段真實可行的程式碼,先透過 ModelState.IsValid 檢查是否必要的欄位都有輸入,然後透過 LoginCheck 檢查帳號密碼是否正確。
只要登入驗證成功,就會寫入一個 FormsAuthentication 的 Cookie,並透過 HTTP 302 轉向到首頁;如果登入失敗,就會繼續顯示這個 Login 表單頁面,並且傳入本次模型繫結傳入的 login 物件 ( LoginViewModel 型別 ):

接著我們嘗試在表單執行登入,並且輸入一個錯誤的帳號、密碼,帳號我輸入 test@example.com 密碼則是 test (錯誤密碼)。
這時你會發現頁面上會再次出現上一頁所輸入的 Email 與 Password 欄位資料,原因是我們在 Login Action 有在傳入一次 login 物件到 View 裡面,所以自動將資料繫結到 View 裡面進去了:

但目前我們的程式有點問題,就是登入失敗的時候並沒有顯示錯誤訊息,這樣對使用者會有點困擾。這時我們可以透過 ModelState.AddModelError 來新增一個自訂錯誤訊息:

這時我們重新送出表單資料,你會發現錯誤訊息會正好顯示在 "密碼" 欄位下方,也就是 View 裡面 @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" }) 的地方。
換句話說,這個 @Html.ValidationMessageFor 會從 ModelState 中取得錯誤訊息內容,並顯示出來。如果你在 Model 中有實作 輸入驗證 (Input Validation) 與 模型驗證 (Model Validation) 的話,錯誤訊息最終也是會寫到 ModelState 物件裡面,所以我們可以透過控制 ModelState 的內容來控制 View 的顯示邏輯。

接下來我們把第 52 行準備傳給 View 的 Model 也給拿掉 ( 將 login 物件從參數列中移除 ),並對專案重新建置。

我們再重新 POST 登入表單,這時你會發現 "密碼" 欄位的內容消失了,這點應該很合理,因為我們根本沒從 Action 傳資料給 View,沒顯示是正常的吧!
不正常的地方應該在 "電子郵件" ( Email ) 這個欄位吧,明明沒傳入資料,為什麼還會顯示上一頁輸入的內容呢?聰明的你,應該已經想到了,這個欄位資料就是從 ModelState 取得的,所以你應該會慢慢了解到 ModelState 的重要性!

所以你現在應該已經知道,原來在 ModelState 裡面,不單單只有「驗證失敗的錯誤訊息」,其實還有「模型繫結過程中得到的資料」啊!
假設我們想要清除所有已經儲存在 ModelState 裡面的內容,你可以用 ModelState.Clear(); 來清除 (包含錯誤訊息與模型繫結的資料 都會被清空),如下圖示:

由於我們在執行模型繫結的過程中會有許多欄位被繫結進來,如果你只想清除特定一個欄位的內容 (包含錯誤訊息與模型繫結的資料),可以使用 ModelState.Remove("Email"); 來清除。
※ 請注意:這個方法會同時清除 ModelState["Email"] 這筆模型狀態的錯誤訊息與模型繫結的資料喔!

如果你只想刪除模型繫結的資料並且想保留錯誤訊息 (該欄位的驗證結果) 的部分,你還可以使用以下程式碼片段來重新指定新的模型繫結資料:
if (!ModelState.IsValidField("Password"))
{
var emptyValue = new ValueProviderResult(
string.Empty,
string.Empty,
System.Globalization.CultureInfo.CurrentCulture);
ModelState.SetModelValue(
"Password",
emptyValue);
}

最後,如果你想要更細部的了解 ModelState 的內部結構,可以透過下圖了解到存取每個欄位的 ModelState 內容的方法:
由下圖的 foreach 迴圈可以知道,ModelState 可以透過枚舉的方式取得每一個欄位的 ModelState 內容,然後可以用 ModelState.IsValidField 查出該欄位是否有驗證錯誤的狀態,然後可以再透過一個 foreach 迴圈取得所有該欄位 ModelState 的所有錯誤 (因為可能不只一個錯誤),最後在取得 err.ErrorMessage 得知錯誤訊息的確切內容或 err.Exception 取得該模型的例外狀況物件 (如果有的話)。

如果你也想透過這種 "弱型別" 的方式取得每個欄位的值 (透過 Model Binding 得到的值),也可以透過以下徒的方式來取得 RawValue (透過 Model Binder 轉型後的值) 或 AttemptedValue (將值轉換成可顯示的字串值) 屬性內容。

講到這裡,你應該對 ModelState 遊刃有餘了,本篇文章的最後一個技巧,則是如何從 View 中取得 ModelState 的內容。
如下圖示,你只要從 View 中透過 ViewData.ModelState 就可以取得所有 ModelState 的相關資訊。

各位不要小看這個技巧,因為有時候你會想從 View 取得特定 Model Binding 的資料 (例如分頁資訊),但這個 Model Binding 的資料並沒有從 Controller 中透過強型別的 Model 傳給 View 使用 ( ViewData.Model ),以往我們都要特別在用一個 ViewBag 來將這些參數傳給 View 使用,但這些 Code 在 Controller 中就會覺得有點多餘。知道了這個技巧,就可以在 Controller 少幾行 Code,而直接從 View 裡面取得 ModelState 的內容即可 (也就是取得本次 Model Binding 過程中的值)。
本文章所有操作步驟的原始碼,都已經做好 Git 版控,每個版本之間的差異都可以從 GitHub 上的 UnderstandingModelState 專案查看每個版本的修改歷史。
相關連結