共用方式為


ASP.NET Core Blazor 表單驗證

注意

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

警告

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

重要

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

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

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

表單驗證

在基本表單驗證案例中,EditForm 執行個體可以使用宣告的 EditContextValidationMessageStore 執行個體來驗證表單欄位。 OnValidationRequestedEditContext 事件處理常式會執行自訂驗證邏輯。 處理常式的結果會更新 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 的架構實作可在參考來源中檢查:

如果您需要在程式代碼中啟用 EditContext 的數據批註驗證支援,請在 EditContext上使用插入 IServiceProvider@inject IServiceProvider ServiceProvider) 呼叫 EnableDataAnnotationsValidation。 如需進階範例,請參閱 ASP.NET Core Blazor 架構 BasicTestAppdotnet/aspnetcore GitHub 存放庫) 中的NotifyPropertyChangedValidationComponent 元件。 在範例的生產版本中,將服務提供者的 new TestServiceProvider() 自變數取代為插入的 IServiceProvider

注意

.NET 參考文件的連結通常會載入儲存庫的預設分支,此分支代表 .NET 的下一個版本正在進行的開發工作。 若要為特定版本選擇標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤

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

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

在自訂驗證案例中:

有兩種達成自定義驗證的一般方法,本文接下來的兩節會說明:

  • 使用 OnValidationRequested 事件手動驗證:當驗證要求來自指派給 OnValidationRequested 事件的事件處理程式時,手動使用資料註解和自訂程式碼來驗證表單的欄位。
  • 驗證程式元件:一或多個自定義驗證程式元件可用來處理相同頁面上不同表單的驗證,或在表單處理的不同步驟中處理相同表單(例如,用戶端驗證後面接著伺服器驗證)。

使用 OnValidationRequested 事件手動驗證

您可以使用指定給 EditContext.OnValidationRequested 事件的自訂事件處理程式手動驗證表單,以管理 ValidationMessageStore

Blazor 架構提供 DataAnnotationsValidator 元件,根據 驗證屬性(數據批注),將額外的驗證支援附加至表單。

回想先前的 Starship8 元件範例,HandleValidationRequested 方法會指派給 OnValidationRequested,您可以在其中在 C# 程式代碼中執行手動驗證。 一些變更展示了如何結合現有的手動驗證與透過 DataAnnotationsValidator 的數據批註驗證,以及將驗證屬性套用至 Holodeck 模型。

參考元件定義檔頂端元件 Razor 指示詞中的 System.ComponentModel.DataAnnotations 命名空間:

@using System.ComponentModel.DataAnnotations

使用驗證屬性將 Id 屬性新增至 Holodeck 模型,以將字元串的長度限制為六個字元:

[StringLength(6)]
public string? Id { get; set; }

DataAnnotationsValidator 元件 (<DataAnnotationsValidator />) 新增至表單。 一般而言,元件會放在 <EditForm> 標籤底下,但您可以將它放在表單中的任何位置:

<DataAnnotationsValidator />

<EditForm> 標籤的表單提交行為從 OnSubmit 更改為 OnValidSubmit,這可確保在執行指派的事件處理方法之前檢查表單是否有效。

- OnSubmit="Submit"
+ OnValidSubmit="Submit"

<EditForm>中,新增 Id 屬性的欄位:

<div>
    <label>
        <InputText @bind-Value="Model!.Id" />
        ID (6 characters max)
    </label>
    <ValidationMessage For="() => Model!.Id" />
</div>

進行上述變更之後,表單的行為會符合下列規格:

  • Id 欄位只會失去焦點時,Id 屬性上的數據批註驗證不會觸發驗證失敗。 當使用者選取 [Update] 按鈕時,就會執行驗證。
  • 當使用者選取表單的 [Update] 按鈕時,任何您要在分配給表單 [OnValidationRequested] 事件的 HandleValidationRequested 方法中執行的手動驗證將被執行。 在 Starship8 元件範例的現有程式代碼中,用戶必須選取或兩個複選框來驗證表單。
  • 在數據批注和手動驗證通過之前,表單不會處理 Submit 方法。

驗證元件

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

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

資料註釋內建驗證器中,只有 Blazor 不受支援。

注意

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

ComponentBase 建立驗證程式元件:

  • 表單的 EditContext 是元件的串聯參數
  • 初始化驗證程式元件時,會建立新的 ValidationMessageStore,以維護目前的表單錯誤清單。
  • 當表單元件中的開發人員程式碼呼叫 DisplayErrors 方法時,訊息存放區會收到錯誤。 錯誤會傳遞至 DisplayErrors 中的 Dictionary<string, List<string>> 方法。 在字典中,鍵是表示一個或多個錯誤的表單欄位名稱。 值是錯誤清單。
  • 發生下列任何一項時,會清除訊息:
    • 引發 EditContext 事件時,會在 OnValidationRequested 上要求驗證。 清除所有錯誤。
    • 引發 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" 飛船分類 (Description),則驗證需要提供飛船的描述值 (Classification)。

在元件中設定驗證訊息時,其會新增至驗證程式的 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),
                [ "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),
                [ "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) 和欄位驗證錯誤。
  • 在成功時停用表單,或顯示錯誤。

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

下列範例是基於:

  • Blazor Web App 包含了從 Blazor Web App 專案範本建立的互動式 WebAssembly 元件。
  • Starship一文中Starship.cs小節的 模型 ()。
  • CustomValidation小節中顯示的 元件。

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

備註

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

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

  • 從 Blazor WebAssembly 建立的託管解決方案託管Blazor方案中所述的任何安全性托管Blazor WebAssembly都支援該方法。
  • Starship一文中Starship.cs小節的 模型 ()。
  • CustomValidation小節中顯示的 元件。

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

注意

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

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

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 = [ "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);
    }
}
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 需要在呼叫 Dictionary<string, List<string>> 之後產生一個錯誤列表 ReadFromJsonAsync。 理想情況下,伺服器 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 的回應,使其只傳回驗證錯誤,請變更在 ApiControllerAttribute 檔案中具有註釋 Program 的動作上叫用的委派。 針對 API 端點 (/StarshipValidation),返回一個 BadRequestObjectResult 並附帶 ModelStateDictionary。 針對任何其他 API 端點,傳回包含新的 ValidationProblemDetails 的物件結果,以保留預設行為。

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

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 命名空間新增至 Program 應用程式中 Server 檔案的頂端:

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.";
        }
    }
}

.Client 的 Blazor Web App 專案也必須向後端 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>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" />

您可以在使用 CustomInputText 的任何位置使用 InputText 元件。 下列元件使用共用的 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);

自訂驗證屬性

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

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.",
            [ validationContext.MemberName! ]);
    }
}
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! });
    }
}
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" };
}

在應用程式的 DI 容器中註冊 SaladChef,位於 Program 檔案中:

builder.Services.AddTransient<SaladChef>();

下列 IsValid 類別的 SaladChefValidatorAttribute 方法會從 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";
    }
}

使用 CustomFieldClassProviderEditContext 類別設定為表單 SetFieldCssClassProvider 執行個體上的欄位 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";
            }
        }
    }
}

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

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

使用 CustomFieldClassProvider3

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

IValidatableObject 中進行類別級別驗證

使用 IValidatableObject 進行類別層級驗證(參見 API 文件)為 Blazor 表單模型提供支援。 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";
    }
}