I love the web and HTML. It’s certainly come a long way since its inception and what it provides as a core experience for web developers. While folks can certainly build HTML-exclusive experiences, adding forms on a page inevitably leads to introducing a backend tech stack. Recently, I’ve been experimenting with Blazor Server-side Rendering (SSR) and how developers can use its component-driven approach while still building the web experience they know and love.
In this post, we’ll see how to use the plain-old form
tag with a Blazor SSR page, handle form posts, and attach
antiforgery tokens.
Why not use EditForm?
Anyone familiar with Blazor would likely immediately think, “Why not use
the EditForm
component?” Well, for my taste, the EditForm
component has so many hooks, fields, and requirements that it begins to
feel like a burden compared to the humble HTML form. In my opinion, much of the EditForm
functionality is overkill for
an SSR scenario.
You’re welcome to use EditForm
if you find its features useful.
HTML Form Blazor Basics
Blazor is a component-driven framework, and even top-level pages are considered components. In a way, it’s simpler to think of each component as a tree of other components, and you have to start somewhere. This approach means that a component page must handle all incoming requests and “route” those requests to the appropriate handlers.
In a Blazor application, there are two levels of “routing”. The first is what you’d consider traditional HTTP path
routing using the @page
directive. The second uses the @formname
attribute on a form
to inject a handler name into
forms, in which you can use that additional information for application logic.
Let’s add a basic form to a page and submit it to a Blazor component. THIS WILL NOT WORK
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
@if (Name is { Length: >0 } name)
{
<h2>@name</h2>
}
else
{
<h2>Welcome to your new app.</h2>
}
<div class="my-2">
<hr>
<form action="" method="post">
<div class="input-group mb-3">
<input type="text" class="form-control"
name="name" placeholder="Say your name...">
<button
id="button-addon2"
class="btn btn-outline-secondary"
type="submit">
Say Hello
</button>
</div>
</form>
</div>
@code
{
[SupplyParameterFromForm] public string? Name { get; set; }
}
As soon as we submit the page, we get the following error.
The POST request does not specify which form is being submitted. To fix this, ensure <form> elements have a @formname attribute with any unique value, or pass a FormName parameter if using <EditForm>.
To fix this, we need to add the @formname
SSR attribute and give it a unique name within the page scope.
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
@if (Name is { Length: >0} name)
{
<h2>Hello, @name!</h2>
}
else
{
<h2>Welcome to your new app.</h2>
}
<div class="my-2">
<hr>
<form action="" method="post" @formname="main">
<div class="input-group mb-3">
<input type="text" class="form-control"
name="name" placeholder="Say your name...">
<button
id="button-addon2"
class="btn btn-outline-secondary"
type="submit">
Say Hello
</button>
</div>
</form>
</div>
@code
{
[SupplyParameterFromForm] public string? Name { get; set; }
}
We still have an issue as you may notice when you submit this form.
A valid antiforgery token was not provided with the request. Add an antiforgery token, or disable antiforgery validation for this endpoint.
Blazor has an AntiforgeryToken
component we forgot to add to the form.
<form action="" method="post" @formname="main">
<div class="input-group mb-3">
<input type="text" class="form-control"
name="name" placeholder="Say your name...">
<button
id="button-addon2"
class="btn btn-outline-secondary"
type="submit">
Say Hello
</button>
</div>
<AntiforgeryToken/>
</form>
Woot! Now it works. If we look at the HTML rendered to the page, you’ll see what Blazor is doing to transform our form into one compatible with the Blazor request pipeline.
<form action="" method="post">
<input type="hidden" name="_handler" value="main"><div class="input-group mb-3"><input type="text" class="form-control" name="name" placeholder="Say your name...">
<button id="button-addon2" class="btn btn-outline-secondary" type="submit">
Say Hello
</button></div>
<input type="hidden" name="__RequestVerificationToken" value="CfDJ8LTlmMRHw3JNmUOvTfhPRjst1GbXskBXT7OtvUmsHbD9sMekv4N4xfoGi1hZlx-XqE_xVTjkPJ2U2T_ZN02Z92RfdhmdofvYJYlPn4QwD_Pno-HJ_z6JkjMTtgOcTkO3q72vEYX_Hl9MaHvju50tTz8"></form>
The hidden
inputs are notable, as they provide values for _handler
and our antiforgery token.
Being Extra with @onsubmit
I hinted at this in the previous section, but Blazor is processing the “HTML” in our components to inject and add input
elements that we didn’t specify. We can take advantage of another attribute, @onsubmit
, to ensure that all submissions
are handled by the appropriate handler on the page.
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
@if (Message is not null)
{
<h2>@Message</h2>
}
else
{
<h2>Welcome to your new app.</h2>
}
<div class="my-2">
<hr>
<form action="" method="post" @formname="main" @onsubmit="Submit">
<div class="input-group mb-3">
<input type="text" class="form-control"
name="name" placeholder="Say your name...">
<button
id="button-addon2"
class="btn btn-outline-secondary"
type="submit">
Say Hello
</button>
</div>
<AntiforgeryToken/>
</form>
</div>
@code
{
[SupplyParameterFromForm(FormName = "main")] public string? Name { get; set; }
string? Message { get; set; }
void Submit()
{
if (!string.IsNullOrWhiteSpace(Name))
{
Message = $"Hello, {Name} to your Blazor App!";
}
}
}
You’ll notice a few new elements in the previous code examples.
- Our form now has an
@onsubmit
attribute. This allows Blazor to execute the method at the time of a request according to the_handler
value passed on a submit. - The
SupplyParameterFromForm
now has aFormName
property to match our form name. This is optional and only necessary when dealing with multiple forms on a single page. - We have a
Submit
method with a bit more complex logic for our component.
Cool! We have a functioning form on a Blazor SSR page and haven’t sacrificed security or HTML readability. We also now understand how to add additional forms and handlers as we expand the page’s functionality.
Conclusion
HTML is great, and using it with ASP.NET Core is pretty great as well. Having Blazor SSR support makes it easy to write performant server-rendered pages while still maintaining a component-driven approach that so many folks like. I hope you found this article helpful. Cheers.