Blazor Series: MudBlazor & Blazor SSR EditForms

Blazor SSR & MudFields: EditContext Issues

Share This Article
This entry is part 1 of 2 in the series MudBlazor

When starting with Blazor Static SSR (Server-Side Rendering), there can be much to wrap your head around. Forms is one of those places where you might get a little stuck. After all, there are many differences when moving from a client-side framework to a server-side framework, and if it’s not something that you’ve studied, then working on it might require you to visit with your ecclesiastical minister to confess new sins that you committed while dealing with the insanity of trying to figure out where you’ve gone wrong.

Don’t worry; through the atonement of Christ, there’s forgiveness, and this article is here to help you solve one of the most common problems developers using third-party controls like MudBlazor run into. The problem of screwing up your form data.

Built-in EditForm Templates

I love that Blazor now ships with an authentication framework that includes the needed components to establish local identity quickly and easily. It’s like someone at Microsoft finally decided to read the developer wish list from their GitHub repo. I’ve spent many nights praying and asking for something just like this.

However, it is worth remembering that it does this with Static SSR and no interactivity, so when you change the look and feel of these pages, you can run into some problems. That’s especially true when you start to use MudBlazor Components.

Let’s examine the Set Password form from the manage pages, which is included in the Identity Framework Template.

<EditForm Model="Input" FormName="set-password" OnValidSubmit="OnValidSubmitAsync" 
          method="post">
  <DataAnnotationsValidator/>
  <ValidationSummary class="text-danger" role="alert"/>
  <div class="form-floating mb-3">
    <InputText type="password" @bind-Value="Input.NewPassword" class="form-control" 
               autocomplete="new-password" placeholder="Please enter your new password."/>
    <label for="new-password" class="form-label">New password</label>
    <ValidationMessage For="() => Input.NewPassword" class="text-danger"/>
  </div>
  <div class="form-floating mb-3">
    <InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" 
               autocomplete="new-password" placeholder="Please confirm your new password."/>
    <label for="confirm-password" class="form-label">Confirm password</label>
    <ValidationMessage For="() => Input.ConfirmPassword" class="text-danger"/>
  </div>
  <button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
</EditForm>

The EditForm uses a model assigned to the code block for this page. That model is given an initial value, as can be seen in this code snippet on line #8:

private string? message;
private ApplicationUser user = default!;

[CascadingParameter] 
private HttpContext HttpContext { get; set; } = default!;

[SupplyParameterFromForm] 
private InputModel Input { get; set; } = new();

protected override async Task OnInitializedAsync()
{
  user = await UserAccessor.GetRequiredUserAsync(HttpContext);

  var hasPassword = await UserManager.HasPasswordAsync(user);
  if (hasPassword)
  {
    RedirectManager.RedirectTo("Account/Manage/ChangePassword");
  }
}

private async Task OnValidSubmitAsync()
{
  var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!);
  if (!addPasswordResult.Succeeded)
  {
    message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}";
    return;
  }

  await SignInManager.RefreshSignInAsync(user);
  RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext);
}

If you’re new to Static SSR, you probably have not seen something new right above that line. It’s the [SupplyParameterFromForm] attribute, but what is it, and what does it do?

The [SupplyParameterFromForm] attribute tells Blazor SSR to pull the value for the Input property from the set-password form. It knows it comes from the set-password form because that is the only form on the page. If other forms were present, you would need to pass the attribute the FormName parameter with the Form’s name. When Blazor SSR constructs the view server-side, it will see if Form-Data has been passed to the request. It will set the property value based on the data received if it has.

Well, that’s nifty, but what happens when we use non-standard components for our input fields?

Field Naming

Well, that depends on how far ahead the component maker has thought. The InputText component with Blazor has a neat little feature: when you specify the binding for the Value, it also sets a name for the field. Let’s take a look at the default form for forgotten passwords:

<EditForm Model="Input" FormName="forgot-password" OnValidSubmit="OnValidSubmitAsync"
          method="post">
  <DataAnnotationsValidator/>
  <ValidationSummary class="text-danger" role="alert"/>

  <div class="form-floating mb-3">
    <InputText @bind-Value="Input.Email" class="form-control" 
               autocomplete="username" aria-required="true" placeholder="name@example.com"/>
    <label for="email" class="form-label">Email</label>
    <ValidationMessage For="() => Input.Email" class="text-danger"/>
  </div>
  <button type="submit" class="w-100 btn btn-lg btn-primary">Reset password</button>
</EditForm>

On line #7, we can see that an InputText element is created. When we render it Server-Side and send it back to the browser, it looks like this:

