Поделиться через


Проверка форм ASP.NET Core

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см. статью о версии .NET 9.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске смотрите версию .NET 9 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см. версию .NET 9 этой статьи.

В этой статье объясняется, как использовать проверку в Blazor формах.

Проверка формы

В базовых сценариях проверки формы экземпляр EditForm может использовать объявленные экземпляры EditContext и ValidationMessageStore для проверки полей формы. Обработчик для события OnValidationRequested объекта EditContext выполняет настраиваемую логику проверки. Результат обработчика обновляет экземпляр ValidationMessageStore.

Базовая проверка формы полезна в тех случаях, когда модель формы определена в компоненте, на котором размещена форма, как элементы непосредственно в компоненте или в подклассе. Рекомендуется использование компонента валидатора, если независимый класс модели используется в нескольких компонентах.

Для Blazor Web Appпроверки на стороне клиента требуется активный BlazorSignalR канал. Проверка на стороне клиента недоступна для форм в компонентах, которые используют статический рендеринг на стороне сервера. Формы, использующие статический 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 в коде, вызовите EnableDataAnnotationsValidation с инъектированным IServiceProvider (@inject IServiceProvider ServiceProvider) на EditContext. Для более сложного примера см. компонент NotifyPropertyChangedValidationComponent в платформе ASP.NET Core Blazor фреймворка BasicTestApp (в репозиторииdotnet/aspnetcore на GitHub). В рабочей версии примера замените аргумент new TestServiceProvider() поставщика услуг на внедренный IServiceProvider.

Примечание.

По ссылкам в документации на справочные материалы по .NET обычно загружается ветвь репозитория по умолчанию, которая представляет текущую разработку для следующего выпуска .NET. Чтобы выбрать тег для конкретного выпуска, используйте выпадающий список "Переключение ветвей или тегов." Дополнительные сведения см. в статье Выбор тега версии исходного кода ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Blazor выполняет два типа проверки данных:

  • Проверка поля выполняется, когда пользователь выходит за пределы поля. Во время проверки поля компонент DataAnnotationsValidator связывает все результаты проверки с полем.
  • Проверка модели выполняется, когда пользователь отправляет форму. Во время проверки модели компонент DataAnnotationsValidator пытается определить поле на основе имени члена из результатов проверки. Результаты проверки, не связанные с отдельным элементом, связаны с моделью, а не с полем.

В пользовательских сценариях проверки:

Существует два общих подхода к достижению пользовательской проверки, описанные в следующих двух разделах этой статьи:

  • Ручная проверка с использованием события OnValidationRequested: вручную проверьте поля формы с помощью проверки через аннотации данных и настраиваемый код для проверки полей, когда проверка запрашивается через обработчик событий, назначенный событию OnValidationRequested.
  • компоненты проверяющего элемента: один или несколько настраиваемых компонентов проверяющего элемента можно использовать для обработки проверки разных форм на одной странице или одной и той же форме на различных этапах обработки форм (например, проверка клиента, за которой следует проверка сервера).

Проверка вручную с помощью события OnValidationRequested

Вы можете вручную проверить форму, используя пользовательский обработчик событий, назначенный на событие EditContext.OnValidationRequested для управления ValidationMessageStore.

Платформа Blazor предоставляет компонент DataAnnotationsValidator для присоединения дополнительной поддержки проверки к формам, основанных на атрибутах проверки (аннотации данных).

Вспоминая предыдущий пример компонента Starship8, метод HandleValidationRequested назначается OnValidationRequested, где можно выполнить проверку вручную в коде C#. В нескольких изменениях демонстрируется объединение существующей ручной проверки с проверкой аннотаций данных с помощью DataAnnotationsValidator и атрибута проверки, примененного к модели Holodeck.

Ссылка на пространство имен System.ComponentModel.DataAnnotations в директивах Razor компонента в верхней части файла определения компонента:

@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.
  • Любая проверка вручную, которую требуется выполнить в методе HandleValidationRequested, назначенном событию OnValidationRequested формы, выполняется, когда пользователь выбирает кнопку Update формы. В существующем коде примера компонента Starship8 пользователь должен выбрать один или оба флажка, чтобы проверить форму.
  • Метод Submit не обрабатывается формой, пока не будут пройдены аннотации данных и ручная проверка.

Компоненты валидатора

Компоненты валидации поддерживают проверку форм, управляя ValidationMessageStore для EditContext формы.

Платформа Blazor предоставляет компонент DataAnnotationsValidator для прикрепления к формам поддержки проверки на основе атрибутов проверки (заметок к данным). Вы можете создать настраиваемые компоненты проверяющего средства для обработки сообщений проверки для разных форм на одной странице или одной и той же форме на различных этапах обработки форм (например, проверка клиента, за которой следует проверка сервера). Показанный в этом разделе пример компонента проверяющего элемента управления (CustomValidation) используется в следующих разделах этой статьи.

