Putting Blazor Form Features in Context
Question | Answer |
---|---|
What are they? | Blazor provides a set of built-in components that present the user with a form that can be easily validated. |
Why are they useful? | Forms remain one of the core building blocks of web applications, and these components provide functionality that will be required in most projects. |
How are they used? | The EditForm component is used as a parent for individual form field components. |
Are there any pitfalls or limitations? | There can be issues with the way that Entity Framework Core and Blazor work together, and these become especially apparent when using forms. |
Are there any alternatives? | You could create your own form components and validation features, although the features described in this chapter are suitable for most projects and, as I demonstrate, can be easily extended. |
Chapter Summary
Problem | Solution | Listing |
---|---|---|
Creating an HTML form | Use the EditForm and Input* components | 7–9, 13 |
Validating data | Use the standard validation attributes and the events emitted by the EditForm component | 10–12 |
Discarding unsaved data | Explicitly release the data or create new scopes for components | 14–16 |
Avoiding repeatedly querying the database | Manage query execution explicitly | 17–19 |
Preparing for This Chapter
This chapter uses the Advanced project from Chapter 35. To prepare for this chapter, create the Blazor/Forms folder and add to it a Razor Component named EmptyLayout.razor with the content shown in Listing 36-1. I will use this component as the main layout for this chapter.
You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/apress/pro-asp.net-core-3. See Chapter 1 for how to get help if you have problems running the examples.
The Contents of the EmptyLayout.razor File in the Blazor/Forms Folder
The Contents of the FormSpy.razor File in the Blazor/Forms Folder
Next, add a component named Editor.razor to the Blazor/Forms folder and add the content shown in Listing 36-3. This component will be used to edit existing Person objects and to create new ones.
Do not use the Editor and List components in real projects until you have read the rest of the chapter. I have included common pitfalls that I explain later in the chapter.
The Contents of the Editor.razor File in the Blazor/Forms Folder
The component in Listing 36-3 uses an @layout expression to override the default layout and select EmptyLayout. The side-by-side layout is used to present the PersonTable component alongside a placeholder, which is where I will add a form.
The Contents of the List.razor File in the Blazor/Forms Folder
Dropping the Database and Running the Application
Dropping the Database
Running the Example Application
Using the Blazor Form Components
The Bazor Form Components
Name | Description |
---|---|
EditForm | This component renders a form element that is wired up for data validation. |
InputText | This component renders an input element that is bound to a C# string property. |
InputCheckbox | This component renders an input element whose type attribute is checkbox and that is bound to a C# bool property. |
InputDate | This component renders an input element those type attribute is date and that is bound to a C# DateTime or DateTimeOffset property. |
InputNumber | This component renders an input element those type attribute is number and that is bound to a C# int, long, float, double, or decimal value. |
InputTextArea | This component renders a textarea component that is bound to a C# string property. |
Using Form Components in the Editor.razor File in the Blazor/Forms Folder
The EditForm component renders a form element and provides the foundation for the validation features described in the “Validating Form Data” section. The Model attribute is used to provide the EditForm with the object that the form is used to edit and validate.
The components in Table 36-3 whose names begin with Input are used to display an input or textarea element for a single model property. These components define a custom binding named Value that is associated with the model property using the @bind-Value attribute. The property-level components must be matched to the type of the property they present to the user. It is for this reason that I have used the InputText component for the Firstname and Surname properties of the Person class, while the InputNumber component is used for the PersonId and DepartmentId properties. If you use a property-level component with a model property of the wrong type, you will receive an error when the component attempts to parse a value entered into the HTML element.
Creating Custom Form Components
The Contents of the CustomSelect.razor File in the Blazor/Forms Folder
The abstract TryParseValueFromString method has to be implemented so that the base class is able to map between string values used by HTML elements and the corresponding value for the C# model property. I don’t want to implement my custom select element to any specific C# data type, so I have used an @typeparam expression to define a generic type parameter. The Values property is used to receive a dictionary mapping string values that will be displayed to the user and TValue values that will be used as C# values. The method receives two out parameters that are used to set the parsed value and a parser validation error message that will be displayed to the user if there is a problem. Since I am working with generic types, the Parser property receives a function that is invoked to parse a string value into a TValue value.
Using a Custom Form Element in the Editor.razor File in the Blazor/Forms Folder
Validating Form Data
The Blazor Validation Components
Name | Description |
---|---|
DataAnnotationsValidator | This component integrates the validation attributes applied to the model class into the Blazor form features. |
ValidationMessage | This component displays validation error messages for a single property. |
ValidationSummary | This component displays validation error messages for the entire model object. |
The Classes Used by the Blazor Validation Components
Name | Description |
---|---|
validation-errors | The ValidationSummary component generates a ul element that is assigned to this class and is the top-level container for the summary of validation messages. |
validation-message | The ValidationSummary component populates its ul element with li elements assigned to this class for each validation message. The ValidationMessage component renders a div element assigned to this class for its property-level messages. |
The Validation Classes Added to Form Elements
Name | Description |
---|---|
modified | Elements are added to this class once the user has edited the value. |
valid | Elements are added to this class if the value they contain passes validation. |
invalid | Elements are added to this class if the value they contain fails validation. |
The Contents of the blazorValidation.css File in the wwwroot Folder
Applying Validation Components in the Editor.razor File in the Blazor/Forms Folder
The expression defines no parameters and selects the property from the object used for the Model attribute of the EditForm component and not the model type. For this example, this means the expression operates on the PersonData object and not the Person class.
Blazor isn’t always able to determine the type of the property for the ValidationMessage component. If you receive an exception, then you can add a TValue attribute to set the type explicitly. For example, if the type of the property the ValidationMessage represents is long, then add a TValue="long" attribute.
Applying Validation Attributes in the Person.cs File in the Models Folder
Handling Form Events
The EditForm Events
Name | Description |
---|---|
OnValidSubmit | This event is triggered when the form is submitted and the form data passes validation. |
OnInvalidSubmit | This event is triggered when the form is submitted and the form data fails validation. |
OnSubmit | This event is triggered when the form is submitted and before validation is performed. |
Handling EditForm Events in the Editor.razor File in the Blazor/Forms Folder
Using Entity Framework Core with Blazor
The Blazor model changes the way that Entity Framework Core behaves, which can lead to unexpected results if you are used to writing conventional ASP.NET Core applications. In the sections that follow, I explain the issues and how to avoid the problems that can arise.
Understanding the Entity Framework Core Context Scope Issue
In a conventional ASP.NET Core application, written using controllers or Razor Pages, clicking a button triggers a new HTTP request. Each request is handled in isolation, and each request receives its own Entity Framework Core context object, which is configured as a scoped service. The result is that the data created when handling one request affects other requests only once it has been written to the database.
Entity Framework Core is trying to be helpful, and this approach allows complex data operations to be performed over time before being stored (or discarded). Unfortunately, much like the helpful approach Entity Framework Core takes to dealing with related data, which I described in Chapter 35, it presents a pitfall for the unwary developer who expects components to handle data like the rest of ASP.NET Core.
Discarding Unsaved Data Changes
Discarding Unsaved Data Changes in the Editor.razor File in the Blazor/Forms Folder
Creating New Dependency Injection Scopes
You must create new dependency injection scopes if you want to preserve the model used by the rest of ASP.NET Core and have each component receive its own Entity Framework Core context object. This is done by using the @inherits expression to set the base class for the component to OwningComponentBase or OwningComponentBase<T>.
Using a New Scope in the Editor.razor File in the Blazor/Forms Folder
In the listing, I commented out the Inject attribute and set the value of the Context property by obtaining a DataContext service. The Microsoft.Extensions.DependencyInjection namespace contains extension methods that make it easier to obtain services from an IServiceProvider object, as described in Chapter 14.
Changing the base class doesn’t affect services that are received using the Inject attribute, which will still be obtained within the request scope. Each service that you require in the dedicated component’s scope must be obtained through the ScopedServices property, and the Inject attribute should not be applied to that property.
Using the Typed Base Class in the Editor.razor File in the Blazor/Forms Folder
The scoped service is available through a property named Service. In this example, I specified DataContext as the type argument for the base class.
Restart ASP.NET Core, navigate to http://localhost:5000/forms/edit/2, clear the Firstname field, and click the Back button. The changes made by the Editor component are not saved to the database, and since the Editor component’s data context is separate from the one used by the List component, the edited data is discarded, producing the same response as shown in Figure 36-8.
Understanding the Repeated Query Issue
Blazor responds to changes in state as efficiently as possible but still has to render a component’s content to determine the changes that should be sent to the browser.
Adding a Button in the List.razor File in the Blazor/Forms Folder
Each time the component is rendered, Entity Framework Core sends two identical requests to the database, even when the Increment button is clicked where no data operations are performed.
This issue can arise whenever Entity Framework Core is used and is exacerbated by Blazor. Although it is common practice to assign database queries to IEnumerable<T> properties, doing so masks an important aspect of Entity Framework Core, which is that its LINQ expressions are expressions of queries and not results, and each time the property is read, a new query is sent to the database. The value of the People property is read twice by the List component: once by the Count property to determine whether the data has loaded and once by the @foreach expression to generate the rows for the HTML table. When the user clicks the Increment button, Blazor renders the List component again to figure out what has changed, which causes the People property to be read twice more, producing two additional database queries.
Blazor and Entity Framework Core are both working the way they should. Blazor must rerender the component’s output to figure out what HTML changes need to be sent to the browser. It has no way of knowing what effect clicking the button has until after it has rendered the elements and evaluated all the Razor expressions. Entity Framework Core is executing its query each time the property is read, ensuring that the application always has fresh data.
This combination of features presents two issues. The first is that needless queries are sent to the database, which can increase the capacity required by an application (although not always because database servers are adept at handling queries).
The second issue is that changes to the database will be reflected in the content presented to the user after they make an unrelated interaction. If another user adds a Person object to the database, for example, it will appear in the table the next time the user clicks the Increment button. Users expect applications to reflect only their actions, and unexpected changes are confusing and distracting.
Managing Queries in a Component
Controlling Queries in the List.razor File in the Blazor/Forms Folder
The UpdateData method performs the same query but applies the ToListAsync method, which forces evaluation of the Entity Framework Core query. The results are assigned to the People property and can be read repeatedly without triggering additional queries. To give the user control over the data, I added a button that invokes the UpdateData method when it is clicked. Restart ASP.NET Core, request http://localhost:5000/forms, and click the Increment button. Monitor the output from the ASP.NET Core server, and you will see that there is a query made only when the component is initialized. To explicitly trigger a query, click the Update button.
Adding Operations to the List.razor File in the Blazor/Forms Folder
You may encounter an exception telling you that “a second operation started on this context before a previous operation completed.” This happens when a child component uses the OnParametersSetAsync method to perform an asynchronous Entity Framework Core query and a change in the parent’s data triggers a second call to OnParametersSetAsync before the query is complete. The second method call starts a duplicate query that causes the exception. This problem can be resolved by performing the Entity Framework Core query synchronously. You can see an example in Listing 36-12, where I perform queries synchronously because the parent component will trigger an update when it receives its data.
Performing Create, Read, Update, and Delete Operations
To show how the features described in previous sections fit together, I am going to create a simple application that allows the user to perform CRUD operations on Person objects.
Creating the List Component
Preparing the Component in the List.razor File in the Blazor/Forms Folder
The operations for creating, viewing, and editing objects navigate to other URLs, but the delete operations are performed by the List component, taking care to reload the data after the changes have been saved to reflect the change to the user.
Creating the Details Component
The Contents of the Details.razor File in the Blazor/Forms Folder
All the input elements displayed by this component are disabled, which means there is no need to handle events or process user input.
Creating the Editor Component
Adding Application Features in the Editor.razor File in the Forms/Blazor Folder
I added support for a new URL and used Bootstrap CSS themes to differentiate between creating a new object and editing an existing one. I removed the validation summary so that only property-level validation messages are displayed and added support for storing the data through Entity Framework Core. Unlike form applications created using controllers or Razor Pages, I don’t have to deal with model binding because Blazor lets me work directly with the object that Entity Framework Core produces from the initial database query. Restart ASP.NET Core and request http://localhost:5000/forms. You will see the list of Person objects shown in Figure 36-11, and clicking the Create, Details, Edit, and Delete buttons will allow you to work with the data in the database.
Open a command prompt and run dotnet ef database drop --force in the Advanced project folder if you need to reset the database to undo the changes you have made. The database will be seeded again when you restart ASP.NET Core, and you will see the data shown in the figure.
Extending the Blazor Form Features
The EditContext Features
Name | Description |
---|---|
OnFieldChanged | This event is triggered when any of the form fields are modified. |
OnValidationRequested | This event is triggered when validation is required and can be used to create custom validation processes. |
OnValidationStateChanged | This event is triggered when the validation state of the overall form changes. |
Model | This property returns the value passed to the EditForm component’s Model property. |
Field(name) | This method is used to get a FieldIdentifier object that describes a single field. |
IsModified() | This method returns true if any of the form fields have been modified. |
IsModified(field) | This method returns true if the field specified by the FieldIdentifier argument has been modified. |
GetValidationMessages() | This method returns a sequence containing the validation error messages for the entire form. |
GetValidationMessages(field) | This method returns a sequence containing the validation error messages for a single field, using a FieldIdentifer object obtained from the Field method. |
MarkAsUnmodified() | This method marks the form as unmodified. |
MarkAsUnmodified(field) | This method marks a specific field as unmodified, using a FieldIdentifer object obtained from the Field method. |
NotifyValidationStateChanged() | This method is used to indicate a change in validation status. |
NotifyFieldChanged(field) | This method is used to indicate when a field has changed, using a FieldIdentifer object obtained from the Field method. |
Validate() | This method performs validation on the form, returning true if all the form fields pass validation and false otherwise. |
Creating a Custom Validation Constraint
The Contents of the DeptStateValidator.cs File in the Blazor/Forms Folder
This component enforces a restriction on the state in which departments can be defined so that, for example, locations in California are the valid options only when the Development department has been chosen, and any other locations will produce a validation error.
The component has its own scoped DataContext object, which it receives by using OwningComponentBase<T> as its base class. The parent component provides values for the DepartmentId and State properties, which are used to enforce the validation rule. The cascading EditContext property is received from the EditForm component and provides access to the features described in Table 36-8.
The arguments to the Add method are a FieldIdentifier that identifies the field the error relates to and the validation message. If there are no validation errors, then the message store’s Clear method is called, which will ensure that any stale messages that have been previously generated by the component are no longer displayed.
Applying a Validation Component in the Editor.razor File in the Blazor/Forms Folder
Creating a Valid-Only Submit Button Component
The Contents of the ValidButton.razor File in the Forms/Blazor Folder
This component responds to the OnValidationStateChanged method, which is triggered when the validation state of the form changes. There is no EditContext property that details the validation state, so the best way to see if there are any validation issues is to see whether there are any validation messages. If there are, there are validation issues. If there are no validation messages, the form is valid. To ensure the button state is displayed correctly, the Validation method is called so that a validation check is performed as soon as the component is initialized.
Applying a Component in the Editor.razor File in the Blazor/Forms Folder
Summary
In this chapter, I described Blazor form features and showed you how they can be used to create forms that validate data. I also explained how the interactions between Entity Framework Core and Blazor can cause unexpected results and how these can be resolved by creating dependency injection scopes and managing how queries are executed. In the next chapter, I describe Blazor WebAssembly, which executes Razor Components entirely in the browser.