共用方式為


ASP.NET Core Blazor 表單驗證

注意

這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前的版本,請參閱 本文的 .NET 9 版本。

本文說明如何在 Blazor 表單中使用驗證。

表單驗證

在基本表單驗證案例中,EditForm 執行個體可以使用宣告的 EditContextValidationMessageStore 執行個體來驗證表單欄位。 EditContextOnValidationRequested 事件處理常式會執行自訂驗證邏輯。 處理常式的結果會更新 ValidationMessageStore 執行個體。

在表單模型定義於裝載表單的元件內,直接做為元件或子類別中成員的情況下,基本表單驗證很有用。 跨數個元件之間使用獨立模型類別時,建議使用驗證程式元件

在 Blazor Web App中,用戶端驗證需要使用中的 BlazorSignalR 線路。 用戶端驗證不適用於已採用靜態伺服器端轉譯 (靜態 SSR) 的元件中的表單。 採用靜態 SSR 的表單在提交表單後會在伺服器上進行驗證。

在下列元件中,HandleValidationRequested 處理常式方法會在驗證表單之前呼叫 ValidationMessageStore.Clear 來清除任何現有的驗證訊息。

Starship8.razor

@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    public Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}

資料註釋驗證程式元件和自訂驗證

DataAnnotationsValidator 元件會將資料註釋驗證附加至串聯的 EditContext。 啟用資料註釋驗證需要 DataAnnotationsValidator 元件。 若要使用與資料註釋不同的驗證系統,請使用自訂實作,而不是 DataAnnotationsValidator 元件。 DataAnnotationsValidator 的架構實作可在參考來源中檢查:

注意

.NET 參考來源的文件連結通常會載入存放庫的預設分支,這表示下一版 .NET 的目前開發。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤

Blazor 會執行兩種類型的驗證:

  • 當使用者使用 Tab 離開欄位時,就會執行欄位驗證。 在欄位驗證期間,DataAnnotationsValidator 元件會將所有報告的驗證結果與欄位產生關聯。
  • 當使用者提交表單時,就會執行模型驗證。 在模型驗證期間,DataAnnotationsValidator 元件會嘗試根據驗證結果所報告的成員名稱來判斷欄位。 未與個別成員相關聯的驗證結果會與模型 (而不是欄位) 建立關聯。

驗證程式元件

驗證程式元件藉由管理表單 EditContextValidationMessageStore 來支援表單驗證。

Blazor 架構會提供 DataAnnotationsValidator 元件,以根據驗證屬性 (資料註釋) 將驗證支援附加至表單。 您可以建立自訂驗證程式元件,以便為相同頁面上的不同表單或是表單處理的不同步驟 (例如,用戶端驗證後面接著伺服器驗證) 的相同表單處理驗證訊息。 本節所示的驗證程式元件範例 CustomValidation,會用於本文的下列各節:

資料註釋內建驗證器中,在 Blazor僅不支援 [Remote] 驗證屬性

注意

在許多情況下,可以使用自訂資料註釋驗證屬性,而不是自訂驗證程式元件。 套用至表單模型的自訂屬性會隨著使用 DataAnnotationsValidator 元件而啟動。 搭配使用伺服器驗證時,套用至模型的任何自訂屬性必須可在伺服器上執行。 如需詳細資訊,請參閱 自訂驗證屬性 章節。

ComponentBase 建立驗證程式元件:

  • 表單的 EditContext 是元件的串聯參數
  • 初始化驗證程式元件時,會建立新的 ValidationMessageStore,以維護目前的表單錯誤清單。
  • 當表單元件中的開發人員程式碼呼叫 DisplayErrors 方法時,訊息存放區會收到錯誤。 錯誤會傳遞至 Dictionary<string, List<string>> 中的 DisplayErrors 方法。 在字典中,索引鍵是有一或多個錯誤的表單欄位名稱。 值是錯誤清單。
  • 發生下列任何一項時,會清除訊息:
    • 引發 OnValidationRequested 事件時,會在 EditContext 上要求驗證。 清除所有錯誤。
    • 引發 OnFieldChanged 事件時,表單中的欄位會變更。 只清除欄位的錯誤。
    • ClearErrors 方法會透過開發人員程式碼呼叫。 清除所有錯誤。

更新下列類別中的命名空間,以符合您應用程式的命名空間。

CustomValidation.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

