From 52cd9086d6c331018fc45e96b2b788f63c467e09 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:18:19 -0400 Subject: [PATCH] Blazor forms and antiforgery 8.0 (#30035) --- aspnetcore/blazor/call-web-api.md | 25 + .../blazor/components/built-in-components.md | 1 + .../blazor/forms-and-input-components.md | 3034 ++++++++++++----- aspnetcore/blazor/security/index.md | 14 + .../security/server/additional-scenarios.md | 5 + aspnetcore/blazor/security/server/index.md | 2 +- aspnetcore/security/anti-request-forgery.md | 281 +- cspell.json | 8 +- 8 files changed, 2465 insertions(+), 905 deletions(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index c77f1adf6450..c1c0660e5beb 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -840,6 +840,31 @@ For more information, see . :::zone-end +:::moniker range=">= aspnetcore-8.0" + +## Antiforgery support + +To add antiforgery support to an HTTP request, inject the `AntiforgeryStateProvider` and add a `RequestToken` to the headers collection as a `RequestVerificationToken`: + +```razor +@inject AntiforgeryStateProvider Antiforgery +``` + +```csharp +private async Task OnSubmit() +{ + var antiforgery = Antiforgery.GetAntiforgeryToken(); + var request = new HttpRequestMessage(HttpMethod.Post, "action"); + request.Headers.Add("RequestVerificationToken", antiforgery.RequestToken); + var response = await client.SendAsync(request); + ... +} +``` + +For more information, see . + +:::moniker-end + ## Blazor framework component examples for testing web API access Various network tools are publicly available for testing web API backend apps directly, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/) and [Postman](https://www.postman.com). Blazor framework's reference source includes test assets that are useful for testing: diff --git a/aspnetcore/blazor/components/built-in-components.md b/aspnetcore/blazor/components/built-in-components.md index 6fa6d9d26858..a87dabc3a62b 100644 --- a/aspnetcore/blazor/components/built-in-components.md +++ b/aspnetcore/blazor/components/built-in-components.md @@ -19,6 +19,7 @@ The following built-in Razor components are provided by the Blazor framework: * [`App`](xref:blazor/project-structure) +* [`AntiforgeryToken`](xref:blazor/forms-and-input-components#antiforgery-support) * [`Authentication`](xref:blazor/security/webassembly/index#authentication-component) * [`AuthorizeView`](xref:blazor/security/index#authorizeview-component) * [`CascadingValue`](xref:blazor/components/cascading-values-and-parameters#cascadingvalue-component) diff --git a/aspnetcore/blazor/forms-and-input-components.md b/aspnetcore/blazor/forms-and-input-components.md index 1aa059ea1186..104a4eeff4ee 100644 --- a/aspnetcore/blazor/forms-and-input-components.md +++ b/aspnetcore/blazor/forms-and-input-components.md @@ -14,217 +14,635 @@ uid: blazor/forms-and-input-components The Blazor framework supports forms and provides built-in input components: -* component bound to a model that uses [data annotations](xref:mvc/models/validation) +:::moniker range=">= aspnetcore-8.0" + +* Bound to an object or model that can use [data annotations](xref:mvc/models/validation) + * An component + * HTML forms with the `
` element * [Built-in input components](#built-in-input-components) +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + +* An component bound to an object or model that can use [data annotations](xref:mvc/models/validation) +* [Built-in input components](#built-in-input-components) + +:::moniker-end + The namespace provides: - * Classes for managing form elements, state, and validation. - * Access to built-in :::no-loc text="Input*"::: components, which can be used in Blazor apps. - -A project created from the Blazor project template includes the namespace by default in the app's `_Imports.razor` file, which makes the namespace available in all of the Razor component files (`.razor`) of the app without explicit [`@using`](xref:mvc/views/razor#using) directives: +* Classes for managing form elements, state, and validation. +* Access to built-in :::no-loc text="Input*"::: components. -```razor -@using Microsoft.AspNetCore.Components.Forms -``` +A project created from the Blazor project template includes the namespace by default in the app's `_Imports.razor` file, which makes the namespace available to the app's Razor components. -:::moniker range=">= aspnetcore-7.0" +## Examples in this article + +:::moniker range=">= aspnetcore-8.0" + +Components are configured for server-side rendering (SSR) and server interactivity. For a client-side experience in a Blazor Web App, change the render mode in the `@attribute` directive at the top of the component to either: -To demonstrate how an component works, consider the following example. `ExampleModel` represents the data model bound to the form and defines a `Name` property, which is used to store the value of the form's `name` field provided by the user. +* `RenderModeWebAssembly` for client-side rendering (CSR) only. +* `RenderModeAuto` for CSR/client-side interactivity after SSR/server interactivity and the WebAssembly-based runtime starts. -`ExampleModel.cs`: +If working with a Blazor WebAssembly app, take ***either*** of the following approaches: + +* Change the render mode to CSR (`RenderModeWebAssembly`): + + ```diff + - @attribute [RenderModeServer] + + @attribute [RenderModeWebAssembly] + ``` + +* Remove the `@attribute` from the component. + +When using CSR, keep in mind that all of the component code is compiled and sent to the client, where users can decompile and inspect it. Don't place private code, app secrets, or other sensitive information in CSR components. + +:::moniker-end + +:::moniker range="< aspnetcore-5.0" + +Examples use the [target-typed `new` operator](/dotnet/csharp/language-reference/operators/new-operator#target-typed-new), which was introduced with C# 9.0 and .NET 5. In the following example, the type isn't explicitly stated for the `new` operator: ```csharp -public class ExampleModel -{ - public string? Name { get; set; } -} +public ShipDescription ShipDescription { get; set; } = new(); +``` + +If using C# 8.0 or earlier (.NET 3.1), modify the example code to state the type to the `new` operator: + +```csharp +public ShipDescription ShipDescription { get; set; } = new ShipDescription(); ``` :::moniker-end -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +:::moniker range="< aspnetcore-6.0" + +Components use nullable reference types (NRTs), and the .NET compiler performs null-state static analysis, both of which are supported in .NET 6 or later. For more information, see . -To demonstrate how an component works with [data annotations](xref:mvc/models/validation) validation, consider the following `ExampleModel` type. The `Name` property is marked required with the and specifies a maximum string length limit and error message. +If using C# 9.0 or earlier (.NET 5 or earlier), remove the NRTs from the examples. Usually, this merely involves removing the question marks (`?`) and exclamation points (`!`) from the types in the example code. -`ExampleModel.cs`: +The .NET SDK applies implicit global `using` directives to projects when targeting .NET 6 or later. The examples use a logger to log information about form processing, but it isn't necessary to specify an `@using` directive for the namespace in the component examples. For more information, see [.NET project SDKs: Implicit using directives](/dotnet/core/project-sdk/overview#implicit-using-directives). -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/ExampleModel.cs" highlight="5-6"::: +If using C# 9.0 or earlier (.NET 5 or earlier), add `@using` directives to the top of the component after the `@page` directive for any API required by the example. Find API namespaces through Visual Studio (right-click the object and select **Peek Definition**) or the [.NET API browser](/dotnet/api/). :::moniker-end -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" +To demonstrate how forms work with [data annotations](xref:mvc/models/validation) validation, example components rely on API. To avoid an extra line of code in each example to use the namespace, make the namespace available throughout the app's components with the imports file. Add the following line to the project's `_Imports.razor` file: -To demonstrate how an component works with [data annotations](xref:mvc/models/validation) validation, consider the following `ExampleModel` type. The `Name` property is marked required with the and specifies a maximum string length limit and error message. +```razor +@using System.ComponentModel.DataAnnotations +``` -`ExampleModel.cs`: +Form examples reference aspects of the [Star Trek](http://www.startrek.com/) universe. Star Trek is a copyright ©1966-2023 of [CBS Studios](https://www.paramount.com/brand/cbs-studios) and [Paramount](https://www.paramount.com). -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/ExampleModel.cs" highlight="5-6"::: + -To demonstrate how an component works with [data annotations](xref:mvc/models/validation) validation, consider the following `ExampleModel` type. The `Name` property is marked required with the and specifies a maximum string length limit and error message. +## Additional form examples -`ExampleModel.cs`: +The following additional form examples are available for inspection in the [ASP.NET Core GitHub repository (`dotnet/aspnetcore`) forms test assets](https://github.com/dotnet/aspnetcore/tree/main/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms). -:::code language="csharp" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/ExampleModel.cs" highlight="5-6"::: + -`Pages/FormExample1.razor`: +[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] -:::moniker range=">= aspnetcore-7.0" +## Introduction -```razor -@page "/form-example-1" -@using Microsoft.Extensions.Logging -@inject ILogger Logger +A form is defined using the Blazor framework's component. The following Razor component demonstrates typical elements, components, and Razor code to render a webform using an component. + +:::moniker range=">= aspnetcore-8.0" + +> [!NOTE] +> Most of this article's examples use Blazor's component to create a form. Alternatively, forms can be bound with an HTML `` element. For more information, see the [HTML forms](#html-forms) section. + +:::moniker-end - - +`Starship1.razor`: + +:::moniker range=">= aspnetcore-8.0" + +```razor +@page "/starship-1" +@attribute [RenderModeServer] +@inject ILogger Logger + + @code { - private ExampleModel exampleModel = new(); + [SupplyParameterFromForm] + public Starship? Model { get; set; } - private void HandleSubmit() + protected override void OnInitialized() => Model ??= new(); + + private void Submit() { - Logger.LogInformation("HandleSubmit called"); + Logger.LogInformation("Id = {Id}", Model?.Id); + } - // Process the form + public class Starship + { + public string? Id { get; set; } } } ``` -In the preceding `FormExample1` component: +In the preceding `Starship1` component: * The component is rendered where the `` element appears. -* The model is created in the component's `@code` block and held in a private field (`exampleModel`). The field is assigned to 's attribute (`Model`) of the `` element. -* The component is an input component for editing string values. The `@bind-Value` directive attribute binds the `exampleModel.Name` model property to the component's property. -* The `HandleSubmit` method is registered as a handler for the callback. The handler is called when the form is submitted by the user. +* The model is created in the component's `@code` block and held in a public property (`Model`). The property is assigned to is assigned to the parameter. The `[SupplyParameterFromForm]` attribute indicates that the value of the associated property should be supplied from the form data for the form. Data in the request that matches the name of the property is bound to the property. +* The component is an input component for editing string values. The `@bind-Value` directive attribute binds the `Model.Id` model property to the component's property. +* The `Submit` method is registered as a handler for the callback. The handler is called when the form is submitted by the user. + +Blazor enhances page navigation and form handling by intercepting the request in order to apply the response to the existing DOM, preserving as much of the rendered form as possible. The enhancement avoids the need to fully load the page and provides a much smoother user experience, similar to a single-page app (SPA), although the component is rendered on the server. + +[Streaming rendering](xref:blazor/components/rendering#streaming-rendering) is supported with forms. For an example, see the [`StreamingRenderingForm` test asset (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/StreamingRenderingForm.razor). + +[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] + +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-1" +@inject ILogger Logger + + + + + + +@code { + public Starship? Model { get; set; } + + protected override void OnInitialized() => Model ??= new(); + + private void Submit() + { + Logger.LogInformation("Model.Id = {Id}", Model?.Id); + } + + public class Starship + { + public string? Id { get; set; } + } +} +``` -To demonstrate how the preceding component works with [data annotations](xref:mvc/models/validation) validation: +In the preceding `Starship1` component: -* The preceding `ExampleModel` uses the namespace. -* The `Name` property of `ExampleModel` is marked required with the and specifies a maximum string length limit and error message. +* The component is rendered where the `` element appears. +* The model is created in the component's `@code` block and held in a private field (`model`). The field is assigned to the parameter. +* The component is an input component for editing string values. The `@bind-Value` directive attribute binds the `Model.Id` model property to the component's property†. +* The `Submit` method is registered as a handler for the callback. The handler is called when the form is submitted by the user. -`ExampleModel.cs`: +:::moniker-end -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/ExampleModel.cs" highlight="5-6"::: +†For more information on property binding, see . -The earlier `FormExample1` component is modified: +In the next example, the preceding component is modified to create the form in the `Starship2` component: -* is replaced with , which processes assigned event handler if the form is valid when submitted by the user. The method name is changed to `HandleValidSubmit`, which reflects that the method is called when the form is valid. +* is replaced with , which processes assigned event handler if the form is valid when submitted by the user. * A component is added to display validation messages when the form is invalid on form submission. * The data annotations validator ( component†) attaches validation support using data annotations: - * If the `` form field is left blank when the **`Submit`** button is selected, an error appears in the validation summary ( component‡) ("`The Name field is required.`") and `HandleValidSubmit` is **not** called. - * If the `` form field contains more than ten characters when the **`Submit`** button is selected, an error appears in the validation summary ("`Name is too long.`") and `HandleValidSubmit` is **not** called. - * If the `` form field contains a valid value when the **`Submit`** button is selected, `HandleValidSubmit` is called. + * If the `` form field is left blank when the **`Submit`** button is selected, an error appears in the validation summary ( component‡) ("`The Id field is required.`") and `Submit` is **not** called. + * If the `` form field contains more than ten characters when the **`Submit`** button is selected, an error appears in the validation summary ("`Id is too long.`"). `Submit` is **not** called. + * If the `` form field contains a valid value when the **`Submit`** button is selected, `Submit` is called. -†The component is covered in the [Validator component](#validator-components) section. ‡The component is covered in the [Validation Summary and Validation Message components](#validation-summary-and-validation-message-components) section. For more information on property binding, see . +†The component is covered in the [Validator component](#validator-components) section. ‡The component is covered in the [Validation Summary and Validation Message components](#validation-summary-and-validation-message-components) section. -`Pages/FormExample1.razor`: +`Starship2.razor`: -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample1.razor"::: +:::moniker range=">= aspnetcore-8.0" -:::moniker-end +```razor +@page "/starship-2" +@attribute [RenderModeServer] +@inject ILogger Logger -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" + + + + + + -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample1.razor"::: +@code { + [SupplyParameterFromForm] + public Starship? Model { get; set; } -:::moniker-end + protected override void OnInitialized() => Model ??= new(); -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" + private void Submit() + { + Logger.LogInformation("Id = {Id}", Model?.Id); + } -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample1.razor"::: + public class Starship + { + [Required] + [StringLength(10, ErrorMessage = "Id is too long.")] + public string? Id { get; set; } + } +} +``` :::moniker-end -:::moniker range="< aspnetcore-5.0" +:::moniker range="< aspnetcore-8.0" -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample1.razor"::: +```razor +@page "/starship-2" +@inject ILogger Logger -:::moniker-end + + + + + + -:::moniker range="< aspnetcore-7.0" +@code { + public Starship? Model { get; set; } -In the preceding `FormExample1` component: + protected override void OnInitialized() => Model ??= new(); -* The component is rendered where the `` element appears. -* The model is created in the component's `@code` block and held in a private field (`exampleModel`). The field is assigned to 's attribute (`Model`) of the `` element. -* The component is an input component for editing string values. The `@bind-Value` directive attribute binds the `exampleModel.Name` model property to the component's property. -* The `HandleValidSubmit` method is assigned to . The handler is called if the form passes validation. -* The data annotations validator ( component†) attaches validation support using data annotations: - * If the `` form field is left blank when the **`Submit`** button is selected, an error appears in the validation summary ( component‡) ("`The Name field is required.`") and `HandleValidSubmit` is **not** called. - * If the `` form field contains more than ten characters when the **`Submit`** button is selected, an error appears in the validation summary ("`Name is too long.`") and `HandleValidSubmit` is **not** called. - * If the `` form field contains a valid value when the **`Submit`** button is selected, `HandleValidSubmit` is called. + private void Submit() + { + Logger.LogInformation("Id = {Id}", Model?.Id); + } -†The component is covered in the [Validator component](#validator-components) section. ‡The component is covered in the [Validation Summary and Validation Message components](#validation-summary-and-validation-message-components) section. For more information on property binding, see . + public class Starship + { + [Required] + [StringLength(10, ErrorMessage = "Id is too long.")] + public string? Id { get; set; } + } +} +``` :::moniker-end -## Binding a form +## Binding + +An creates an based on the assigned object as a [cascading value](xref:blazor/components/cascading-values-and-parameters) for other components in the form. The tracks metadata about the edit process, including which form fields have been modified and the current validation messages. Assigning to either an or an can bind a form to data. -An creates an based on the assigned model instance as a [cascading value](xref:blazor/components/cascading-values-and-parameters) for other components in the form. The tracks metadata about the edit process, including which fields have been modified and the current validation messages. Assigning to either an or an can bind a form to data. +### Model binding Assignment to : +:::moniker range=">= aspnetcore-8.0" + ```razor - + + ... + @code { - private ExampleModel exampleModel = new() { ... }; + [SupplyParameterFromForm] + public Starship? Model { get; set; } + + protected override void OnInitialized() => Model ??= new(); } ``` +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + +```razor + + ... + + +@code { + public Starship? Model { get; set; } = new(); +} +``` + +> [!NOTE] +> Most of this article's form model examples bind forms to C# *properties*, but C# field binding is also supported. + +:::moniker-end + +### Context binding + Assignment to : -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-8.0" ```razor - + + ... + @code { - private ExampleModel exampleModel = new() { ... }; + [SupplyParameterFromForm] + public Starship? Model { get; set; } + private EditContext? editContext; protected override void OnInitialized() { - editContext = new(exampleModel); + Model ??= new(); + editContext = new(Model); } } ``` :::moniker-end -:::moniker range="< aspnetcore-6.0" +:::moniker range="< aspnetcore-8.0" ```razor + ... + + +@code { + private Starship model = new(); + private EditContext? editContext; + + protected override void OnInitialized() => editContext = new(model); +} +``` + +:::moniker-end + +Assign **either** an **or** a to an . If both are assigned, a runtime error is thrown. + +:::moniker range=">= aspnetcore-8.0" + +### Supported types + +Binding supports: + +* Primitive types +* Collections +* Complex types +* Recursive types +* Types with constructors +* Enums + +You can also use the [`[DataMember]`](xref:System.Runtime.Serialization.DataMemberAttribute) and [`[IgnoreDataMember]`](xref:System.Runtime.Serialization.IgnoreDataMemberAttribute) attributes to customize model binding. Use these attributes to rename properties, ignore properties, and mark properties as required. + +### Additional binding options + +Additional model binding options are available from `RazorComponentOptions` when calling . + +### Form names + +Form names must be unique to bind model data. When a form name is provided, use the `FormName` parameter. The following form is named `RomulanAle`: + +```razor + + ... + +``` + +Supplying a form name isn't required if only one form is present in a component. Form names also aren't required when using multiple child components with forms, as long as each child only has a single form. If a child component has multiple forms, the forms are named in the child component. + +`MultipleForms.razor`: + +```razor +@page "/multiple-forms" + +

Combine Forms

+ +
+ Form names NOT required for Starship1 + and Starship2 form components. +
+ + + + +``` + +Define a scope for form names using the `FormMappingScope` component, which is useful for preventing form name collisions when a library supplies a form to a component and you have no way to control the form name used by the library's developer. In the following example, the `FormMappingScope` scope name is `ParentContext`. The `Hello`-named form doesn't collide with a form in the app using the same form name. + +`HelloFormFromLibrary.razor`: + +```razor + + + + + +@if (submitted) +{ +

Hello @Name from the library form!

+} @code { - private ExampleModel exampleModel = new() { ... }; - private EditContext editContext; + bool submitted = false; + + [SupplyParameterFromForm(Handler = "Hello")] + public string? Name { get; set; } + + private void Submit() => submitted = true; +} +``` + +`NamedFormsWithScope.razor`: + +```razor +@page "/named-forms-with-scope" + +
Hello form from a library
+ + + + + +
Hello form using the same form name
+ + + + + + +@if (submitted) +{ +

Hello @Name from the app form!

+} + +@code { + bool submitted = false; + + [SupplyParameterFromForm(Handler = "Hello")] + public string? Name { get; set; } + + private void Submit() => submitted = true; +} +``` + +### Supply a parameter from the form (`[SupplyParameterFromForm]`) + +The `[SupplyParameterFromForm]` attribute indicates that the value of the associated property should be supplied from the form data for the form. Data in the request that matches the name of the property is bound to the property. Inputs based on `InputBase` generate form value names that match the names Blazor uses for model binding. + +You can specify the name Blazor should use to bind form data to the model using the `Name` or the `Handler` property on `[SupplyParameterFromForm]`. + +The following example independently binds two forms to their models by form name. + +`Starship3.razor`: + +```razor +@page "/starship-3" +@attribute [RenderModeServer] +@inject ILogger Logger + + + + + + + + + + + +@code { + [SupplyParameterFromForm(Name = "Starship1")] + public Starship? Model1 { get; set; } + + [SupplyParameterFromForm(Name = "Starship2")] + public Starship? Model2 { get; set; } protected override void OnInitialized() { - editContext = new(exampleModel); + Model1 ??= new(); + Model2 ??= new(); + } + + private void Submit1() + { + Logger.LogInformation("Submit1: Id = {Id}", Model1?.Id); + } + + private void Submit2() + { + Logger.LogInformation("Submit2: Id = {Id}", Model2?.Id); + } + + public class Starship + { + public string? Id { get; set; } } } ``` -:::moniker-end +### Nest and bind forms + +The following guidance demonstrates how to nest and bind child forms. + + + +The following ship details class (`ShipDetails`) holds a description and length for a subform. + +`ShipDetails.cs`: + +```csharp +public class ShipDetails +{ + public string? Description { get; set; } + public int? Length { get; set; } +} +``` + +The following `Ship` class names an identifier (`Id`) and includes the ship details. + +`Ship.cs`: + +```csharp +public class Ship +{ + public string? Id { get; set; } + public ShipDetails Details { get; set; } = new(); +} +``` + +The following subform is used for editing values of the `ShipDetails` type. This is implemented by inheriting `Editor` at the top of the component. `Editor` ensures that the child component generates the correct form field names based on the model (`T`), where `T` in the following example is `ShipDetails`. + +`StarshipSubform.razor`: + +```razor +@inherits Editor + +
+ +
+
+ +
+``` -Assign **either** an **or** a to an . Assignment of both isn't supported and generates a runtime error: +The main form is bound to the `Ship` class. The `StarshipSubform` component is used to edit ship details, bound as `Model!.Details`. -> Unhandled exception rendering component: EditForm requires a Model parameter, or an EditContext parameter, but not both. +`Starship4.razor`: + +```razor +@page "/starship-4" +@attribute [RenderModeServer] +@inject ILogger Logger + + +
+ +
+ +
+ +
+
+ +@code { + [SupplyParameterFromForm] + public Ship? Model { get; set; } + + protected override void OnInitialized() => Model ??= new(); + + private void Submit() + { + Logger.LogInformation("Id = {Id} Desc = {Description} Length = {Length}", + Model?.Id, Model?.Details?.Description, Model?.Details?.Length); + } +} +``` + +:::moniker-end ## Handle form submission @@ -234,6 +652,48 @@ The provides the following * Use to assign an event handler to run when a form with invalid fields is submitted. * Use to assign an event handler to run regardless of the form fields' validation status. The form is validated by calling in the event handler method. If returns `true`, the form is valid. +:::moniker range=">= aspnetcore-8.0" + +## Antiforgery support + +The `AntiforgeryToken` component renders an antiforgery token as a hidden field, and the `[RequireAntiforgeryToken]` attribute enables antiforgery protection. If an antiforgery check fails, a [`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) response is thrown and the form isn't processed. + +For forms based on , the `AntiforgeryToken` component and `[RequireAntiforgeryToken]` attribute are automatically added to provide antiforgery protection by default. + +For [forms based on the HTML `` element](#html-forms), manually add the `AntiforgeryToken` component to the form: + +```razor +@attribute [RenderModeServer] + + + + + + +@if (submitted) +{ +

Form submitted!

+} + +@code{ + private bool submitted = false; + + private void Submit() => submitted = true; +} +``` + +> [!WARNING] +> For forms based on either or the HTML `
` element, antiforgery protection can be disabled by passing `required: false` to the `[RequireAntiforgeryToken]` attribute. The following example disables antiforgery and is ***not recommended*** for public apps: +> +> ```razor +> @using Microsoft.AspNetCore.Antiforgery +> @attribute [RequireAntiforgeryToken(required: false)] +> ``` + +For more information, see . + +:::moniker-end + ## Built-in input components The Blazor framework provides built-in input components to receive and validate user input. The built-in input components in the following table are supported in an with an . @@ -297,7 +757,7 @@ For more information on the . `Identifier` requires a value of at least one character but no more than 16 characters using the . +* `Id` is required because it's annotated with the . `Id` requires a value of at least one character but no more than 16 characters using the . * `Description` is optional because it isn't annotated with the . * `Classification` is required. * The `MaximumAccommodation` property defaults to zero but requires a value from one to 100,000 per its . @@ -306,171 +766,510 @@ The following `Starship` type, which is used in several of this article's exampl `Starship.cs`: -:::moniker range=">= aspnetcore-7.0" +```csharp +using System.ComponentModel.DataAnnotations; + +public class Starship +{ + [Required] + [StringLength(16, ErrorMessage = "Id too long (16 character limit).")] + public string? Id { get; set; } + + public string? Description { get; set; } + + [Required] + public string? Classification { get; set; } + + [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")] + public int MaximumAccommodation { get; set; } + + [Required] + [Range(typeof(bool), "true", "true", + ErrorMessage = "This form disallows unapproved ships.")] + public bool IsValidatedDesign { get; set; } + [Required] + public DateTime ProductionDate { get; set; } +} +``` + + + +The following form accepts and validates user input using: + +* The properties and validation defined in the preceding `Starship` model. +* Several of Blazor's [built-in input components](#built-in-input-components). + +`Starship5.razor`: + +:::moniker range=">= aspnetcore-8.0" + +```razor +@page "/starship-5" +@attribute [RenderModeServer] +@inject ILogger Logger + +

Starfleet Starship Database

+ +

New Ship Entry Form

+ + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +@code { + [SupplyParameterFromForm] + private Starship? Model { get; set; } + + protected override void OnInitialized() => + Model ??= new() { ProductionDate = DateTime.UtcNow }; + + private void Submit() + { + Logger.LogInformation("Id = {Id} Description = {Description} " + + "Classification = {Classification} MaximumAccommodation = " + + "{MaximumAccommodation} IsValidatedDesign = " + + "{IsValidatedDesign} ProductionDate = {ProductionDate}", + Model?.Id, Model?.Description, Model?.Classification, + Model?.MaximumAccommodation, Model?.IsValidatedDesign, + Model?.ProductionDate); + } +} +``` + + :::moniker-end -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-5" +@inject ILogger Logger -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Starship.cs"::: +

Starfleet Starship Database

+ +

New Ship Entry Form

+ + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +@code { + private Starship? Model { get; set; } + + protected override void OnInitialized() => + Model ??= new() { ProductionDate = DateTime.UtcNow }; + + private void Submit() + { + Logger.LogInformation("Id = {Id} Description = {Description} " + + "Classification = {Classification} MaximumAccommodation = " + + "{MaximumAccommodation} IsValidatedDesign = " + + "{IsValidatedDesign} ProductionDate = {ProductionDate}", + Model?.Id, Model?.Description, Model?.Classification, + Model?.MaximumAccommodation, Model?.IsValidatedDesign, + Model?.ProductionDate); + } +} +``` + + + +:::moniker-end + +The in the preceding example creates an based on the assigned `Starship` instance (`Model="..."`) and handles a valid form. The next example demonstrates how to assign an to a form and validate when the form is submitted. + +In the following example: + +* A shortened version of the earlier `Starfleet Starship Database` form (`Starship5` component) of the [Example form](#example-form) section is used that only accepts a value for the starship's Id. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created. +* The `Submit` method executes when the **`Submit`** button is selected. +* The form is validated by calling in the `Submit` method. +* Logging is executed depending on the validation result. -:::moniker-end +> [!NOTE] +> `Submit` in the next example is demonstrated as an asynchronous method because storing form values often uses asynchronous calls (`await ...`). If the form is used in a test app as shown, `Submit` merely runs synchronously. For testing purposes, ignore the following build warning: +> +> > This async method lacks 'await' operators and will run synchronously. ... -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" +`Starship6.razor`: -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Starship.cs"::: +:::moniker range=">= aspnetcore-8.0" -:::moniker-end +```razor +@page "/starship-6" +@attribute [RenderModeServer] +@inject ILogger Logger -:::moniker range="< aspnetcore-5.0" + + +
+ +
+
+ +
+
-:::code language="csharp" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Starship.cs"::: +@code { + private EditContext? editContext; -:::moniker-end + [SupplyParameterFromForm] + private Starship? Model { get; set; } -The following form accepts and validates user input using: + protected override void OnInitialized() + { + Model ??= + new() + { + Id = "NCC-1701", + Classification = "Exploration", + MaximumAccommodation = 150, + IsValidatedDesign = true, + ProductionDate = new DateTime(2245, 4, 11) + }; + editContext = new(Model); + } -* The properties and validation defined in the preceding `Starship` model. -* Several of Blazor's [built-in input components](#built-in-input-components). + private async Task Submit() + { + if (editContext != null && editContext.Validate()) + { + Logger.LogInformation("Submit called: Form is valid"); -`Pages/FormExample2.razor`: + // await ... -:::moniker range=">= aspnetcore-7.0" + await Task.CompletedTask; + } + else + { + Logger.LogInformation("Submit called: Form is INVALID"); + } + } +} +``` -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample2.razor"::: + :::moniker-end -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-5" +@inject ILogger Logger -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample2.razor"::: + + +
+ +
+
+ +
+
-:::moniker-end +@code { + private Starship Model { get; set; } + + private EditContext? editContext; -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" + protected override void OnInitialized() + { + Model ??= + new() + { + Id = "NCC-1701", + Classification = "Exploration", + MaximumAccommodation = 150, + IsValidatedDesign = true, + ProductionDate = new DateTime(2245, 4, 11) + }; + editContext = new(Model); + } -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample2.razor"::: + private async Task Submit() + { + if (editContext != null && editContext.Validate()) + { + Logger.LogInformation("Submit called: Form is valid"); -:::moniker-end + // await ... -:::moniker range="< aspnetcore-5.0" + await Task.CompletedTask; + } + else + { + Logger.LogInformation("Submit called: Form is INVALID"); + } + } +} +``` -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample2.razor"::: + :::moniker-end -The in the preceding example creates an based on the assigned `Starship` instance (`Model="@starship"`) and handles a valid form. The next example (`FormExample3` component) demonstrates how to assign an to a form and validate when the form is submitted. - -In the following example: +> [!NOTE] +> Changing the after it's assigned is **not** supported. -* A shortened version of the preceding `Starfleet Starship Database` form (`FormExample2` component) is used that only accepts a value for the starship's identifier. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created. -* The `HandleSubmit` method executes when the **`Submit`** button is selected. -* The form is validated by calling in the `HandleSubmit` method. -* Logging is executed depending on the validation result. +:::moniker range=">= aspnetcore-6.0" -> [!NOTE] -> `HandleSubmit` in the `FormExample3` component is demonstrated as an asynchronous method because storing form values often uses asynchronous calls (`await ...`). If the form is used in a test app as shown, `HandleSubmit` merely runs synchronously. For testing purposes, ignore the following build warning: -> -> > This async method lacks 'await' operators and will run synchronously. ... +## Multiple option selection with the `InputSelect` component -`Pages/FormExample3.razor`: +Binding supports [`multiple`](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) option selection with the component. The [`@onchange`](xref:mvc/views/razor#onevent) event provides an array of the selected options via [event arguments (`ChangeEventArgs`)](xref:blazor/components/event-handling#event-arguments). The value must be bound to an array type, and binding to an array type makes the [`multiple`](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) attribute optional on the tag. -:::moniker range=">= aspnetcore-7.0" +In the following example, the user must select at least two starship classifications but no more than three classifications. -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample3.razor" highlight="5,39,44"::: +`Starship7.razor`: :::moniker-end -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +:::moniker range=">= aspnetcore-8.0" -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample3.razor" highlight="5,39,44"::: - -:::moniker-end +```razor +@page "/starship-7" +@attribute [RenderModeServer] +@inject ILogger Logger -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" +

Bind Multiple InputSelect Example

-:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample3.razor" highlight="5,39,44"::: + + + +
+ +
+
+ +
+
-:::moniker-end +@if (Model?.SelectedClassification?.Length > 0) +{ +
@string.Join(", ", Model.SelectedClassification)
+} -:::moniker range="< aspnetcore-5.0" +@code { + private EditContext? editContext; -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample3.razor" highlight="5,39,44"::: + [SupplyParameterFromForm] + private Starship? Model { get; set; } -:::moniker-end + protected override void OnInitialized() + { + Model = new(); + editContext = new(Model); + } -> [!NOTE] -> Changing the after it's assigned is **not** supported. + private void Submit() + { + Logger.LogInformation("Submit called: Processing the form"); + } -:::moniker range=">= aspnetcore-6.0" + private class Starship + { + [Required] + [MinLength(2, ErrorMessage = "Select at least two classifications.")] + [MaxLength(3, ErrorMessage = "Select no more than three classifications.")] + public Classification[]? SelectedClassification { get; set; } = + new[] { Classification.None }; + } -## Multiple option selection with the `InputSelect` component + private enum Classification { None, Exploration, Diplomacy, Defense, Research } +} +``` -Binding supports [`multiple`](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) option selection with the component. The [`@onchange`](xref:mvc/views/razor#onevent) event provides an array of the selected options via [event arguments (`ChangeEventArgs`)](xref:blazor/components/event-handling#event-arguments). The value must be bound to an array type, and binding to an array type makes the [`multiple`](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) attribute optional on the tag. + -In the following example, the user must select at least two starship classifications but no more than three classifications. +:::moniker-end -`Pages/BindMultipleWithInputSelect.razor`: +:::moniker range="< aspnetcore-8.0" ```razor -@page "/bind-multiple-with-inputselect" -@using System.ComponentModel.DataAnnotations -@using Microsoft.Extensions.Logging -@inject ILogger Logger +@page "/starship-7" +@inject ILogger Logger -

Bind Multiple InputSelectExample

+

Bind Multiple InputSelect Example

- + - -

+

-

- - +
+
+ +
-

- Selected Classifications: - @string.Join(", ", starship.SelectedClassification) -

+@if (Model?.SelectedClassification?.Length > 0) +{ +
@string.Join(", ", Model.SelectedClassification)
+} @code { private EditContext? editContext; - private Starship starship = new(); + + private Starship? Model { get; set; } protected override void OnInitialized() { - editContext = new(starship); + Model ??= new(); + editContext = new(Model); } - private void HandleValidSubmit() + private void Submit() { - Logger.LogInformation("HandleValidSubmit called"); + Logger.LogInformation("Submit called: Processing the form"); } private class Starship { - [Required, MinLength(2), MaxLength(3)] - public Classification[] SelectedClassification { get; set; } = - new[] { Classification.Diplomacy }; + [Required] + [MinLength(2, ErrorMessage = "Select at least two classifications.")] + [MaxLength(3, ErrorMessage = "Select no more than three classifications.")] + public Classification[]? SelectedClassification { get; set; } = + new[] { Classification.None }; } - private enum Classification { Exploration, Diplomacy, Defense, Research } + private enum Classification { None, Exploration, Diplomacy, Defense, Research } } ``` + + +:::moniker-end + +:::moniker range=">= aspnetcore-6.0" + For information on how empty strings and `null` values are handled in data binding, see the [Binding `InputSelect` options to C# object `null` values](#binding-inputselect-options-to-c-object-null-values) section. :::moniker-end @@ -485,12 +1284,12 @@ For information on how empty strings and `null` values are handled in data bindi Several built-in components support display names with the parameter. -In the `Starfleet Starship Database` form (`FormExample2` component) of the [Example form](#example-form) section, the production date of a new starship doesn't specify a display name: +In the `Starfleet Starship Database` form (`Starship5` component) of the [Example form](#example-form) section, the production date of a new starship doesn't specify a display name: ```razor ``` @@ -503,7 +1302,7 @@ Set the Production Date: - ``` @@ -523,7 +1322,7 @@ The validation summary displays the friendly name when the field's value is inva :::moniker range=">= aspnetcore-5.0" -In the `Starfleet Starship Database` form (`FormExample2` component) of the [Example form](#example-form) section with a [friendly display name](#display-name-support) assigned, the `Production Date` field produces an error message using the following default error message template: +In the `Starfleet Starship Database` form (`Starship5` component) of the [Example form](#example-form) section with a [friendly display name](#display-name-support) assigned, the `Production Date` field produces an error message using the following default error message template: ```css The {0} field must be a date. @@ -534,8 +1333,8 @@ The position of the `{0}` placeholder is where the value of the Production Date: - + ``` @@ -546,9 +1345,9 @@ Assign a custom template to Production Date: - + ``` @@ -558,7 +1357,7 @@ Assign a custom template to Production Date: - + ``` @@ -580,8 +1379,8 @@ Assign a custom template to Production Date: - + ``` @@ -595,31 +1394,168 @@ In basic form validation scenarios, an before validating the form. +In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form. -`Pages/FormExample4.razor`: +`Starship8.razor`: -:::moniker range=">= aspnetcore-7.0" +:::moniker range=">= aspnetcore-8.0" -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample4.razor" highlight="38,42-52,72"::: +```razor +@page "/starship-8" +@attribute [RenderModeServer] +@implements IDisposable +@inject ILogger Logger + +

Holodeck Configuration

+ + + + + +
+ +
+
-:::moniker-end +@code { + private EditContext? editContext; -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" + [SupplyParameterFromForm] + public Holodeck? Model { get; set; } -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample4.razor" highlight="38,42-52,72"::: + private ValidationMessageStore? messageStore; -:::moniker-end + 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 Type1 { get; set; } + public bool Type2 { get; set; } + public bool Options => Type1 || Type2; + } -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" + public void Dispose() + { + if (editContext is not null) + { + editContext.OnValidationRequested -= HandleValidationRequested; + } + } +} +``` -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample4.razor" highlight="38,42-53,70"::: + :::moniker-end -:::moniker range="< aspnetcore-5.0" +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-8" +@implements IDisposable +@inject ILogger Logger + +

Holodeck Configuration

+ + + + + +
+ +
+ +
+ +@code { + private EditContext? editContext; + + public Holodeck? Model { get; set; } -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample4.razor" highlight="38,42-53,70"::: + 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 Type1 { get; set; } + public bool Type2 { get; set; } + public bool Options => Type1 || Type2; + } + + public void Dispose() + { + if (editContext is not null) + { + editContext.OnValidationRequested -= HandleValidationRequested; + } + } +} +``` + + :::moniker-end @@ -659,86 +1595,260 @@ Create a validator component from event is raised. Only the errors for the field are cleared. * The `ClearErrors` method is called by developer code. All of the errors are cleared. -`CustomValidation.cs` (if used in a test app, change the namespace, `BlazorSample`, to match the app's namespace): +Update the namespace in the following class to match your app's namespace. -:::moniker range=">= aspnetcore-7.0" +`CustomValidation.cs`: + +```csharp +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> 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(); + } +} +``` + -:::moniker-end +> [!IMPORTANT] +> Specifying a namespace is **required** when deriving from . Failing to specify a namespace results in a build error: +> +> > Tag helpers cannot target tag name '\.{CLASS NAME}' because it contains a ' ' character. +> +> The `{CLASS NAME}` placeholder is the name of the component class. The custom validator example in this section specifies the example namespace `BlazorSample`. + +> [!NOTE] +> Anonymous lambda expressions are registered event handlers for and in the preceding example. It isn't necessary to implement and unsubscribe the event delegates in this scenario. For more information, see . + +## Business logic validation with a validator component + +For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary. + +[Basic validation](#basic-validation) is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components. + +In the following example: + +* A shortened version of the `Starfleet Starship Database` form (`Starship5` component) of the [Example form](#example-form) section is used that only accepts the starship's classification and description. Data annotation validation is **not** triggered on form submission because the `DataAnnotationsValidator` component isn't included in the form. +* The `CustomValidation` component from the [Validator components](#validator-components) section of this article is used. +* The validation requires a value for the ship's description (`Description`) if the user selects the "`Defense`" ship classification (`Classification`). + +When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary. + +`Starship9.razor`: + +:::moniker range=">= aspnetcore-8.0" + +```razor +@page "/starship-9" +@attribute [RenderModeServer] +@inject ILogger Logger + +

Starfleet Starship Database

+ +

New Ship Entry Form

+ + + + +
+ +
+
+ +
+
+ +
+
-:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +@code { + private CustomValidation? customValidation; -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/CustomValidation.cs"::: + [SupplyParameterFromForm] + public Starship? Model { get; set; } -:::moniker-end + protected override void OnInitialized() => + Model ??= new() { ProductionDate = DateTime.UtcNow }; -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" + private void Submit() + { + customValidation?.ClearErrors(); -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/CustomValidation.cs"::: + var errors = new Dictionary>(); -:::moniker-end + if (Model!.Classification == "Defense" && + string.IsNullOrEmpty(Model.Description)) + { + errors.Add(nameof(Model.Description), + new() { "For a 'Defense' ship classification, " + + "'Description' is required." }); + } -:::moniker range="< aspnetcore-5.0" + if (errors.Any()) + { + customValidation?.DisplayErrors(errors); + } + else + { + Logger.LogInformation("Submit called: Processing the form"); + } + } +} +``` -:::code language="csharp" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/CustomValidation.cs"::: + :::moniker-end -> [!IMPORTANT] -> Specifying a namespace is **required** when deriving from . Failing to specify a namespace results in a build error: -> -> > Tag helpers cannot target tag name '\.{CLASS NAME}' because it contains a ' ' character. -> -> The `{CLASS NAME}` placeholder is the name of the component class. The custom validator example in this section specifies the example namespace `BlazorSample`. - -> [!NOTE] -> Anonymous lambda expressions are registered event handlers for and in the preceding example. It isn't necessary to implement and unsubscribe the event delegates in this scenario. For more information, see . +:::moniker range="< aspnetcore-8.0" -## Business logic validation with a validator component +```razor +@page "/starship-9" +@inject ILogger Logger -For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary. +

Starfleet Starship Database

-[Basic validation](#basic-validation) is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components. +

New Ship Entry Form

-In the following example: + + + +
+ +
+
+ +
+
+ +
+
-* A shortened version of the `Starfleet Starship Database` form (`FormExample2` component) from the [Example form](#example-form) section is used that only accepts the starship's classification and description. Data annotation validation is **not** triggered on form submission because the `DataAnnotationsValidator` component isn't included in the form. -* The `CustomValidation` component from the [Validator components](#validator-components) section of this article is used. -* The validation requires a value for the ship's description (`Description`) if the user selects the "`Defense`" ship classification (`Classification`). +@code { + private CustomValidation? customValidation; -When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary. + public Starship? Model { get; set; } -`Pages/FormExample5.razor`: + protected override void OnInitialized() => + Model ??= new() { ProductionDate = DateTime.UtcNow }; -:::moniker range=">= aspnetcore-7.0" + private void Submit() + { + customValidation?.ClearErrors(); -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample5.razor"::: + var errors = new Dictionary>(); -:::moniker-end + if (Model!.Classification == "Defense" && + string.IsNullOrEmpty(Model.Description)) + { + errors.Add(nameof(Model.Description), + new() { "For a 'Defense' ship classification, " + + "'Description' is required." }); + } -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" + if (errors.Any()) + { + customValidation?.DisplayErrors(errors); + } + else + { + Logger.LogInformation("Submit called: Processing the form"); + } + } +} +``` -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample5.razor"::: + :::moniker-end -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" - -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample5.razor"::: +> [!NOTE] +> As an alternative to using [validation components](#validator-components), data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation, the attributes must be executable on the server. For more information, see . -:::moniker-end +## Server validation with a validator component -:::moniker range="< aspnetcore-5.0" +*This section is focused on hosted Blazor WebAssembly scenarios, but the approach for any type of app that uses server validation with web API adopts the same general approach.* -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample5.razor"::: +:::moniker range=">= aspnetcore-8.0" -:::moniker-end + > [!NOTE] -> As an alternative to using [validation components](#validator-components), data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation, the attributes must be executable on the server. For more information, see . +> This section hasn't been updated to include [new .NET 8 antiforgery support features](#antiforgery-support) or guidance for Blazor Web Apps. Article updates are scheduled by [Add server validation with validator components for 8.0/BWA (dotnet/AspNetCore.Docs #30055)](https://github.com/dotnet/AspNetCore.Docs/issues/30055). -## Server validation with a validator component +:::moniker-end Server validation is supported in addition to client-side validation: @@ -753,7 +1863,7 @@ Server validation is supported in addition to client-side validation: The following example is based on: * A hosted Blazor WebAssembly [solution](xref:blazor/tooling#visual-studio-solution-file-sln) created from the [Blazor WebAssembly project template](xref:blazor/project-structure). The approach is supported for any of the secure hosted Blazor solutions described in the [hosted Blazor WebAssembly security documentation](xref:blazor/security/webassembly/index#implementation-guidance). -* The `Starship` model (`Starship.cs`) from the [Example form](#example-form) section. +* The `Starship` model (`Starship.cs`) of the [Example form](#example-form) section. * The `CustomValidation` component shown in the [Validator components](#validator-components) section. Place the `Starship` model (`Starship.cs`) into the solution's **`Shared`** project so that both the client and server apps can use the model. Add or update the namespace to match the namespace of the shared app (for example, `namespace BlazorSample.Shared`). Since the model requires data annotations, add the [`System.ComponentModel.Annotations`](https://www.nuget.org/packages/System.ComponentModel.Annotations) package to the **`Shared`** project. @@ -774,8 +1884,6 @@ The validation for the `Defense` ship classification only occurs server-side in `Controllers/StarshipValidation.cs`: -:::moniker range=">= aspnetcore-6.0" - ```csharp using System; using System.Threading.Tasks; @@ -803,16 +1911,16 @@ public class StarshipValidationController : ControllerBase static readonly string[] scopeRequiredByApi = new[] { "API.Access" }; [HttpPost] - public async Task Post(Starship starship) + public async Task Post(Starship model) { HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); try { - if (starship.Classification == "Defense" && - string.IsNullOrEmpty(starship.Description)) + if (model.Classification == "Defense" && + string.IsNullOrEmpty(model.Description)) { - ModelState.AddModelError(nameof(starship.Description), + ModelState.AddModelError(nameof(model.Description), "For a 'Defense' ship " + "classification, 'Description' is required."); } @@ -820,7 +1928,6 @@ public class StarshipValidationController : ControllerBase { logger.LogInformation("Processing the form asynchronously"); - // Process the valid form // async ... return Ok(ModelState); @@ -836,73 +1943,6 @@ public class StarshipValidationController : ControllerBase } ``` -:::moniker-end - -:::moniker range="< aspnetcore-6.0" - -```csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Identity.Web.Resource; -using BlazorSample.Shared; - -namespace BlazorSample.Server.Controllers -{ - [Authorize] - [ApiController] - [Route("[controller]")] - public class StarshipValidationController : ControllerBase - { - private readonly ILogger logger; - - public StarshipValidationController( - ILogger logger) - { - this.logger = logger; - } - - static readonly string[] scopeRequiredByApi = new[] { "API.Access" }; - - [HttpPost] - public async Task Post(Starship starship) - { - HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); - - try - { - if (starship.Classification == "Defense" && - string.IsNullOrEmpty(starship.Description)) - { - ModelState.AddModelError(nameof(starship.Description), - "For a 'Defense' ship " + - "classification, 'Description' is required."); - } - else - { - logger.LogInformation("Processing the form asynchronously"); - - // Process the valid form - // async ... - - return Ok(ModelState); - } - } - catch (Exception ex) - { - logger.LogError("Validation Error: {Message}", ex.Message); - } - - return BadRequest(ModelState); - } - } -} -``` - -:::moniker-end - If using the preceding controller in a hosted Blazor WebAssembly app, update the namespace (`BlazorSample.Server.Controllers`) to match the app's controllers namespace. When a model binding validation error occurs on the server, an [`ApiController`](xref:web-api/index) () normally returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation: @@ -912,7 +1952,7 @@ When a model binding validation error occurs on the server, an [`ApiController`] "title": "One or more validation errors occurred.", "status": 400, "errors": { - "Identifier": ["The Identifier field is required."], + "Id": ["The Id field is required."], "Classification": ["The Classification field is required."], "IsValidatedDesign": ["This form disallows unapproved ships."], "MaximumAccommodation": ["Accommodation invalid (1-100000)."] @@ -927,7 +1967,7 @@ If the server API returns the preceding default JSON response, it's possible for ```json { - "Identifier": ["The Identifier field is required."], + "Id": ["The Id field is required."], "Classification": ["The Classification field is required."], "IsValidatedDesign": ["This form disallows unapproved ships."], "MaximumAccommodation": ["Accommodation invalid (1-100000)."] @@ -969,89 +2009,84 @@ In the **:::no-loc text="Client":::** project, add the `CustomValidation` compon In the **:::no-loc text="Client":::** project, the `Starfleet Starship Database` form is updated to show server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary. -In the following `FormExample6` component, update the namespace of the **`Shared`** project (`@using BlazorSample.Shared`) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form. +In the following component, update the namespace of the **`Shared`** project (`@using BlazorSample.Shared`) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form. -`Pages/FormExample6.razor`: +`Starship10.razor`: -:::moniker range=">= aspnetcore-6.0" + ```razor -@page "/form-example-6" +@page "/starship-10" @using System.Net @using System.Net.Http.Json @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using Microsoft.Extensions.Logging @using BlazorSample.Shared @attribute [Authorize] @inject HttpClient Http -@inject ILogger Logger +@inject ILogger Logger

Starfleet Starship Database

New Ship Entry Form

- + - -

+

-

-

+

+
-

-

+

+
-

-

+

+
-

-

+

+
-

-

+

+
-

- - - +
+
+ +

@message -

- -

- Star Trek, - ©1966-2019 CBS Studios, Inc. and - Paramount Pictures -

+
@code { private bool disabled; - private string message; - private string messageStyles = "visibility:hidden"; - private CustomValidation customValidation; - private Starship starship = new() { ProductionDate = DateTime.UtcNow }; + private string? message; + private string? messageStyles = "visibility:hidden"; + private CustomValidation? customValidation; + + public Starship? Model { get; set; } - private async Task HandleValidSubmit(EditContext editContext) + protected override void OnInitialized() => + Model ??= new() { ProductionDate = DateTime.UtcNow }; + + private async Task Submit(EditContext editContext) { - customValidation.ClearErrors(); + customValidation?.ClearErrors(); try { @@ -1206,12 +2246,13 @@ In the following `FormExample6` component, update the namespace of the **`Shared "StarshipValidation", (Starship)editContext.Model); var errors = await response.Content - .ReadFromJsonAsync>>(); + .ReadFromJsonAsync>>() ?? + new Dictionary>(); if (response.StatusCode == HttpStatusCode.BadRequest && errors.Any()) { - customValidation.DisplayErrors(errors); + customValidation?.DisplayErrors(errors); } else if (!response.IsSuccessStatusCode) { @@ -1240,7 +2281,12 @@ In the following `FormExample6` component, update the namespace of the **`Shared } ``` -:::moniker-end + > [!NOTE] > As an alternative to the use of a [validation component](#validator-components), data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation, the attributes must be executable on the server. For more information, see . @@ -1256,87 +2302,106 @@ In the following `FormExample6` component, update the namespace of the **`Shared Use the component to create a custom component that uses the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)) instead of the `onchange` event ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). Use of the `input` event triggers field validation on each keystroke. -The following example uses the `ExampleModel` class. - -`ExampleModel.cs`: - -:::moniker range=">= aspnetcore-7.0" - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/ExampleModel.cs"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" - -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/ExampleModel.cs"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" - -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/ExampleModel.cs"::: - -:::moniker-end - -:::moniker range="< aspnetcore-5.0" - -:::code language="csharp" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/ExampleModel.cs"::: - -:::moniker-end - The following `CustomInputText` component inherits the framework's `InputText` component and sets event binding to the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)). -`Shared/CustomInputText.razor`: +`CustomInputText.razor`: -:::moniker range=">= aspnetcore-7.0" - -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Shared/forms-and-validation/CustomInputText.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +```razor +@inherits InputText -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Shared/forms-and-validation/CustomInputText.razor"::: + +``` -:::moniker-end +The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component. -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" +`Starship11.razor`: -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Shared/forms-and-validation/CustomInputText.razor"::: +:::moniker range=">= aspnetcore-8.0" -:::moniker-end +```razor +@page "/starship-11" +@attribute [RenderModeServer] +@inject ILogger Logger -:::moniker range="< aspnetcore-5.0" + + + + + + -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Shared/forms-and-validation/CustomInputText.razor"::: +
+ CurrentValue: @Model?.Id +
-:::moniker-end +@code { + [SupplyParameterFromForm] + public Starship? Model { get; set; } -The `CustomInputText` component can be used anywhere is used. The following `FormExample7` component uses the shared `CustomInputText` component. + protected override void OnInitialized() => Model ??= new(); -`Pages/FormExample7.razor`: + private void Submit() + { + Logger.LogInformation("Submit called: Processing the form"); + } -:::moniker range=">= aspnetcore-7.0" + public class Starship + { + [Required] + [StringLength(10, ErrorMessage = "Id is too long.")] + public string? Id { get; set; } + } +} +``` -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample7.razor" highlight="9"::: + :::moniker-end -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +:::moniker range="< aspnetcore-8.0" -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample7.razor" highlight="9"::: +```razor +@page "/starship-11" +@inject ILogger Logger -:::moniker-end + + + + + + -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" +
+ CurrentValue: @Model?.Id +
-:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample7.razor" highlight="9"::: +@code { + public Starship? Model { get; set; } -:::moniker-end + protected override void OnInitialized() => Model ??= new(); -:::moniker range="< aspnetcore-5.0" + 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; } + } +} +``` -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample7.razor" highlight="9"::: + :::moniker-end @@ -1344,7 +2409,7 @@ The `CustomInputText` component can be used anywhere components with the component to create a radio button group. In the following example, properties are added to the `Starship` model described in the [Example form](#example-form) section: - -:::moniker-end - -:::moniker range=">= aspnetcore-6.0" - -```csharp -[Required] -[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX), - nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a manufacturer.")] -public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown; - -[Required, EnumDataType(typeof(Color))] -public Color? Color { get; set; } = null; - -[Required, EnumDataType(typeof(Engine))] -public Engine? Engine { get; set; } = null; + public enum Color { ImperialRed, SpacecruiserGreen, StarshipBlue, VoyagerOrange } + public enum Engine { Ion, Plasma, Fusion, Warp } +} ``` -:::moniker-end +Make the `enums` class accessible to the: -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" +* `Starship` model in `Starship.cs` (for example, `using static ComponentEnums;`). +* `Starfleet Starship Database` form (`Starship5.razor`) (for example, `@using static ComponentEnums`). + +Use components with the component to create a radio button group. In the following example, properties are added to the `Starship` model described in the [Example form](#example-form) section: ```csharp [Required] @@ -1392,17 +2436,13 @@ public Engine? Engine { get; set; } = null; public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown; [Required, EnumDataType(typeof(Color))] -public Color Color { get; set; } = null; +public Color? Color { get; set; } = null; [Required, EnumDataType(typeof(Engine))] -public Engine Engine { get; set; } = null; +public Engine? Engine { get; set; } = null; ``` -:::moniker-end - -:::moniker range=">= aspnetcore-5.0" - -Update the `Starfleet Starship Database` form (`FormExample2` component) from the [Example form](#example-form) section. Add the components to produce: +Update the `Starfleet Starship Database` form (`Starship5` component) of the [Example form](#example-form) section. Add the components to produce: * A radio button group for the ship manufacturer. * A nested radio button group for engine and ship color. @@ -1410,50 +2450,117 @@ Update the `Starfleet Starship Database` form (`FormExample2` component) from th > [!NOTE] > Nested radio button groups aren't often used in forms because they can result in a disorganized layout of form controls that may confuse users. However, there are cases when they make sense in UI design, such as in the following example that pairs recommendations for two user inputs, ship engine and ship color. One engine and one color are required by the form's validation. The form's layout uses nested s to pair engine and color recommendations. However, the user can combine any engine with any color to submit the form. +> [!NOTE] +> Be sure to make the `ComponentEnums` class available to the component for the following example: +> +> ```razor +> @using static ComponentEnums +> ``` + ```razor
Manufacturer - + @foreach (var manufacturer in (Manufacturer[])Enum .GetValues(typeof(Manufacturer))) { - +
+ +
}
-
+
-

- Select one engine and one color. Recommendations are paired but any - combination of engine and color is allowed:
- - - - Engine: Ion
- - Color: Imperial Red

- - Engine: Plasma
- - Color: Spacecruiser Green

- - Engine: Fusion
- - Color: Starship Blue

- - Engine: Warp
- - Color: Voyager Orange +

+ Engine and Color +

+ Engine and color pairs are recommended, but any + combination of engine and color is allowed. +

+ + +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
-

+
``` > [!NOTE] > If `Name` is omitted, components are grouped by their most recent ancestor. +If you implemented the preceding Razor markup in the `Starship5` component of the [Example form](#example-form) section, update the logging for the `Submit` method: + +```csharp +Logger.LogInformation("Id = {Id} Description = {Description} " + + "Classification = {Classification} MaximumAccommodation = " + + "{MaximumAccommodation} IsValidatedDesign = " + + "{IsValidatedDesign} ProductionDate = {ProductionDate} " + + "Manufacturer = {Manufacturer}, Engine = {Engine}, " + + "Color = {Color}", + Model?.Id, Model?.Description, Model?.Classification, + Model?.MaximumAccommodation, Model?.IsValidatedDesign, + Model?.ProductionDate, Model?.Manufacturer, Model?.Engine, + Model?.Color); +``` + :::moniker-end :::moniker range="< aspnetcore-5.0" @@ -1463,7 +2570,7 @@ When working with radio buttons in a form, data binding is handled differently t * Handle data binding for a radio button group. * Support validation using a custom component. -`Shared/InputRadio.razor`: +`InputRadio.razor`: ```razor @using System.Globalization @@ -1497,7 +2604,7 @@ When working with radio buttons in a form, data binding is handled differently t else { result = default; - errorMessage = $"{FieldIdentifier.FieldName} field isn't valid."; + errorMessage = $"{FieldId.FieldName} field isn't valid."; return false; } @@ -1513,7 +2620,7 @@ For more information on generic type parameters (`@typeparam`), see the followin The following `RadioButtonExample` component uses the preceding `InputRadio` component to obtain and validate a rating from the user: -`Pages/RadioButtonExample.razor`: +`RadioButtonExample.razor`: ```razor @page "/radio-button-example" @@ -1523,31 +2630,36 @@ The following `RadioButtonExample` component uses the preceding `InputRadio` com

Radio Button Example

- + @for (int i = 1; i <= 5; i++) { - +
+ +
} - +
+ +
-

You chose: @model.Rating

+
@Model.Rating
@code { - private Model model = new(); + public Starship Model { get; set; } + + protected override void OnInitialized() => Model ??= new(); private void HandleValidSubmit() { Logger.LogInformation("HandleValidSubmit called"); - - // Process the valid form } public class Model @@ -1571,13 +2683,13 @@ The component sum Output validation messages for a specific model with the `Model` parameter: ```razor - + ``` The component displays validation messages for a specific field, which is similar to the [Validation Message Tag Helper](xref:mvc/views/working-with-forms#the-validation-message-tag-helper). Specify the field for validation with the attribute and a lambda expression naming the model property: ```razor - + ``` The and components support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the generated `
` or `
    ` element. @@ -1615,15 +2727,15 @@ public class CustomValidator : ValidationAttribute Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI). -The `SaladChef` class indicates the approved fruit ingredient list for a salad. +The `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad. `SaladChef.cs`: ```csharp public class SaladChef { - public string[] ThingsYouCanPutInASalad = { "Strawberries", "Pineapple", - "Honeydew", "Watermelon", "Grapes" }; + public string[] ThingsYouCanPutInASalad = { "Horva", "Kanda Root", + "Krintar", "Plomeek", "Syto Bean" }; } ``` @@ -1637,8 +2749,6 @@ The `IsValid` method of the following `SaladChefValidatorAttribute` class obtain `SaladChefValidatorAttribute.cs`: -:::moniker range=">= aspnetcore-6.0" - ```csharp using System.ComponentModel.DataAnnotations; @@ -1654,65 +2764,40 @@ public class SaladChefValidatorAttribute : ValidationAttribute return ValidationResult.Success; } - return new ValidationResult("You should not put that in a salad!"); - } -} -``` - -:::moniker-end - -:::moniker range="< aspnetcore-6.0" - -```csharp -using System.ComponentModel.DataAnnotations; - -public class SaladChefValidatorAttribute : ValidationAttribute -{ - protected override ValidationResult IsValid(object value, - ValidationContext validationContext) - { - var saladChef = validationContext.GetRequiredService(); - - if (saladChef.ThingsYouCanPutInASalad.Contains(value?.ToString())) - { - return ValidationResult.Success; - } - - return new ValidationResult("You should not put that in a salad!"); + return new ValidationResult("You should not put that in a salad! " + + "Only use an ingredient from this list: " + + string.Join(", ", saladChef.ThingsYouCanPutInASalad)); } } ``` -:::moniker-end - -The following `ValidationWithDI` component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). +The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). -`Pages/ValidationWithDI.razor`: +`Starship12.razor`: -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-8.0" ```razor -@page "/validation-with-di" -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Components.Forms +@page "/starship-12" +@attribute [RenderModeServer] - -

    - Name something you can put in a salad: - -

    - - - +
    + +
    +
    + +
      @foreach (var message in context.GetValidationMessages()) {
    • @message
    • }
    -
    @code { @@ -1723,35 +2808,31 @@ The following `ValidationWithDI` component validates user input by applying the :::moniker-end -:::moniker range="< aspnetcore-6.0" +:::moniker range="< aspnetcore-8.0" ```razor -@page "/validation-with-di" -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Components.Forms +@page "/starship-12" - -

    +

    Name something you can put in a salad: -

    - - - +
    +
    + +
      @foreach (var message in context.GetValidationMessages()) {
    • @message
    • }
    -
    @code { [SaladChefValidator] - public string SaladIngredient { get; set; } + public string? SaladIngredient { get; set; } } ``` @@ -1763,15 +2844,9 @@ The following `ValidationWithDI` component validates user input by applying the Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as [Bootstrap](https://getbootstrap.com/). -The following example uses the `ExampleModel` class. - -`ExampleModel.cs`: - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/ExampleModel.cs"::: - To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (`validField`) and invalid (`invalidField`) styles are specified. -`wwwroot/css/app.css` (Blazor WebAssembly) or `wwwroot/css/site.css` (Blazor Server): +Add the following CSS classes to the app's stylesheet: ```css .validField { @@ -1787,239 +2862,178 @@ Create a class derived from instance with . - -`Pages/FormExample8.razor`: - -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample8.razor" highlight="21"::: - -The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. Using [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the field is matched to the model member's property or field name, not an `id` assigned to the HTML entity. - -`CustomFieldClassProvider2.cs`: - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider2.cs"::: - -> [!NOTE] -> Matching the field name in the preceding example is case sensitive, so a model property member designated "`Name`" must match a conditional check on "`Name`": -> -> * Correctly matches: `fieldIdentifier.FieldName == "Name"` -> * Fails to match: `fieldIdentifier.FieldName == "name"` -> * Fails to match: `fieldIdentifier.FieldName == "NAME"` -> * Fails to match: `fieldIdentifier.FieldName == "nAmE"` - -Add an additional property to `ExampleModel`, for example: - ```csharp -[StringLength(10, ErrorMessage = "Description is too long.")] -public string? Description { get; set; } -``` +using Microsoft.AspNetCore.Components.Forms; -Add the `Description` to the `ExampleForm7` component's form: - -```razor - -``` - -Update the `EditContext` instance in the component's `OnInitialized` method to use the new Field CSS Class Provider: +public class CustomFieldClassProvider : FieldCssClassProvider +{ + public override string GetFieldCssClass(EditContext editContext, + in FieldIdentifier fieldIdentifier) + { + var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); -```csharp -editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2()); + return isValid ? "validField" : "invalidField"; + } +} ``` -Because a CSS validation class isn't applied to the `Description` field, it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error: - -> Description is too long. - -In the following example: - -* The custom CSS style is applied to the `Name` field. -* Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, `modified` with `valid` or `invalid`. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet: - - ```css - .valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; - } - - .invalid { - outline: 1px solid red; - } - ``` - -`CustomFieldClassProvider3.cs`: - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider3.cs"::: - -Update the `EditContext` instance in the component's `OnInitialized` method to use the preceding Field CSS Class Provider: - -```csharp -editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3()); -``` + -Using `CustomFieldClassProvider3`: +Set the `CustomFieldClassProvider` class as the Field CSS Class Provider on the form's instance with . -* The `Name` field uses the app's custom validation CSS styles. -* The `Description` field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles. +`Starship13.razor`: :::moniker-end -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +:::moniker range=">= aspnetcore-8.0" -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as [Bootstrap](https://getbootstrap.com/). - -The following example uses the `ExampleModel` class. +```razor +@page "/starship-13" +@attribute [RenderModeServer] +@inject ILogger Logger -`ExampleModel.cs`: + + + + + + -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/ExampleModel.cs"::: +@code { + private EditContext? editContext; -To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (`validField`) and invalid (`invalidField`) styles are specified. + [SupplyParameterFromForm] + public Starship? Model { get; set; } -`wwwroot/css/app.css` (Blazor WebAssembly) or `wwwroot/css/site.css` (Blazor Server): + protected override void OnInitialized() + { + Model ??= new(); + editContext = new(Model); + editContext.SetFieldCssClassProvider(new CustomFieldClassProvider()); + } -```css -.validField { - border-color: lawngreen; -} + private void Submit() + { + Logger.LogInformation("Submit called: Processing the form"); + } -.invalidField { - background-color: tomato; + public class Starship + { + [Required] + [StringLength(10, ErrorMessage = "Id is too long.")] + public string? Id { get; set; } + } } ``` -Create a class derived from that checks for field validation messages and applies the appropriate valid or invalid style. - -`CustomFieldClassProvider.cs`: - -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/CustomFieldClassProvider.cs"::: - -Set the `CustomFieldClassProvider` class as the Field CSS Class Provider on the form's instance with . - -`Pages/FormExample8.razor`: - -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample8.razor" highlight="21"::: - -The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. - -`CustomFieldClassProvider2.cs`: - -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/CustomFieldClassProvider2.cs"::: - -Add an additional property to `ExampleModel`, for example: + -```csharp -[StringLength(10, ErrorMessage = "Description is too long.")] -public string? Description { get; set; } -``` +:::moniker-end -Add the `Description` to the `ExampleForm7` component's form: +:::moniker range="< aspnetcore-8.0" ```razor - -``` - -Update the `EditContext` instance in the component's `OnInitialized` method to use the new Field CSS Class Provider: +@page "/starship-13" +@inject ILogger Logger -```csharp -editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2()); -``` - -Because a CSS validation class isn't applied to the `Description` field, it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error: - -> Description is too long. - -In the following example: - -* The custom CSS style is applied to the `Name` field. -* Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, `modified` with `valid` or `invalid`. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet: - - ```css - .valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; - } + + + + + + - .invalid { - outline: 1px solid red; - } - ``` +@code { + private EditContext? editContext; -`CustomFieldClassProvider3.cs`: + public Starship? Model { get; set; } -:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/CustomFieldClassProvider3.cs"::: + protected override void OnInitialized() + { + Model ??= new(); + editContext = new(Model); + editContext.SetFieldCssClassProvider(new CustomFieldClassProvider()); + } -Update the `EditContext` instance in the component's `OnInitialized` method to use the preceding Field CSS Class Provider: + private void Submit() + { + Logger.LogInformation("Submit called: Processing the form"); + } -```csharp -editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3()); + public class Starship + { + [Required] + [StringLength(10, ErrorMessage = "Id is too long.")] + public string? Id { get; set; } + } +} ``` -Using `CustomFieldClassProvider3`: - -* The `Name` field uses the app's custom validation CSS styles. -* The `Description` field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles. + :::moniker-end -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" - -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as [Bootstrap](https://getbootstrap.com/). - -The following example uses the `ExampleModel` class. - -`ExampleModel.cs`: - -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/ExampleModel.cs"::: - -To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (`validField`) and invalid (`invalidField`) styles are specified. - -`wwwroot/css/app.css` (Blazor WebAssembly) or `wwwroot/css/site.css` (Blazor Server): - -```css -.validField { - border-color: lawngreen; -} - -.invalidField { - background-color: tomato; -} -``` - -Create a class derived from that checks for field validation messages and applies the appropriate valid or invalid style. +:::moniker range=">= aspnetcore-7.0" -`CustomFieldClassProvider.cs`: +The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. Using [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the field is matched to the model member's property or field name, not an `id` assigned to the HTML entity. -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/CustomFieldClassProvider.cs" highlight="11"::: +`CustomFieldClassProvider2.cs`: -Set the `CustomFieldClassProvider` class as the Field CSS Class Provider on the form's instance with . +```csharp +using Microsoft.AspNetCore.Components.Forms; -`Pages/FormExample8.razor`: +public class CustomFieldClassProvider2 : FieldCssClassProvider +{ + public override string GetFieldCssClass(EditContext editContext, + in FieldIdentifier fieldIdentifier) + { + if (fieldIdentifier.FieldName == "Name") + { + var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample8.razor" highlight="21"::: + return isValid ? "validField" : "invalidField"; + } -The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. + return string.Empty; + } +} +``` -`CustomFieldClassProvider2.cs`: + -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/CustomFieldClassProvider2.cs" highlight="9,16"::: +> [!NOTE] +> Matching the field name in the preceding example is case sensitive, so a model property member designated "`Name`" must match a conditional check on "`Name`": +> +> * Correctly matches: `fieldId.FieldName == "Name"` +> * Fails to match: `fieldId.FieldName == "name"` +> * Fails to match: `fieldId.FieldName == "NAME"` +> * Fails to match: `fieldId.FieldName == "nAmE"` -Add an additional property to `ExampleModel`, for example: +Add an additional property to `Model`, for example: ```csharp [StringLength(10, ErrorMessage = "Description is too long.")] -public string Description { get; set; } +public string? Description { get; set; } ``` -Add the `Description` to the `ExampleForm7` component's form: +Add the `Description` to the `CustomValidationForm` component's form: ```razor - + ``` Update the `EditContext` instance in the component's `OnInitialized` method to use the new Field CSS Class Provider: ```csharp -editContext.SetFieldCssClassProvider(new CustomFieldClassProvider2()); +editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2()); ``` Because a CSS validation class isn't applied to the `Description` field, it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error: @@ -2043,7 +3057,38 @@ In the following example: `CustomFieldClassProvider3.cs`: -:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/CustomFieldClassProvider3.cs" highlight="17-24"::: +```csharp +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"; + } + } + } +} +``` + + Update the `EditContext` instance in the component's `OnInitialized` method to use the preceding Field CSS Class Provider: @@ -2080,7 +3125,7 @@ Blazor provides support for validating form input using data annotations with th To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package: ```razor - + ... @@ -2090,8 +3135,6 @@ Annotate model properties with `[ValidateComplexType]`. In the following model c `Starship.cs`: -:::moniker range=">= aspnetcore-5.0" - ```csharp using System; using System.ComponentModel.DataAnnotations; @@ -2107,31 +3150,8 @@ public class Starship } ``` -:::moniker-end - -:::moniker range="< aspnetcore-5.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class Starship -{ - ... - - [ValidateComplexType] - public ShipDescription ShipDescription { get; set; } = new ShipDescription(); - - ... -} -``` - -:::moniker-end - `ShipDescription.cs`: -:::moniker range=">= aspnetcore-6.0" - ```csharp using System; using System.ComponentModel.DataAnnotations; @@ -2148,33 +3168,11 @@ public class ShipDescription } ``` -:::moniker-end - -:::moniker range="< aspnetcore-6.0" - -```csharp -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; } -} -``` - -:::moniker-end - ## Enable the submit button based on form validation To enable and disable the submit button based on form validation, the following example: -* Uses a shortened version of the preceding `Starfleet Starship Database` form (`FormExample2` component) that only accepts a value for the ship's identifier. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created. +* Uses a shortened version of the earlier `Starfleet Starship Database` form (`Starship5` component) of the [Example form](#example-form) section that only accepts a value for the ship's Id. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created. * Uses the form's to assign the model when the component is initialized. * Validates the form in the context's callback to enable and disable the submit button. * Implements and unsubscribes the event handler in the `Dispose` method. For more information, see . @@ -2182,29 +3180,151 @@ To enable and disable the submit button based on form validation, the following > [!NOTE] > When assigning to the , don't also assign an to the . -`Pages/FormExample9.razor`: +`Starship14.razor`: -:::moniker range=">= aspnetcore-7.0" +:::moniker range=">= aspnetcore-8.0" -:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample9.razor"::: +```razor +@page "/starship-14" +@attribute [RenderModeServer] +@implements IDisposable +@inject ILogger Logger -:::moniker-end + + + +
    + +
    +
    + +
    +
    -:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" +@code { + private bool formInvalid = false; + private EditContext? editContext; -:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample9.razor"::: + [SupplyParameterFromForm] + private Starship? Model { get; set; } -:::moniker-end + 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"); + } -:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" + public void Dispose() + { + if (editContext is not null) + { + editContext.OnFieldChanged -= HandleFieldChanged; + } + } +} +``` -:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample9.razor"::: + :::moniker-end -:::moniker range="< aspnetcore-5.0" +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-14" +@implements IDisposable +@inject ILogger Logger + + + + +
    + +
    +
    + +
    +
    + +@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; + } + } +} +``` -:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/forms-and-validation/FormExample9.razor"::: + :::moniker-end @@ -2213,10 +3333,10 @@ If a form isn't preloaded with valid values and you wish to disable the **`Submi A side effect of the preceding approach is that a validation summary ( component) is populated with invalid fields after the user interacts with any one field. Address this scenario in either of the following ways: * Don't use a component on the form. -* Make the component visible when the submit button is selected (for example, in a `HandleValidSubmit` method). +* Make the component visible when the submit button is selected (for example, in a `Submit` method). ```razor - + @@ -2230,7 +3350,7 @@ A side effect of the preceding approach is that a validation summary (`) is used with streaming JS in Add a JavaScript (JS) `getText` function to the app: +:::moniker-end + +:::moniker range=">= aspnetcore-8.0" + + + +> [!NOTE] +> During the .NET 8 preview, add `suppress-error="BL9992"` to `