[Required]與[BindRequired] Attribute

by vivid 8. 一月 2020 03:05

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N200121501
出刊日期: 2020/1/8

在ASP.NET Core MVC的專案中,我們利用「System.ComponentModel.DataAnnotations」命名空間下的Attribute類別設定模型屬性的資料驗證規則,例如「Range」Attribute用來指定資料欄位值的數值範圍;「StringLength」Attribute 用來指定資料欄位中允許的最小和最大字元長度。這些Attribute會應用在伺服端模型繫結(Model Binding)的過程中進行資料驗證,若用戶端傳送到伺服端的資料不滿足指定的條件約束,就會將「ModelState.IsValid」設定為「false」,而在檢視(View)中,便可以利用「asp-validation-for」與「asp-validation-summary」來顯示錯誤訊息。這篇文章要討論的是兩個非常相似的「Required」與「BindRequired」Attribute。

「Required」Attribute指定表單欄位驗證時,欄位必須包含值。 如果屬性值為「null」、空字串(""),或只包含空白字元,則會引發驗證例外狀況。

我們先來看一下下面「Required」Attribute的範例,「Employee」模型的「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個屬性定義的上方都套用了 [Required] Attribute,要求其值不可以為空白,同時利用「ErrorMessage」屬性自訂了錯誤訊息:「Not Empty!」。

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace StarterM.Models {
  public class Employee {
    [Display( Name = "Employee Id" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeId { get; set; }
    [Display( Name = "Employee Name" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeName { get; set; }
    [Display( Name = "Birth Date" )]
    [DataType( DataType.Date )]
    [Required( ErrorMessage = "Not Empty!" )]
    public DateTime BirthDate { get; set; }
    [Display( Name = "Age" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public int Age { get; set; }
  }
}

 

在「HomeController」控制器中,我們撰寫了兩個「Create」方法,在標識「HttpPost」Attribute的「Create」方法之中,利用「ModelState.IsValid」屬性的值來驗證資料是否有問題,當資料都不為空白的情況下,讀出這些資料,利用「ViewBag」傳送到檢視顯示在網頁上;若驗證有問題,便從「ModelState」讀出所有錯誤,並將發生錯誤的屬性名稱(Key)與錯誤訊息(ErrorMessage)串接成字串,利用「ViewBag」傳送到檢視顯示在網頁上:

HomeController

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using StarterM.Models;
using System.Linq;

namespace StarterM.Controllers {
  public class HomeController : Controller {
    public IActionResult Index() {
      return View();
    }
    public IActionResult Create() {
      return View();
    }
    [HttpPost]
    public IActionResult Create( Employee emp ) {
      if ( ModelState.IsValid ) {
        ViewBag.result = $@"
        Employee id : {emp.EmployeeId}
        Employee Name : {emp.EmployeeName}
        Birth Date : {emp.BirthDate}
        ";
      }
      else {
        string sp = "<br/>";
        string msg = "";
        foreach ( var item in ModelState ) {
          if ( item.Value.ValidationState == ModelValidationState.Invalid ) {
            msg += $" {sp} {item.Key} : {string.Join( null , item.Value.Errors.Select( i => sp + i.ErrorMessage ) )}";
          }
          msg += sp;
        }
        ViewBag.result = msg;
      }
      return View();
    }
  }
}

 

以下的「Create」檢視程式碼則利用<form>標籤來送出表單資料:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                <div>
                    <label asp-for="EmployeeName"></label>
                    <input asp-for="EmployeeName" />
                    <span asp-validation-for="EmployeeName"></span>
                </div>
                <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

請參考下圖所示,從執行結果可以發現,我們未在表單上填入任何資料,就透過表單將「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個表單欄位傳送到伺服端,模型繫結器(Model Binder)將會根據這些屬性套用的Attribute進行資料驗證。套用[Required] Attribute的「EmployeeId」、「EmployeeName」兩個屬性都是字串型別(參考型別),能夠正確的顯示出我們設定的自訂錯誤訊息「Not Empty!」;而「BirthDate」(DateTime型別)與 「Age」(int型別)顯示的卻是內建的錯誤訊息:「The value '' is invalid.」,很顯然[Required] Attribute對於「DateTime」、「int」等實值型別沒有作用:

clip_image002

圖 1:[Required] Attribute驗證結果。

讓我們在請求中只送出「EmployeeId」與「BirthDate」兩個表單欄位,註解掉「Create」檢視程式中「EmployeeName」與「Age」兩個<input>欄位的相關標籤:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                @*<div>
                    <label asp-for="EmployeeName"></label>
                    <input asp-for="EmployeeName" />
                    <span asp-validation-for="EmployeeName"></span>
                </div>*@
                <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>

                @*<div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>*@
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

 

同樣不要在表單欄位上填入任何資料,將「EmployeeId」、「BirthDate」兩個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。從驗證的結果可以發現,這次表單只提供「EmployeeId」、「BirthDate」兩個表單欄位,換句說請求中未包含「Age」與「EmployeeName」,而「Age」屬性的型別為「int」,套用[Required] Attribute並無法檢測出錯誤,底下沒有印出任何關於「Age」欄位的錯誤訊息,這表示「ModelState」物件沒有記錄任何關於「Age」的錯誤資訊。而屬性型別為「string」的「EmployeeName」則可如預期運作。這是因為「int」型別的資料其預設值為「0」,由於要求「Age」不可為空白,不管用戶端有沒有傳「Age」到伺服端,它的預設值都是「0」,[Required] Attribute無法識別出這個問題。「BirthDate」這次顯示的是內建的錯誤訊息,請參考下圖所示:

clip_image004

圖 2:[Required] Attribute並無法檢測非Nullable型別的錯誤。

「DateTime」型別和「int」型別有相同的問題,它的預設值是「1/1/0001 12:00:00 AM」。修改「Create」檢視程式,這次讓我們在請求中只送出「EmployeeId」與「Age」兩個表單欄位:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                @*<div>
                    <label asp-for="employeename"></label>
                    <input asp-for="employeename" />
                    <span asp-validation-for="employeename"></span>
                </div>
               <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>*@

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

不要在表單欄位上填入任何資料,將「EmployeeId」、「Age」兩個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。從驗證的結果可以發現,[Required] Attribute並無法正確顯示出「Age」的自訂錯誤,顯示的是模型繫結內建的錯誤訊息,而「ModelState」物件沒有記錄任何關於「BirthDate」的錯誤資訊,請參考下圖所示:

clip_image006

圖 3:[Required] Attribute並無法檢測非Nullable型別的錯誤。

從測試中可以得到一個結論,在模型屬性型別為「int」、「DateTime」的情況下,[Required] Attribute無法檢測請求(Request)中空白的表單欄位值。除了這兩個型別之外,不可為Null的型別都有相同的問題。

 

使用Nullable型別

這個問題的其中一個解法便是使用Nullable型別,讓「int」、「DateTime」屬性都變成可允許「Null」的型別(Nullable),修改「Employee」模型的程式如下:

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace StarterM.Models {
  public class Employee {
    [Display( Name = "Employee Id" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeId { get; set; }
    [Display( Name = "Employee Name" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeName { get; set; }
    [Display( Name = "Birth Date" )]
    [DataType( DataType.Date )]
    [Required( ErrorMessage = "Not Empty!" )]
    public DateTime? BirthDate { get; set; }
    [Display( Name = "Age" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public int? Age { get; set; }
  }
}


 

修改「Create」檢視如下,利用<form>標籤來送出「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個表單欄位資料:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                <div>
                    <label asp-for="EmployeeName"></label>
                    <input asp-for="EmployeeName" />
                    <span asp-validation-for="EmployeeName"></span>
                </div>
               <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

 

這次執行的結果如下,[Required] Attribute 生效了:

clip_image008

圖 4:[Required] Attribute可以檢測Nullable型別的錯誤。

雖然修改「Employee」模型的屬性型別為Nullable可以解決驗證資料不可為空白的問題,但這會造成一個問題,若搭配Entity Framework Core Code First 來設計資料存取程式,屬性的型別會影響到資料庫資料表欄位的結構,Nullable型別對應的欄位結構是「允許Null值」,非Nullable型別對應的欄位結構是「不允許Null值」,因此這個解法並不一定會是你想要的。

[BindRequired] Attribute

另一個解法便是使用[BindRequired] Attribute,強制請求(Request)一定要提供屬性值。現在修改「Employee」類別如下,「BirthDate」、「Age」的型別分別改為不可為Null的「DateTime」與「int」型別:

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace StarterM.Models {
  public class Employee {
    [Display( Name = "Employee Id" )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired]
    public string EmployeeId { get; set; }
    [Display( Name = "Employee Name" )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired]
    public string EmployeeName { get; set; }
    [Display( Name = "Birth Date" )]
    [DataType( DataType.Date )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired ]
    public DateTime BirthDate { get; set; }
    [Display( Name = "Age" )]
    [Required( ErrorMessage = "Not Empty!" )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired]
    public int Age { get; set; }
  }
}

 

請參考下圖所示,從執行結果可以發現,我們未在表單上填入任何資料,就透過表單將「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。

請求中有包含「EmployeeId」、「EmployeeName」兩個字串型別屬性,其值為「Null」,模型繫結器沒有記錄任何錯誤;而「Age」(int型別,值為0)與「BirthDate」(DateTime型別,值是「1/1/0001 12:00:00 AM」)都各包含兩個錯誤「The value '' is invalid.」,以及「The value for XXX parameter or property was not provided」,顯示的是內建模型繫結錯誤訊息。

clip_image010

圖 5:[BindRequired] Attribute驗證錯誤。

修改「Create」檢視如下,註解掉「EmployeeName」、「BirthDate」兩個表單欄位:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                @*<div>
                        <label asp-for="EmployeeName"></label>
                        <input asp-for="EmployeeName" />
                        <span asp-validation-for="EmployeeName"></span>
                    </div>
                    <div>
                        <label asp-for="BirthDate"></label>
                        <input asp-for="BirthDate" />
                        <span asp-validation-for="BirthDate"></span>
                    </div>
                *@

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

 

同樣不要在表單欄位上填入任何資料,將「EmployeeId」、「Age」兩個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。

clip_image012

從驗證的結果可以發現:

· 請求中包含「EmployeeId」,其值是空(Null),不視為錯誤。

· 請求中未包含「EmployeeName」,視為錯誤。

· 請求中不包含「BirthDate」,視為錯誤。

· 請求中包含「Age」,但值是空白,視為錯誤。

由以上測試,得到以下小結:

· [BindRequired] Attribute檢測請求(Request)中是否有包含對應到模型屬性的表單欄位,若是參考型別屬性,表單欄位值可以是空(Null)。 若是實值型別屬性,則請求中必需包含對應的表單欄位,且值不可以為空(Null)。

· [Required] Attribute會檢查屬性值是否為空(Null),並不要求請求(Request)中要包含屬性對應的表單欄位值。

最後要注意,根據微軟文件的說明,[BindRequired] Attribute只適用於表單資料,對XML或JSON資料不起作用。

 

[BindRequired] Attribute自訂錯誤訊息

那麼要怎麼替使用[BindRequired] Attribute驗證的屬性自訂錯誤訊息呢? 這需要修改一下「Startup」類別,參可以下程式碼,在「ConfigureServices」方法中,叫用「AddControllersWithViews」方法設定MVC服務時,傳入「MvcOptions」物件當參數,叫用「SetValueMustNotBeNullAccessor」與「SetMissingBindRequiredValueAccessor」方法來指定自訂錯誤訊息:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace StarterM {
  public class Startup {
    public void ConfigureServices( IServiceCollection services ) {
      //services.AddControllersWithViews();
      services.AddControllersWithViews( options => {
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor( _ => " 不可為空(Null)!!" );
        options.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor( _ => "屬性的值未提供!!" );
      } );
    }

    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      app.UseStaticFiles();
      app.UseRouting();

      app.UseEndpoints( endpoints => {
        endpoints.MapControllerRoute(
            name: "default" ,
            pattern: "{controller=Home}/{action=Index}/{id?}" );
      } );

    }
  }
}

用戶端同樣只送出「EmployeeId」、「Age」兩個表單欄位,顯示的訊息如下圖所示:

clip_image014

圖 6:自訂驗證錯誤訊息。

Tags:

.NET Magazine國際中文電子雜誌 | ASP.NET Core MVC | .NET Core | C# | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List