public class CustomValidation : ComponentBase
{
    private ValidationMessageStore? messageStore;

    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }

    protected override void OnInitialized()
    {
        if (CurrentEditContext is null)
        {
            throw new InvalidOperationException(
                $"{nameof(CustomValidation)} requires a cascading " +
                $"parameter of type {nameof(EditContext)}. " +
                $"For example, you can use {nameof(CustomValidation)} " +
                $"inside an {nameof(EditForm)}.");
        }

        messageStore = new(CurrentEditContext);

        CurrentEditContext.OnValidationRequested += (s, e) => 
            messageStore?.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore?.Clear(e.FieldIdentifier);
    }

    public void DisplayErrors(Dictionary<string, List<string>> errors)
    {
        if (CurrentEditContext is not null)
        {
            foreach (var err in errors)
            {
                messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }
    }

    public void ClearErrors()
    {
        messageStore?.Clear();
        CurrentEditContext?.NotifyValidationStateChanged();
    }
}

重要

ComponentBase 衍生時,必須指定命名空間。 無法指定命名空間會導致建置錯誤:

Tag helpers cannot target tag name '<global namespace>.{CLASS NAME}' because it contains a ' ' character.

{CLASS NAME} 預留位置是元件類別的名稱。 本節中的自訂驗證程式範例會指定範例命名空間 BlazorSample

注意

匿名 Lambda 運算式是在上述範例中註冊 OnValidationRequestedOnFieldChanged 的事件處理常式。 在此案例中,不需要實作 IDisposable 和取消訂閱事件委派。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件生命週期

使用驗證程式元件的商務邏輯驗證

針對一般商務邏輯驗證,請使用驗證程式元件來接收字典中的表單錯誤。

在表單模型定義於裝載表單的元件內,直接做為元件或子類別中成員的情況下,基本驗證很有用。 跨數個元件之間使用獨立模型類別時,建議使用驗證程式元件。

在以下範例中:

  • 使用了輸入元件一文中範例表單小節的 Starfleet Starship Database 表單 (Starship3 元件) 的縮減版本,其僅接受星際飛船的分類和描述。 因為 DataAnnotationsValidator 元件未包含在表單中,因此在表單提交時不會觸發資料註釋驗證。
  • 本文的驗證程式元件小節會使用 CustomValidation 元件。
  • 如果使用者選取 "Defense" 飛船分類 (Classification),驗證需要飛船的描述值 (Description)。

在元件中設定驗證訊息時,其會新增至驗證程式的 ValidationMessageStore,並顯示在 EditForm 的驗證摘要中。

Starship9.razor

@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}
@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}
@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    public Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}

注意

除了使用驗證元件,也可以使用資料註釋驗證屬性。 套用至表單模型的自訂屬性會隨著使用 DataAnnotationsValidator 元件而啟動。 搭配伺服器驗證使用時,屬性必須可在伺服器上執行。 如需詳細資訊,請參閱 自訂驗證屬性 章節。

使用驗證程式元件的伺服器驗證

本節著重於裝載的 Blazor Web App 案例,但使用伺服器驗證搭配 Web API 的任何類型應用程式會採用相同的一般方法。

本節著重於裝載的 Blazor WebAssembly 案例,但使用伺服器驗證搭配 Web API 的任何類型的應用程式會採用相同的一般方法。

除了用戶端驗證之外,還支援伺服器驗證:

  • 使用 DataAnnotationsValidator 元件處理表單中的用戶端驗證。
  • 當表單通過用戶端驗證 (呼叫 OnValidSubmit),將 EditContext.Model 傳送至後端伺服器 API 以進行表單處理。
  • 在伺服器上處理模型驗證。
  • 伺服器 API 包含內建架構資料註釋驗證和開發人員提供的自訂驗證邏輯。 如果驗證在伺服器上通過,則處理表單,並送回成功狀態碼 (200 - OK)。 如果驗證失敗,則傳回失敗狀態碼 (400 - Bad Request) 和欄位驗證錯誤。
  • 在成功時停用表單,或顯示錯誤。

在表單模型定義於裝載表單的元件內,直接做為元件或子類別中成員的情況下,基本驗證很有用。 跨數個元件之間使用獨立模型類別時,建議使用驗證程式元件。

下列範例是基於:

