Exploring Blazor Changes in .NET 8 - Capture User Input with Forms
August 22, 2023 · 7 minute read · Tags: blazor
So far in this series we’ve seen how to render Blazor components using Server Side Rendering, and make certain components interactive using Blazor Server or Blazor WASM.
This works very nicely for presenting information via your .NET web app, but what about capturing data from users?
For example, continuing with our product store, sooner or later the customer will want to go ahead and checkout…
How can we capture details like their address and enable them to place an order?
At this point in the web’s evolution you’d be forgiven for thinking this is a JavaScript thing, that you’ve got to take the user’s input and submit it to an API endpoint as JSON.
But there’s another option…
Enter stage left, the humble form.
Forms, the smart choice for capturing data input
The web ran on forms long before JavaScript and SPAs came along to take the limelight, and they’re still a crucial part of most web apps.
With .NET 8 and server side rendering, your components are rendered once on the server, and plain old HTML sent back to the browser.
HTML forms give you a mechanism to go the other way, to take user input and submit it back to your component (where Blazor/ASP.NET can process it and figure out what to do next).
There are two ways to implement this using .NET 8: either using Blazor’s EditForm
or sticking to plain old HTML forms.
In this post we’ll explore the EditForm
option.
EditForms in Blazor are pretty useful, they provide a straightforward way to bind a form to a model, then interact with that model when the user submits the form.
For example, here’s a form for adding a new post to a blog:
<h3>Add new</h3>
<EditForm Model="Command" OnValidSubmit="HandleValidSubmit">
<p>
<label for="title">Title</label>
<InputText id="title" @bind-Value="Command.Title" />
</p>
<p>
<label for="slug">Slug</label>
<InputText id="slug" @bind-Value="Command.Slug" />
</p>
<p>
<InputTextArea @bind-Value="Command.Body" />
</p>
<button type="submit">Submit</button>
</EditForm>
This title
and slug
fields are bound to the underlying Command
model.
As the user interacts with the form this binding ensures Command
is kept in sync with the entered values.
@code {
protected Add.Command Command { get; set; } = new Add.Command();
protected async Task HandleValidSubmit()
{
await Http.PostAsJsonAsync("api/post", Command);
NavigationManager.NavigateTo($"/{Command.Slug}");
}
}
When the user submits the form the HandleValidSubmit
method is invoked.
This takes the data from Command
and processes it accordingly (in this case, posting it as JSON to an endpoint).
Using EditForm with Blazor SSR
So how do we implement checkout using EditForm?
and Blazor SSR?
The first step is to define a model for the form, to capture the entered data.
Here’s a simple model for capturing key checkout details.
public record PlaceOrderCommand
{
public Address BillingAddress { get; set; } = new();
public class Address
{
public string Name { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string City { get; set; }
public string PostCode { get; set; }
}
}
Nothing too complicated. We’re essentially defining a Data Transfer Object (DTO).
Now for the form itself:
Checkout.razor
@page "/Checkout"
@using BlazorDemoApp.Shared.Data
@using BlazorDemoApp.Shared.Checkout
<h3>Checkout</h3>
<EditForm Model="Command" method="post" OnSubmit="SubmitOrder" FormName="checkout">
<div>
<label>Name</label>
<InputText @bind-Value="Command.BillingAddress.Name"/>
</div>
<div>
<label>Address 1</label>
<InputText @bind-Value="Command.BillingAddress.AddressLine1"/>
</div>
<div>
<label>Address 2</label>
<InputText @bind-Value="Command.BillingAddress.AddressLine2"/>
</div>
<div>
<label>City</label>
<InputText @bind-Value="Command.BillingAddress.City"/>
</div>
<div>
<label>Post Code</label>
<InputText @bind-Value="Command.BillingAddress.PostCode"/>
</div>
<button>Place Order</button>
</EditForm>
@if (submitted)
{
<p>Hey, look at that, you placed the order!</p>
}
@code {
[SupplyParameterFromForm]
PlaceOrderModel Model { get; set; } = new();
bool submitted = false;
private void SubmitOrder()
{
submitted = true;
}
}
Here we’ve bound the form to Model
which is an instance of PlaceOrderModel
.
Remember, when we’re rendering components using Server Side Rendering the component is rendered on the server, and the resulting HTML returned to be displayed in the browser.
At this point we have no direct connection between the browser and the server (unlike Blazor Server).
Here’s how it looks in the browser.
<form method="post">
<input type="hidden" name="_handler" value="checkout">
<input type="hidden" name="__RequestVerificationToken" value="CfDJ8E8DIx4kqPVGtpXcVLVj5byAEc7hPtxbLwQMUc2yQrmnKeLNuYFZPRipIibqOGl6_8AsZLANsVZ9yPa2F3-yM3_F0XCwpo56iuTB45-v9RIqsm4JrcUdt-XdciOLTjpNM2u59sG4aR-f2Md5fY-JZJQ">
<div>
<label>Name</label>
<input name="command.BillingAddress.Name" class="valid" value=""></div>
</div>
<!-- other fields -->
<button>Place Order</button>
</form>
When the form is submitted, a standard HTTP POST
request will be made, including the submitted form details
Blazor needs to find a way to take that submitted form data and map it to the relevant component.
Notice how we specified a form name when we defined the EditForm
:
<EditForm Model="Model" method="post" OnSubmit="SubmitOrder" FormName="checkout">
...
</EditForm>
This helps Blazor direct the incoming POST
to the correct component where it will then use model binding to bind the incoming data to the Model
property.
To make that model binding work we just need to decorate the relevant property with the [SupplyParameterFromForm]
attribute.
[SupplyParameterFromForm]
PlaceOrderModel Model { get; set; } = new();
Now when the form is submitted the SubmitOrder
method will be invoked and the the incoming form values will be available via the Model
parameter.
Pre-populate the form with existing data
In reality we probably want to pre-populate our checkout form with existing data (items added to the customer’s basket, saved addresses etc.)
Let’s update our component to fetch this data from our backend service/database and use it to populate the form.
@page "/Checkout"
@inject IProductStore Store
<!-- Form (as before) -->
@code {
[SupplyParameterFromForm]
public PlaceOrderCommand? Command { get; set; }
protected override void OnInitialized()
{
Command ??= Store.GetCheckout();
}
bool submitted = false;
private void SubmitOrder()
{
submitted = true;
}
}
With this we’ll fetch an instance of the PlaceOrderCommand
from our IProductStore
implementation, but only if Command
is null,
Here’s the flow:
- On first load
Command
is null so… - Fetch the data from the backend
- User submits the form (thereby posting the submitted values)
- The posted form data is routed to the
SubmitOrder
handler in our component, which… - Maps the incoming data to the
Command
field - Because
Command
field is no longer null, the check inOnInitialized
will leave it alone
To wrap this up, here’s a version of the form complete with pre-populated data plus a handy little summary which the customer will see when the form is submitted.
@page "/Checkout"
@using BlazorDemoApp.Shared.Checkout
@using BlazorDemoApp.Shared.Data
@inject IProductStore Store
<h3>Checkout</h3>
@if (Command != null)
{
<EditForm Model="Command" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
<DataAnnotationsValidator/>
<h4>Ship To:</h4>
<div>
<label>Name</label>
<InputText @bind-Value="Command.BillingAddress.Name"/>
</div>
<div>
<label>Address 1</label>
<InputText @bind-Value="Command.BillingAddress.AddressLine1"/>
</div>
<div>
<label>Address 2</label>
<InputText @bind-Value="Command.BillingAddress.AddressLine2"/>
</div>
<div>
<label>City</label>
<InputText @bind-Value="Command.BillingAddress.City"/>
</div>
<div>
<label>Post Code</label>
<InputText @bind-Value="Command.BillingAddress.PostCode"/>
</div>
<button type="submit">Place Order</button>
<ValidationSummary/>
</EditForm>
}
@if (submitted)
{
<div class="orderSummary">
<p>Hey, look at that, you placed the order!</p>
<h2>Order Summary</h2>
<h3>Shipping To:</h3>
<dl>
<dt>Name</dt>
<dd>@Command.BillingAddress.Name</dd>
<dt>Address 1</dt>
<dd>@Command.BillingAddress.AddressLine1</dd>
<dt>Address 2</dt>
<dd>@Command.BillingAddress.AddressLine2</dd>
<dt>City</dt>
<dd>@Command.BillingAddress.City</dd>
<dt>Post Code</dt>
<dd>@Command.BillingAddress.PostCode</dd>
</dl>
</div>
}
@code {
[SupplyParameterFromForm]
public PlaceOrderCommand? Command { get; set; }
protected override void OnInitialized()
{
Command ??= Store.GetCheckout();
}
bool submitted = false;
private void SubmitOrder()
{
submitted = true;
}
}
In Summary
With Server Side Rendering of Razor components in .NET 8 you have a simpler way to render HTML and make it show up in the browser.
With forms you can go the other way and take user input to be processed on the server.
Blazor’s existing EditForm
component works with SSR to route posted form data to your Razor components.
Just remember to name each form (the name must be unique), and use the [SupplyParameterFromForm]
to bind incoming form data to your model.
All posts in the
NET 8 Blazor Evolved series.
- Exploring Blazor Changes in .NET 8 - Server Side Rendering (SSR)
- Exploring Blazor Changes in .NET 8 - Interactive Components using Blazor Server
- Exploring Blazor Changes in .NET 8 - Interactive Components using Blazor WASM
- Exploring Blazor Changes in .NET 8 - Capture User Input with Forms
- Exploring Blazor Changes in .NET 8 - Auto Render Mode