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

img.png

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 in OnInitialized 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.

Next up

Finally! Improved Blazor Server reconnection UX
.NET 9 changes how your Blazor Server app behaves when server connection is lost
.NET 9 improves JavaScript module importing for Blazor
.NET 9 ensures your users always get the latest version of your JS modules
How to use .NET 9 to ensure users always get the latest version of your stylesheets
.NET 9 changes how static files are served, and it solves a long-standing problem