Starship 模型 (Starship.cs) 放入共用類別庫專案中,讓用戶端和伺服器專案都可使用模型。 新增或更新命名空間,以符合共用應用程式的命名空間 (例如 namespace BlazorSample.Shared)。 由於此模型需要資料註釋,請確認共用類別庫使用共用架構,或將 System.ComponentModel.Annotations 套件新增至共用專案。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

在 Blazor Web App的主要專案中,新增控制器來處理星際飛船驗證要求,並傳回失敗驗證訊息。 更新共用類別庫專案的最後一個 using 陳述式中的命名空間,以及控制器類別的 namespace。 除了用戶端和伺服器資料註釋驗證之外,控制器也會驗證如果使用者選取 Defense 飛船分類 (Classification),則會針對飛船的描述 (Description) 提供值。

Starship 模型 (Starship.cs) 放入方案 Shared 的專案,讓用戶端和伺服器應用程式都可以使用模型。 新增或更新命名空間,以符合共用應用程式的命名空間 (例如 namespace BlazorSample.Shared)。 由於模型需要資料註釋,因此請將 System.ComponentModel.Annotations 封裝新增至 Shared 專案。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

Server 專案中,新增控制器來處理星際飛船驗證要求,並傳回失敗的驗證訊息。 更新 Shared 專案最後一個 using 陳述式中的命名空間,以及控制器類別的 namespace。 除了用戶端和伺服器資料註釋驗證之外,控制器也會驗證如果使用者選取 Defense 飛船分類 (Classification),則會針對飛船的描述 (Description) 提供值。

Defense 飛船分類的驗證只會發生在控制器的伺服器上,因為即將推出的表單在表單提交至伺服器時不會執行相同的驗證用戶端。 沒有用戶端驗證的伺服器驗證,在要求在伺服器上進行使用者輸入的私人商務邏輯驗證的應用程式中很常見。 例如,可能需要針對使用者儲存資料的私人資訊,才能驗證使用者輸入。 私人資料顯然無法傳送至用戶端以進行用戶端驗證。

注意

本節中的 StarshipValidation 控制器會使用 Microsoft Identity 2.0。 Web API 只會接受此 API 具有 "API.Access" 範圍使用者的權杖。 如果 API 的範圍名稱與 API.Access 不同,則需要額外的自訂。

如需安全性的詳細資訊,請參閱:

Controllers/StarshipValidation.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class StarshipValidationController(
    ILogger<StarshipValidationController> logger) 
    : ControllerBase
{
    static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

    [HttpPost]
    public async Task<IActionResult> Post(Starship model)
    {
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

        try
        {
            if (model.Classification == "Defense" && 
                string.IsNullOrEmpty(model.Description))
            {
                ModelState.AddModelError(nameof(model.Description),
                    "For a 'Defense' ship " +
                    "classification, 'Description' is required.");
            }
            else
            {
                logger.LogInformation("Processing the form asynchronously");

                // async ...

                return Ok(ModelState);
            }
        }
        catch (Exception ex)
        {
            logger.LogError("Validation Error: {Message}", ex.Message);
        }

        return BadRequest(ModelState);
    }
}

確認或更新上述控制器的命名空間 (BlazorSample.Server.Controllers),以符合應用程式的控制器命名空間。

當伺服器上發生模型繫結驗證錯誤時,ApiController (ApiControllerAttribute) 通常會傳回預設錯誤要求回應ValidationProblemDetails。 當 Starfleet Starship Database 表單的所有欄位未提交且表單驗證失敗時,回應會包含不僅是驗證錯誤的資料,如下列範例所示:

{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Id": ["The Id field is required."],
    "Classification": ["The Classification field is required."],
    "IsValidatedDesign": ["This form disallows unapproved ships."],
    "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
  }
}

注意

若要示範上述 JSON 回應,您必須停用表單的用戶端驗證,以允許空白欄位表單提交,或使用工具將要求直接傳送至伺服器 API,例如 Firefox Browser Developer

如果伺服器 API 傳回上述的預設 JSON 回應,用戶端可以在開發人員程式碼中剖析回應,以取得 errors 節點的子系,以進行表單驗證錯誤處理。 撰寫開發人員程式碼來剖析檔案並不方便。 手動剖析 JSON 需要在呼叫 ReadFromJsonAsync之後產生 Dictionary<string, List<string>> 錯誤。 理想情況下,伺服器 API 應該僅傳回驗證錯誤,如下列範例所示:

