[鐵人賽Day15] ASP.Net Core MVC 進化之路 - Model Validation(1) / 前端 vs. 後端驗證

講完了Controller的基礎應用後,

本文將介紹ASP.Net Core中的Model Validation。

 

 

許多人將Model Validation翻成模型驗證,

但筆者比較習慣稱呼它為資料驗證

 

資料驗證主要是檢查HTML表單中的欄位內容是否符合規則,

常見的如email格式、信用卡卡號、電話號碼等。

而資料驗證又可分為前端驗證及後端驗證,

雖然都是作資料檢查的工作,

但在本質上還是有很大的差別。

前端驗證是在Client端使用Javascript預先檢查表單的內容,

而後端驗證則在Server端作為資料保護最後一道防線,

簡單示意圖如下。

既然後端驗證是最後一道防線,

幹嘛還要脫褲子放屁作兩次?


使用前端驗證除了能有效減少Request的數量(因為後端驗證就會發Request),

也會讓使用者體驗比較舒服一點(畫面不會閃一下),

後端驗證也能透過Ajax達到畫面不閃的效果(遠端驗證Remote Validation)。

 

理論上資料驗證是需要分開作兩次的,

但如果使用ASP.Net Core MVC開發網站,

你只需要做一次。

 

我們要先來講講後端驗證的使用方式。

ASP.Net Core MVC中我們可以在Model中設定後端驗證

只要在屬性掛上[ValidationAttribute]可以達到後端驗證的效果。

這裡簡單摘列幾種內建常用的[ValidationAttribute]

  • [Required]:驗證欄位內容是否為空值或空字串。

  • [EmailAddress]:驗證欄位內容是否符合信箱格式。

  • [Compare]:比對兩個欄位內容是否相同。

  • [Range]:驗證欄位數值是否介於指定範圍中。

  • [RegularExpression]:驗證資料符合指定的規則運算式。

  • [MaxLength][MinLength][StringLength]:驗證字串長度。

  • [Url]:驗證欄位內容是否符合URL格式 。

 

我們來簡單的寫個範例。

public class Book
{
    [Required]
    public int Id { get; set; }

    [Required]
    public string Title{ get; set; }
        
    public DateTime PublishDate { get; set; }
}

我們將Id及Title設定為必要輸入欄位,

之後在Controller中加入下面程式碼。

[HttpGet]
public IActionResult Create()
{
    return View();
}

 

接著使用範本建立預設檢視(在程式碼區塊內按右鍵 > 新增檢視 )

 

按下新增後它按照我們提供的Model產生Create.cshtml,

我們故意先將Id的部分註解調。

@model IronmenMvcWeb.Models.Book

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Book</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <!--故意將Id註解掉-->
            @*<div class="form-group">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>*@
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="PublishDate" class="control-label"></label>
                <input asp-for="PublishDate" class="form-control" />
                <span asp-validation-for="PublishDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

 

接著建立[HttpPost] 的Action,

請記得要加入ModelState.IsValid 判斷,

才會對資料欄位執行後端驗證。

[HttpPost]
public IActionResult Create(Book book)
{
    if(ModelState.IsValid)
    {
        //Create book.
    }
    return View();
}

 

接著直接按下F5執行,

隨便填個內容之後按下Create送出。

 

透過Debug模式觀察Book資料,

可以看到我們雖然沒有填入Id的值,

但它預設幫我們產生「0」這個數值。

 

這導致了驗證的效果達不到預期的效果。

 

但為什麼Id並沒有被視為null呢?

在資料綁定(Model Binding)過程沒有收到資料時,

會將結構型別(struct)使用初始值當作預設值

參考型別的話則會顯示null

一般常見的結構型別intdoublefloatDateTime等。

參考型別則如string及一般的Class

 

在過去我們會使用Nullable<int>來解決這個問題(簡寫為int?)

然後接著觀察這個數值是否為null。

但ASP.Net Core有一個新玩意兒可以解決 - [BindRequired]

設定方法只要把它原封不動掛上去可以了。

public class Book
{
    [BindRequired]
    public int Id { get; set; }

    [Required]
    public string Title{ get; set; }
        
    public DateTime PublishDate { get; set; }
}

 

再觀察一次ModelState的狀態,

會發現一樣的操作模式卻顯示為false了。

如果想讓[Required]擁有[BindRequired]的特性可以參考這篇

 

剛才講的全是後端驗證的方式,那前端驗證呢?

上面有提到前端是使用javascript來驗證的,

所以第一步我們要把前端驗證的套件加進來。

在範本專案中很貼心地已經幫我們安裝好了,

只要在.cshtml中加入下方程式碼即可啟動前端驗證。

@section scripts{
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
}

 

接著再測試一次,

不輸入資料就按下Create送出,

會得到一樣的驗證提示,

但差別在於畫面不會再轉了(沒有送Request)。

 

有關為什麼可以只做一次這個問題,

其實是Tag Helper的魔法。

View Engine.cshtml轉譯成.html的過程,

會讀取Model的驗證條件,

然後自動幫<input>的欄位加上前端驗證套件(javascript)看得懂的data-annotaion

Book中的Title屬性為例:

<input class="form-control input-validation-error" 
 type="text" data-val="true" data-val-required="The Title field is required." 
 id="Title" name="Title" value="" 
 aria-describedby="Title-error" aria-invalid="true">

 

使用時請記得把前端套件include進來,

不然前端驗證是不會動的!

 

關於前後端的驗證簡單介紹到這邊,

下篇會介紹如何撰寫自訂的驗證。

 

參考

https://docs.microsoft.com/zh-tw/aspnet/core/mvc/models/validation?view=aspnetcore-2.1

https://www.strathweb.com/2017/12/required-and-bindrequired-in-asp-net-core-mvc/