Из встроенных валидаторов аннотаций данных только атрибут проверки не поддерживается в .

Примечание.

Во многих случаях вместо настраиваемых валидаторов можно использовать настраиваемые атрибуты проверки аннотаций данных. Настраиваемые атрибуты, применяемые к модели формы, активируются с помощью компонента 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.

Примечание.

Анонимные лямбда-выражения — это зарегистрированные обработчики событий для OnValidationRequested и OnFieldChanged в предыдущем примере. В этом сценарии нет необходимости в реализации IDisposable и отключении делегатов событий. Дополнительные сведения см. в разделе ASP.NET Core Razor удаления компонентов.

Проверка бизнес-логики с помощью компонента валидации

Для общей проверки бизнес-логики используйте компонент валидатора, который получает ошибки формы в словаре.

Базовая проверка полезна в тех случаях, когда модель формы определена в компоненте, на котором размещена форма, как элементы непосредственно в компоненте или в подклассе. Рекомендуется использовать компонент валидатора, когда независимый класс модели применяется в нескольких компонентах.

В следующем примере :

  • Используется сокращенная версия Starfleet Starship Database формы (Starship3 компонента) секции Пример формы статьи Входные компоненты, которая принимает только классификацию и описание космического корабля. Валидация аннотации данных не запускается при отправке формы, так как компонент DataAnnotationsValidator не включен в форму.
  • Используется компонент CustomValidation из раздела компоненты валидатора этой статьи.
  • Проверка требует значение для описания корабля (Description), если пользователь выбирает классификацию корабля Defense (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 сценариям, но подход для любого типа приложения, использующего проверку сервера с веб-API, принимает тот же общий подход.

Этот раздел посвящен размещенным Blazor WebAssembly сценариям, но подход для любого типа приложения, использующего проверку сервера с веб-API, принимает тот же общий подход.

Проверка сервера поддерживается в дополнение к проверке клиента:

  • Обрабатывать проверку клиента в форме с компонентом DataAnnotationsValidator.
  • Когда форма проходит клиентскую проверку (OnValidSubmit вызывается), отправьте EditContext.Model на API сервер для обработки формы.
  • Проверка модели процесса на сервере.
  • Серверный API включает как встроенную платформенную проверку заметок к данным, так и настраиваемую логику проверки, предоставленную разработчиком. Если проверка на сервере пройдена, выполните обработку формы и отправьте обратно код состояния успеха (200 - OK). Если проверка завершается неудачно, возвращайте код состояния сбоя (400 - Bad Request) и ошибки проверки полей.
  • Отключите форму в случае успешного выполнения или отобразите ошибки.

Базовая проверка полезна в тех случаях, когда модель формы определена в компоненте, на котором размещена форма, как элементы непосредственно в компоненте или в подклассе. Рекомендуется использовать компонент проверки, если класс модели без зависимости используется в нескольких компонентах.

Основу приведенного ниже примера составляют следующие компоненты.

  • С Blazor Web App компонентами Interactive WebAssembly, созданными на основе Blazor Web App шаблона проекта.
  • Модель Starship (Starship.cs) раздела Пример формы статьи Входные компоненты.
  • Компонент CustomValidation, показанный в разделе Валидатор.

Поместите Starship модель (Starship.cs) в проект библиотеки общих классов, чтобы клиентские и серверные проекты могли использовать модель. Добавьте или обновите пространство имен, чтобы оно соответствовало пространству имен общего приложения (например, namespace BlazorSample.Shared). Так как для модели требуются заметки данных, убедитесь, что библиотека общих классов использует общую платформу или добавляет System.ComponentModel.Annotations пакет в общий проект.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

В главном проекте Blazor Web Appдобавьте контроллер для обработки запросов на проверку звездохода и возврата сообщений о сбое проверки. Обновите пространства имен в последней using инструкции для проекта библиотеки общих классов и namespace класса контроллера. Помимо проверки заметок данных клиента и сервера контроллер проверяет, предоставляется ли значение для описания корабля (Description), если пользователь выбирает Defense классификацию кораблей (Classification).

Поместите модель Starship (Starship.cs) в проект решения Shared, чтобы эту модель могли использовать клиентские и серверные приложения. Добавьте или обновите пространство имен, чтобы оно соответствовало пространству имен общего приложения (например, namespace BlazorSample.Shared). Так как для модели требуются заметки к данным, добавьте пакет System.ComponentModel.Annotations в проект Shared.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

В проекте Server добавьте контроллер для обработки запросов проверки космического корабля и возвращайте сообщения о неудачной проверке. Обновите пространства имен в последней инструкции using для проекта Shared и namespace для класса контроллера. Помимо проверки заметок данных клиента и сервера контроллер проверяет, предоставляется ли значение для описания корабля (Description), если пользователь выбирает Defense классификацию кораблей (Classification).

Проверка Defense классификации корабля выполняется только на сервере в контроллере, так как будущая форма не выполняет такую же проверку на стороне клиента при отправке формы на сервер. Проверка сервера без проверки клиента распространена в приложениях, требующих проверки частной бизнес-логики входных данных пользователя на сервере. Например, для проверки данных, введенных пользователем, может понадобиться конфиденциальная информация из данных, хранящихся для пользователя. Частные данные, очевидно, не могут быть отправлены клиенту для проверки клиента.

Примечание.

Контроллер StarshipValidation в этом разделе использует Microsoft Identity 2.0. Веб-API принимает токены только для пользователей, которые имеют область API.Access для этого API. Дополнительная настройка необходима, если имя области 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 сервера, чтобы он возвращал только ошибки проверки, измените делегат, который вызывается для действий, которые аннотированы в ApiControllerAttributeProgram файле. Для конечной точки 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));
            }
        };
    });