{
  "Id": ["The Id field is required."],
  "Classification": ["The Classification field is required."],
  "IsValidatedDesign": ["This form disallows unapproved ships."],
  "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}

若要修改伺服器 API 的回應,使其只傳回驗證錯誤,請變更在 Program 檔案中具有註釋 ApiControllerAttribute 的動作上叫用的委派。 針對 API 端點 (/StarshipValidation),使用 ModelStateDictionary 傳回 BadRequestObjectResult。 針對任何其他 API 端點,使用新的 ValidationProblemDetails 傳回物件結果來保留預設行為。

Microsoft.AspNetCore.Mvc 命名空間新增至 Blazor Web App主要專案中的 Program 檔案頂端:

using Microsoft.AspNetCore.Mvc;

Program 檔案中,新增或更新下列 AddControllersWithViews 擴充方法,並新增下列對 ConfigureApiBehaviorOptions 的呼叫:

builder.Services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

如果您是第一次將控制器新增至 Blazor Web App 的主要專案,當您放置上述用來註冊控制器服務的程式碼時,請對應控制器端點。 下列範例使用預設控制器路由:

app.MapDefaultControllerRoute();

注意

上述範例會呼叫 AddControllersWithViews 以自動減輕跨網站偽造要求 (XSRF/CSRF) 攻擊,藉以明確註冊控制器服務。 如果您僅使用 AddControllers,則不會自動啟用防偽功能。

如需控制器路由和驗證失敗錯誤回應的詳細資訊,請參閱下列資源:

.Client 專案中,新增驗證程式元件小節中顯示的 CustomValidation 元件。 更新命名空間以符合應用程式 (例如,namespace BlazorSample.Client)。

.Client 專案中,Starfleet Starship Database 表單會更新,以在 CustomValidation 元件的協助下顯示伺服器驗證錯誤。 當伺服器 API 傳回驗證訊息時,它們會新增至 CustomValidation 元件的 ValidationMessageStore。 錯誤可在表單的 EditContext 中取得,提供表單的驗證摘要顯示。

在下列元件中,將共用專案的命名空間 (@using BlazorSample.Shared) 更新為共用專案的命名空間。 請注意,表單需要授權,因此使用者必須登入應用程式,才能瀏覽至表單。

Microsoft.AspNetCore.Mvc 命名空間新增至 Server 應用程式中 Program 檔案的頂端:

using Microsoft.AspNetCore.Mvc;

Program 檔案中,找出 AddControllersWithViews 擴充方法,並新增 ConfigureApiBehaviorOptions 的下列呼叫:

builder.Services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

注意

上述範例會呼叫 AddControllersWithViews 以自動減輕跨網站偽造要求 (XSRF/CSRF) 攻擊,藉以明確註冊控制器服務。 如果您僅使用 AddControllers,則不會自動啟用防偽功能。

Client 專案中,新增驗證程式元件小節中顯示的 CustomValidation 元件。 更新命名空間以符合應用程式 (例如,namespace BlazorSample.Client)。

Client 專案中,Starfleet Starship Database 表單會更新,以在 CustomValidation 元件的協助下顯示伺服器驗證錯誤。 當伺服器 API 傳回驗證訊息時,它們會新增至 CustomValidation 元件的 ValidationMessageStore。 錯誤可在表單的 EditContext 中取得,提供表單的驗證摘要顯示。

在下列元件中,將 Shared 專案的命名空間 (@using BlazorSample.Shared) 更新為共用專案的命名空間。 請注意,表單需要授權,因此使用者必須登入應用程式,才能瀏覽至表單。

Starship10.razor

注意

EditForm 為基礎的表單會自動啟用防偽支援。 控制器應使用 AddControllersWithViews 來註冊控制器服務,並自動啟用 Web API 的防偽支援。

@page "/starship-10"
@rendermode InteractiveWebAssembly
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm FormName="Starship10" Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@disabled">Submit</button>
    </div>
    <div style="@messageStyles">
        @message
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;
    private bool disabled;
    private string? message;
    private string messageStyles = "visibility:hidden";

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => 
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private async Task Submit(EditContext editContext)
    {
        customValidation?.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>() ?? 
                new Dictionary<string, List<string>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {Message}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

Blazor Web App 的 .Client 專案也必須向後端 Web API 控制器註冊 HTTP POST 要求的 HttpClient。 確認下列內容或將其新增至 .Client 專案的 Program 檔案:

builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

上述範例會使用 builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress) 設定基底位址 (Base Address),其會取得應用程式的基底位址 (Base Address),而且通常衍生自主頁面中 <base> href 標籤的值。

@page "/starship-10"
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@disabled">Submit</button>
    </div>
    <div style="@messageStyles">
        @message
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;
    private bool disabled;
    private string? message;
    private string messageStyles = "visibility:hidden";

    public Starship? Model { get; set; }

    protected override void OnInitialized() => 
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private async Task Submit(EditContext editContext)
    {
        customValidation?.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>() ?? 
                new Dictionary<string, List<string>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {Message}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

注意

除了使用驗證元件,也可以使用資料註釋驗證屬性。 套用至表單模型的自訂屬性會隨著使用 DataAnnotationsValidator 元件而啟動。 搭配伺服器驗證使用時,屬性必須可在伺服器上執行。 如需詳細資訊,請參閱 自訂驗證屬性 章節。

注意

本節中的伺服器驗證方法適合此文件集中任何裝載的 Blazor WebAssembly 方案範例:

根據輸入事件的 InputText

使用 InputText 元件來建立使用 oninput 事件 (input) 而不是 onchange 事件 (change) 的自訂元件。 對每個按鍵輸入使用 input 事件觸發程序欄位驗證。

下列 CustomInputText 元件會繼承架構的 InputText 元件,並將事件繫結設定為 oninput 事件 (input)。

CustomInputText.razor

@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />

您可以在使用 InputText 的任何位置使用 CustomInputText 元件。 下列元件使用共用的 CustomInputText 元件。

Starship11.razor

@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <CustomInputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}

驗證摘要和驗證訊息元件

ValidationSummary 元件會摘要所有驗證訊息,其類似於驗證摘要標籤協助程式

<ValidationSummary />

使用 Model 參數輸出特定模型的驗證訊息:

<ValidationSummary Model="Model" />

ValidationMessage<TValue> 元件會顯示特定欄位的驗證訊息,其類似於驗證訊息標籤協助程式。 使用 For 屬性指定用於驗證的欄位,以及命名模型屬性的 Lambda 運算式:

<ValidationMessage For="@(() => Model!.MaximumAccommodation)" />

ValidationMessage<TValue>ValidationSummary 元件支援任意屬性。 不符合元件參數的任何屬性都會新增至產生的 <div><ul> 元素。

控制應用程式的樣式表 (wwwroot/css/app.csswwwroot/css/site.css) 中的驗證訊息樣式。 預設 validation-message 類別會將驗證訊息的文字色彩設定為紅色:

.validation-message {
    color: red;
}

判斷表單欄位是否有效

在未取得驗證訊息的情況下,使用 EditContext.IsValid 來判斷欄位是否有效。

支援,但不建議:

var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

建議:

var isValid = editContext.IsValid(fieldIdentifier);

自訂驗證屬性

若要在使用自訂驗證屬性時,確保驗證結果與欄位正確相關聯,請在建立 ValidationResult 時傳遞驗證內容的 MemberName

CustomValidator.cs

using System;
using System.ComponentModel.DataAnnotations;

public class CustomValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
        ValidationContext validationContext)
    {
        ...

        return new ValidationResult("Validation message to user.",
            new[] { validationContext.MemberName });
    }
}