<form method="post" action="/Account/ForgotPassword">
  <input type="hidden" name="_handler" value="forgot-password">
  <input type="hidden" name="__RequestVerificationToken" 
         value="CfDJ8JoJ-lFcFvhBrufNGTi4YZY6RlStctvKAWap1v05jz_gOUcvjMvVdqx3Cpf4NPpdea2DvYxumzu_e2dFkLDuVWvgfwqUevzinqKx6oWf_RaOyYrCMNLjsB0_ZZmNHQPH_sK6J6F4z7ebBGaKaMJIowA">
   
  <div class="form-floating mb-3">
    <input autocomplete="username" aria-required="true" 
           placeholder="name@example.com" name="Input.Email" 
           class="form-control valid" value="">
    <label for="email" class="form-label">Email</label>
  </div>
  <button type="submit" class="w-100 btn btn-lg btn-primary">Reset password</button>
</form>

On line #8, you can see that the server has taken the value from the binding for our input and added a name attribute. This makes sense because HTML needs a name for the field to send form data back to a server. When working in WASM or a client-side framework, we can pull the value from an input element without this and then send it to the server as JSON, but when sending it as form data, HTML dictates how that data is labeled. Its rules state that every field in form data must have a name attribute to be included.

MudBlazor and Field Naming

MudBlazor was designed for use in the era of .NET 7 and previous versions of Blazor. It was not designed to be used in Blazor Static SSR. Now, that does not mean that we can’t use it; it just means that we need to be aware of the differences provided. One of those differences comes in how it renders its input controls.

Here’s an EditForm using Mud Controls, and it’s taken from the documentation for MudBlazor:

<EditForm Model="@model" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator/>
<MudGrid>
    <MudItem xs="12" sm="7">
        <MudCard>
            <MudCardContent>
                <MudTextField Label="First name" HelperText="Max. 8 characters"
                              @bind-Value="model.Username" For="@(() => model.Username)"/>
                <MudTextField Label="Email" Class="mt-3"
                              @bind-Value="model.Email" For="@(() => model.Email)"/>
                <MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
                              @bind-Value="model.Password" For="@(() => model.Password)" InputType="InputType.Password"/>
                <MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
                              @bind-Value="model.Password2" For="@(() => model.Password2)" InputType="InputType.Password"/>
            </MudCardContent>
            <MudCardActions>
                <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
            </MudCardActions>
        </MudCard>
    </MudItem>
    <MudItem xs="12" sm="5">
        <MudPaper Class="pa-4 mud-height-full">
            <MudText Typo="Typo.subtitle2">Validation Summary</MudText>
            @if (success)
            {
                <MudText Color="Color.Success">Success</MudText>
            }
            else
            {
                <MudText Color="@Color.Error">
                    <ValidationSummary />
                </MudText>
            }
        </MudPaper>
    </MudItem>
    <MudItem xs="12">
        <MudText Typo="Typo.body2" Align="Align.Center">
            Fill out the form correctly to see the success message.
        </MudText>
    </MudItem>
</MudGrid>
</EditForm>

This form uses the MudTextField instead of the InputText control in the stock framework. Let’s take a look at how it gets rendered:

<form>
<div class="mud-grid mud-grid-spacing-xs-3 justify-start">
  <div class="mud-grid-item mud-grid-item-xs-12 mud-grid-item-sm-7">
    <div class="mud-paper mud-elevation-1 mud-card" style="">
      <div class="mud-card-content">
        <div class="mud-input-control mud-input-input-control">
          <div class="mud-input-control-input-container">
            <div class="mud-input mud-input-text mud-input-underline">
              <input class="mud-input-slot mud-input-root mud-input-root-text" 
                     type="text" inputmode="text" maxlength="524288" 
                     aria-invalid="false" _bl_60="">
              <div class="mud-input-slot mud-input-root mud-input-root-text" 
                   style="display:none" tabindex="-1" _bl_61="">
              </div>
            </div>
            <label class="mud-input-label mud-input-label-animated mud-input-label-text 
                          mud-input-label-inputcontrol" 
                   for="mudinput-d66ed60e-7eb7-4c6f-a5e6-17dc715463a9">First name</label>
          </div>
          <div class="mud-input-control-helper-container">
            <p class="mud-input-helper-text">
            <div class="d-flex">
              <div class="me-auto">Max. 8 characters</div>
            </div>
            </p>
        </div>
      </div>
      
                
      <div class="mud-input-control mud-input-input-control mt-3">
        <div class="mud-input-control-input-container">
          <div class="mud-input mud-input-text mud-input-underline">
            <input class="mud-input-slot mud-input-root mud-input-root-text" 
                   type="text" inputmode="text" maxlength="524288" 
                   aria-invalid="false" _bl_62="">
            <div class="mud-input-slot mud-input-root mud-input-root-text" 
                 style="display:none" tabindex="-1" _bl_63="">
            </div>
          </div>
          <label class="mud-input-label mud-input-label-animated 
                        mud-input-label-text mud-input-label-inputcontrol" 
                 for="mudinput-a7f49e8b-d080-448b-83ad-503e2c6fa075">Email</label>
        </div>
      </div>
      <div class="mud-input-control mud-input-input-control mt-3">
        <div class="mud-input-control-input-container">
          <div class="mud-input mud-input-text mud-input-underline">
            <input class="mud-input-slot mud-input-root mud-input-root-text" 
                   type="password" inputmode="text" maxlength="524288" 
                   aria-invalid="false" _bl_64="">
            <div class="mud-input-slot mud-input-root mud-input-root-text" 
                 style="display:none" tabindex="-1" _bl_65="">
            </div>
          </div>
          <label class="mud-input-label mud-input-label-animated 
                        mud-input-label-text mud-input-label-inputcontrol" 
                 for="mudinput-fef3c601-4988-4ba0-8600-f3b098f07b3e">Password</label>
        </div>
        <div class="mud-input-control-helper-container">
          <p class="mud-input-helper-text">
          <div class="d-flex">
            <div class="me-auto">Choose a strong password</div>
          </div>
          </p>
      </div>
    </div>
                    
    <div class="mud-input-control mud-input-input-control mt-3"><div class="mud-input-control-input-container"><!--!--><!--!--><div class="mud-input mud-input-text mud-input-underline"><input class="mud-input-slot mud-input-root mud-input-root-text" type="password" inputmode="text" maxlength="524288" aria-invalid="false" _bl_66=""><div class="mud-input-slot mud-input-root mud-input-root-text" style="display:none" tabindex="-1" _bl_67=""></div></div><!--!--><label class="mud-input-label mud-input-label-animated mud-input-label-text mud-input-label-inputcontrol" for="mudinput-c17113bf-bd0c-4c51-93f4-9303fa6e08ec">Password</label></div><div class="mud-input-control-helper-container"><p class="mud-input-helper-text"><div class="d-flex"><div class="me-auto">Repeat the password</div></div></p></div></div></div><!--!-->
            <!--!--><div class="mud-card-actions"><!--!--><!--!--><button type="submit" class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-button-filled-size-medium mud-ripple ml-auto" _bl_49=""><span class="mud-button-label">Register</span></button></div></div></div><!--!-->
    <!--!--><div class="mud-grid-item mud-grid-item-xs-12 mud-grid-item-sm-5"><!--!--><div class="mud-paper mud-elevation-1 pa-4 mud-height-full" style=""><!--!--><h6 class="mud-typography mud-typography-subtitle2">Validation Summary</h6><!--!--><p class="mud-typography mud-typography-body1 mud-error-text"><!--!--></p></div></div><!--!-->
    <!--!--><div class="mud-grid-item mud-grid-item-xs-12"><!--!--><p class="mud-typography mud-typography-body2 mud-typography-align-center"><!--!-->
            Fill out the form correctly to see the success message.
        </p></div></div></form>

Ok, this doesn’t look bad unless you want to send this form to Blazor Static SSR to be rendered. Then, the HTML Form Rules apply, and as a result, these fields don’t show up in posted form data. Don’t worry, though; Blazor gives us a very useful message to point out our mistake:

EditForm requires either a Model parameter, or an EditContext parameter, please provide one of these.

Now, that’s great, especially when you look and see that you’ve specified the model for the edit form. I spent hours trying to figure out what’s happening here, and it’s not what you might expect. Let me break it down for you.

  1. You bound your form to the model, and as such, when you sent the submission through the browser, it gathered the fields looking for field names based on the name attribute.
  2. It found no fields, so an empty form was returned to the server. Okay, it did include an anti-forgery token and the name of our form, so there’s that.
  3. The server received your empty form, but alas, it could not map it to the object that you specified with the SupplyParameterFromForm attribute.
  4. In retaliation for thoroughly confusing the server, it responded with the above error message, designed to make you spend hours questioning your sanity as you stare longly at your form element, hoping for Jesus to save you from this new hell.

It might not seem fair, but that’s life sometimes. Now that you know the secret about what happened, what can you do about it? Don’t cast out your Mud Components like a catholic priest on a six-week exorcist tour. Instead, pass it the name attribute manually and set the For parameter on the element.

<MudTextField ShrinkLabel="true" Variant="Variant.Outlined"
                                  name="Input.Email" For="()=>Input.Email"
                                  @bind-Value="Input.Email" Label="Email" Required="true"/>

Recompile and run your project and wala! Your form starts working as expected, and the server is no longer confused. Life moves on, and you have a beautiful form built with all the MudBlazor aesthetic magic you have come to know and love.

Series NavigationManaging State with Blazor SSR Interactive Components >>

Leave a Reply

Your email address will not be published. Required fields are marked *