Если вы добавляете контроллеры в основной проект впервые, сопоставляйте конечные точки контроллера при размещении вышеупомянутого кода, который регистрирует службы для контроллеров. В следующем примере используются маршруты контроллера по умолчанию:

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 возвращает сообщения проверки, они добавляются в компонент CustomValidationValidationMessageStore. Эти ошибки можно вывести из EditContext формы с помощью сводки проверки формы.

В следующем компоненте обновите пространство имен проекта Shared до пространства имен общего проекта @using BlazorSample.Shared. Обратите внимание, что форма требует авторизации, поэтому пользователь должен войти в приложение для перехода к форме.

Starship10.razor:

Примечание.

Формы, основанные на , автоматически поддерживают антифальсификацию. Контроллер должен использовать AddControllersWithViews для регистрации сервисов контроллера и автоматического включения поддержки защиты от подделки для веб-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 также должен зарегистрировать обработку HTTP-запросов POST для контроллера веб-API серверной части. Подтвердите или добавьте следующее .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 и лямбда-выражения с именем свойства модели:

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

Компоненты ValidationMessage<TValue> и ValidationSummary поддерживают произвольные атрибуты. В созданный элемент <div> или <ul> добавляется любой атрибут, который не соответствует параметру компонента.

Вы можете управлять стилем сообщений проверки с помощью таблицы стилей приложения (wwwroot/css/app.css или wwwroot/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. В следующем примере показана форма для повара по приготовлению салатов, которая проверяет введенные пользователем данные с использованием внедрения зависимостей (Dependency Injection).

Класс SaladChef указывает утвержденный список ингредиентов звездолета для салата Ten Forward.

SaladChef.cs:

namespace BlazorSample;

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

Зарегистрируйте SaladChef в контейнере DI приложения в файле Program.

builder.Services.AddTransient<SaladChef>();

Метод IsValid следующего класса SaladChefValidatorAttribute получает службу SaladChef из DI для проверки ввода пользователя.

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 полезны при интеграции с платформами CSS, такими как Bootstrap.

Чтобы указать атрибуты 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";
    }
}

Задайте класс CustomFieldClassProvider как поставщик класса CSS для полей в экземпляре формы EditContext с помощью SetFieldCssClassProvider.

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-сущности.

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" />

Обновите экземпляр EditContext в методе OnInitialized компонента, чтобы использовать нового поставщика CSS-классов для полей.

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

Поскольку класс проверки CSS не применяется к полю Description, оно не оформлено. Однако проверка полей выполняется обычным образом. Если будет использоваться более 10 символов, сводка проверки укажет на ошибку:

Description is too long (Слишком длинное описание.)

В следующем примере :

  • К полю Name применяется пользовательский стиль CSS.

  • Любые другие поля применяют логику, похожую на логику Blazor по умолчанию, и используют стили проверки CSS по умолчанию Blazor для полей, modified с valid или invalid. Обратите внимание, что стили по умолчанию не нужно добавлять в таблицу стилей приложения, если приложение основано на шаблоне проекта 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, и стили проверки CSS по умолчанию для полей Blazor.

Проверка уровня класса с помощью 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 проверяет только свойства верхнего уровня модели, привязанной к форме, которые не являются свойствами типа коллекции или сложного типа.

Чтобы проверить весь граф объектов привязанной модели, включая свойства типа коллекции и сложного типа, используйте ObjectGraphDataAnnotationsValidator, предоставляемый экспериментальным пакетом Microsoft.AspNetCore.Components.DataAnnotations.Validation:

<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) заполняется недопустимыми полями после того, как пользователь взаимодействует с одним полем. Решить эту ситуацию можно одним из приведенных ниже способов.

  • Не используйте компонент ValidationSummary в форме.
  • Сделайте компонент ValidationSummary видимым при нажатии кнопки "Отправить" (например, в методе Submit).
<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";
    }
}