透過 ValidationContext 將服務插入自訂驗證屬性。 下列範例示範會使用相依性插入 (DI) 驗證使用者輸入的沙拉廚師表單。

SaladChef 類別會指出 Ten Forward 沙拉核准的星際飛船食材清單。

SaladChef.cs

namespace BlazorSample;

public class SaladChef
{
    public string[] SaladToppers = { "Horva", "Kanda Root", "Krintar", "Plomeek",
        "Syto Bean" };
}

Program 檔案的應用程式 DI 容器中註冊 SaladChef

builder.Services.AddTransient<SaladChef>();

下列 SaladChefValidatorAttribute 類別的 IsValid 方法會從 DI 取得 SaladChef 服務,以檢查使用者的輸入。

SaladChefValidatorAttribute.cs

using System.ComponentModel.DataAnnotations;

namespace BlazorSample;

public class SaladChefValidatorAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        var saladChef = validationContext.GetRequiredService<SaladChef>();

        if (saladChef.SaladToppers.Contains(value?.ToString()))
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("Is that a Vulcan salad topper?! " +
            "The following toppers are available for a Ten Forward salad: " +
            string.Join(", ", saladChef.SaladToppers));
    }
}

下列元件會藉由將 SaladChefValidatorAttribute ([SaladChefValidator]) 套用至沙拉食材字串 (SaladIngredient) 來驗證使用者輸入。

Starship12.razor

@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off" FormName="Starship12">
    <DataAnnotationsValidator />
    <div>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() =>
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off" FormName="Starship12">
    <DataAnnotationsValidator />
    <div>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() =>
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off">
    <DataAnnotationsValidator />
    <p>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </p>
    <button type="submit">Submit</button>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() => 
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}

自訂驗證 CSS 類別屬性

與 CSS 架構整合時 (例如 Bootstrap),自訂驗證 CSS 類別屬性很有用。

若要指定自訂驗證 CSS 類別屬性,請從提供自訂驗證的 CSS 樣式開始。 在下列範例中,會指定有效 (validField) 和無效 (invalidField) 的樣式。

將下列 CSS 類別新增至應用程式的樣式表:

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

建立衍生自 FieldCssClassProvider 的類別,以檢查欄位驗證訊息,並套用適當的有效或無效樣式。

CustomFieldClassProvider.cs

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = editContext.IsValid(fieldIdentifier);

        return isValid ? "validField" : "invalidField";
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        return isValid ? "validField" : "invalidField";
    }
}

使用 SetFieldCssClassProviderCustomFieldClassProvider 類別設定為表單 EditContext 執行個體上的欄位 CSS 類別提供者。

Starship13.razor

@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship13">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship13">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

@code {
    private EditContext? editContext;

    public Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}

上述範例會檢查所有表單欄位的有效性,並將樣式套用至每個欄位。 如果表單只應該將自訂樣式套用至欄位的子集,請有條件地讓 CustomFieldClassProvider 套用樣式。 下列 CustomFieldClassProvider2 範例只會將樣式套用至 Name 欄位。 針對名稱不符合 Name 的任何欄位,會傳回 string.Empty,而且不會套用任何樣式。 使用反映,欄位會與模型成員的屬性或欄位名稱比對,而不是指派給 HTML 實體的 id

CustomFieldClassProvider2.cs

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = editContext.IsValid(fieldIdentifier);

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}

注意

比對上述範例中的欄位名稱會區分大小寫,因此指定 "Name" 的模型屬性成員必須符合 "Name" 的條件式檢查:

  • 正確比對: fieldId.FieldName == "Name"
  • 無法比對: fieldId.FieldName == "name"
  • 無法比對: fieldId.FieldName == "NAME"
  • 無法比對: fieldId.FieldName == "nAmE"

將其他屬性新增至 Model,例如:

[StringLength(10, ErrorMessage = "Description is too long.")]
public string? Description { get; set; } 

Description 新增至 CustomValidationForm 元件的表單:

<InputText @bind-Value="Model!.Description" />

更新元件的 OnInitialized 方法中的 EditContext 執行個體,以使用新的欄位 CSS 類別提供者:

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

因為 CSS 驗證類別未套用至 Description 欄位,不會設定樣式。 不過,欄位驗證會正常執行。 如果提供超過 10 個字元,驗證摘要會指出錯誤:

描述太長。

在以下範例中:

  • 自訂 CSS 樣式會套用至 Name 欄位。

  • 任何其他欄位會套用類似 Blazor 預設邏輯的邏輯,並使用 Blazor 的預設欄位 CSS 驗證樣式 modified 搭配 validinvalid。 請注意,針對預設樣式,如果應用程式是以 Blazor 專案範本為基礎,則不需要將它們新增至應用程式的樣式表。 針對不是以 Blazor 專案範本為基礎的應用程式,可以將預設樣式新增至應用程式的樣式表:

    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }
    
    .invalid {
        outline: 1px solid red;
    }
    

CustomFieldClassProvider3.cs

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = editContext.IsValid(fieldIdentifier);

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}

更新元件的 OnInitialized 方法中的 EditContext 執行個體,以使用以上的欄位 CSS 類別提供者:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

使用 CustomFieldClassProvider3

  • Name 欄位會使用應用程式的自訂驗證 CSS 樣式。
  • Description 欄位使用的邏輯類似 Blazor 的邏輯和 Blazor 的預設欄位 CSS 驗證樣式。

使用 IValidatableObject類別層級驗證

Blazor 表單模型支援使用 IValidatableObject 類別層級驗證 (API 文件)。 IValidatableObject 只有在提交表單,且在所有其他驗證都成功時,才會執行驗證。

Blazor 資料註釋驗證封裝

Microsoft.AspNetCore.Components.DataAnnotations.Validation 是使用 DataAnnotationsValidator 元件填滿驗證體驗落差的封裝。 封裝目前是實驗性

警告

Microsoft.AspNetCore.Components.DataAnnotations.Validation 封裝具有最新版本的候選版,位於 NuGet.org。目前繼續使用實驗性候選版封裝。 實驗性功能僅供您探索功能的可行性,穩定版內不一定會有此功能。 觀看公告 GitHub 存放庫dotnet/aspnetcore GitHub 存放庫或本主題章節,以取得進一步的更新。

[CompareProperty] 屬性

CompareAttribute 無法與 DataAnnotationsValidator 元件搭配運作,因為 DataAnnotationsValidator 不會將驗證結果與特定成員產生關聯。 這可能會導致欄位層級驗證與提交時驗證整個模型時的行為不一致。 Microsoft.AspNetCore.Components.DataAnnotations.Validation 實驗性封裝引進了額外的驗證屬性 ComparePropertyAttribute,可因應這些限制。 在 Blazor 應用程式中,[CompareProperty][Compare] 屬性的直接取代項目。

巢狀模型、集合型別和複雜型別

Blazor 支援使用資料註釋搭配內建的 DataAnnotationsValidator 來驗證表單輸入。 不過,DataAnnotationsValidator 只會驗證繫結至不是集合或複雜型別屬性表單模型的最上層屬性。

若要驗證繫結模型的整個物件圖形,包括集合和複雜型別屬性,請使用實驗性 Microsoft.AspNetCore.Components.DataAnnotations.Validation封裝所提供的 ObjectGraphDataAnnotationsValidator

<EditForm ...>
    <ObjectGraphDataAnnotationsValidator />
    ...
</EditForm>

使用 [ValidateComplexType] 標註模型屬性。 在下列模型類別中,ShipDescription 類別包含模型繫結至表單時要驗證的其他資料註釋:

Starship.cs

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    ...

    [ValidateComplexType]
    public ShipDescription ShipDescription { get; set; } = new();

    ...
}

ShipDescription.cs

using System;
using System.ComponentModel.DataAnnotations;

public class ShipDescription
{
    [Required]
    [StringLength(40, ErrorMessage = "Description too long (40 char).")]
    public string? ShortDescription { get; set; }

    [Required]
    [StringLength(240, ErrorMessage = "Description too long (240 char).")]
    public string? LongDescription { get; set; }
}

根據表單驗證啟用提交按鈕

若要根據表單驗證啟用和停用提交按鈕,下列範例:

  • 使用輸入元件一文中範例表單小節的舊版 Starfleet Starship Database 表單 (Starship3 元件) 的縮減版本,其僅接受飛船識別碼的值。建立 Starship 型別的執行個體時,其他 Starship 屬性會收到有效的預設值。
  • 使用表單的 EditContext,在元件初始化時指派模型。
  • 驗證內容 OnFieldChanged 回撥中的表單,以啟用和停用提交按鈕。
  • 實作 IDisposable 並取消訂閱 Dispose 方法中的事件處理常式。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件生命週期

注意

指派給 EditForm.EditContext 時,請勿同時將 EditForm.Model 指派給 EditForm

Starship14.razor

@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
            {
                Id = "NCC-1701",
                Classification = "Exploration",
                MaximumAccommodation = 150,
                IsValidatedDesign = true,
                ProductionDate = new DateTime(2245, 4, 11)
            };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}

如果表單未預先載入有效值,而且您想要停用表單載入上的 Submit 按鈕,請將 formInvalid 設定為 true

上述方法的副作用是驗證摘要 (ValidationSummary 元件) 會在使用者與任何一個欄位互動之後填入無效的欄位。 以下列其中一種方式解決此案例:

<EditForm ... EditContext="editContext" OnValidSubmit="Submit" ...>
    <DataAnnotationsValidator />
    <ValidationSummary style="@displaySummary" />

    ...

    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    private string displaySummary = "display:none";

    ...

    private void Submit()
    {
        displaySummary = "display:block";
    }
}