Search code examples
c#blazor.net-8.0

Blazor Server EditForm Model Initialize Net 8


With a background a long time ago in Razor MVC 5 I'm playing around with Blazor after using Angular 12+ for the recent years.

I'm struggling with EditForm Submit - only a simple application but it isn't behaving as I expected and I'm wondering what OnInitialized is doing.

The CreateUserDto I generated through NSwag and all of the fields have the required attribute.

The form that is being edited only contains four of the six properties, so they are not getting bound to within the EditForm.

Instead, as I don't want the user to have control over these, I initialised the properties in the OnInitialize() override method.

However when I populate the fields within the form and submit, I get errors saying that Username and Role are required.

If I remove OnValidSubmit and just use Submit, I can see that there properties are null, but I am unsure why.

Any help would be great, thanks.

Code is below:

@page "/users/register"
@inject IClient HttpClient
@inject NavigationManager _navManager;

<h3>Register New User</h3>
@if (!string.IsNullOrEmpty(message))
{
    <div class="alert-danger">
        <p>@message</p>
    </div>
}

<div class="card-body">
    <EditForm EditContext="_editContext" OnValidSubmit="HandleRegistration" FormName="Register">
        @* Enforces validation *@
        <DataAnnotationsValidator></DataAnnotationsValidator>
        @* Prints out anything thats invalid *@
        <ValidationSummary></ValidationSummary>
        <div class="form-group">
            <label>Email Address</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.Email"></InputText>
            <ValidationMessage For="() => _registrationModel.Email"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>First Name</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.FirstName"></InputText>
            <ValidationMessage For="() => _registrationModel.FirstName"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>Last Name</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.LastName"></InputText>
            <ValidationMessage For="() => _registrationModel.LastName"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>Password</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.Password"></InputText>
            <ValidationMessage For="() => _registrationModel.Password"></ValidationMessage>
        </div>
        
        <button type="submit" class="btn btn-primary btn-block" >Register</button>
    </EditForm>
</div>


@code {

    private EditContext? _editContext;
    [SupplyParameterFromForm]
    public CreateUserDto? _registrationModel { get; set; }

    protected override async Task OnInitializedAsync()
    {
        _registrationModel ??= new CreateUserDto
        {
            Username = string.Empty,
            Role = "User"
        };
        _editContext = new EditContext(_registrationModel);
    }
    
    string message = String.Empty;
    
    private async Task HandleRegistration()
    {
        if (_registrationModel != null)
        {
            _registrationModel.Username = _registrationModel.Email;
            _registrationModel.Role = "User";
        }

        try
        {
            await HttpClient.RegisterAsync(_registrationModel);
            _navManager.NavigateTo("'/users/login");
        }
        catch (ApiException ae)
        {
            message = ae.Message;
        }
        catch (Exception e)
        {
            message = e.Message;
        }
    }
}

Solution

  • As you have [SupplyParameterFromForm] I'm assuming this is a statically rendered page.

    The key point to understand is that this page exists in two contexts. The model is built in OnInitializedAsync in the Http Request context. The page is then returned to the browser as a static form. You interact with it in a static browser context.

    When you submit the form it builds a CreateUserDto from the form and posts it back to the server as the form data. Any data not in the form is not submitted. The Http Request context that handles the posted form does the validation and calls the correct valid/not valid hanlder.

    Here's my MRE working version of your form where there's validation on the fields the user doesn't complete. They are hidden fields within the form. Note string.Empty satifies [Required]

    @page "/"
    @using System.ComponentModel;
    @using System.ComponentModel.DataAnnotations;
    
    <PageTitle>Home</PageTitle>
    
    <h1>Hello, world!</h1>
    
    <EditForm EditContext="_editContext" OnValidSubmit="HandleRegistration" OnInvalidSubmit="HandleValidation" FormName="Home">
        @* Enforces validation *@
        <DataAnnotationsValidator></DataAnnotationsValidator>
        @* Prints out anything thats invalid *@
        <ValidationSummary></ValidationSummary>
        <div class="form-group">
            <label>Email Address</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.Email"></InputText>
            <ValidationMessage For="() => _registrationModel.Email"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>First Name</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.FirstName"></InputText>
            <ValidationMessage For="() => _registrationModel.FirstName"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>Last Name</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.LastName"></InputText>
            <ValidationMessage For="() => _registrationModel.LastName"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>Password</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.Password"></InputText>
            <ValidationMessage For="() => _registrationModel.Password"></ValidationMessage>
        </div>
    
        <InputText hidden @bind-Value="_registrationModel!.Username"></InputText>
        <InputText hidden @bind-Value="_registrationModel!.Role"></InputText>
    
        <button type="submit" class="btn btn-primary btn-block">Register</button>
    </EditForm>
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>First Name: @_registrationModel!.FirstName</pre>
        <pre>Last Name: @_registrationModel.LastName</pre>
        <pre>Password: @_registrationModel.Password</pre>
        <pre>Role: @_registrationModel.Role</pre>
        <pre>Email: @_registrationModel.Email</pre>
        <pre>Username: @_registrationModel.Username</pre>
    
    </div>
    
    @code {
        private EditContext? _editContext;
    
        [SupplyParameterFromForm] public CreateUserDto? _registrationModel { get; set; }
    
        protected override Task OnInitializedAsync()
        {
            _registrationModel ??= new CreateUserDto
                {
                    Username = string.Empty,
                    Role = "User"
                };
    
            _editContext = new EditContext(_registrationModel);
    
            return Task.CompletedTask;
        }
    
        private async Task HandleValidation()
        {
            await Task.Yield();
        }
    
        private async Task HandleRegistration()
        {
            if (_registrationModel != null)
            {
                _registrationModel.Username = _registrationModel.Email;
                _registrationModel.Role = "User";
            }
    
            await Task.Yield();
        }
    
        public class CreateUserDto
        {
            [Required] public string? FirstName { get; set; }
            [Required] public string? LastName { get; set; }
            [Required] public string? Email { get; set; }
            [Required] public string? Password { get; set; }
            [Required] public string? Username { get; set; }
            [Required] public string? Role { get; set; }
        }
    }
    

    Alternatively you can remove the validation from the fields you set in the form handler.

    @page "/"
    @using System.ComponentModel;
    @using System.ComponentModel.DataAnnotations;
    
    <PageTitle>Home</PageTitle>
    
    <h1>Hello, world!</h1>
    
    <EditForm EditContext="_editContext" OnValidSubmit="HandleRegistration" OnInvalidSubmit="HandleValidation" FormName="Home">
        @* Enforces validation *@
        <DataAnnotationsValidator></DataAnnotationsValidator>
        @* Prints out anything thats invalid *@
        <ValidationSummary></ValidationSummary>
        <div class="form-group">
            <label>Email Address</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.Email"></InputText>
            <ValidationMessage For="() => _registrationModel.Email"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>First Name</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.FirstName"></InputText>
            <ValidationMessage For="() => _registrationModel.FirstName"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>Last Name</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.LastName"></InputText>
            <ValidationMessage For="() => _registrationModel.LastName"></ValidationMessage>
        </div>
        <div class="form-group">
            <label>Password</label>
            <InputText class="form-control" @bind-Value="_registrationModel!.Password"></InputText>
            <ValidationMessage For="() => _registrationModel.Password"></ValidationMessage>
        </div>
        <button type="submit" class="btn btn-primary btn-block">Register</button>
    </EditForm>
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>First Name: @_registrationModel!.FirstName</pre>
        <pre>Last Name: @_registrationModel.LastName</pre>
        <pre>Password: @_registrationModel.Password</pre>
        <pre>Role: @_registrationModel.Role</pre>
        <pre>Email: @_registrationModel.Email</pre>
        <pre>Username: @_registrationModel.Username</pre>
    
    </div>
    
    @code {
        private EditContext? _editContext;
    
        [SupplyParameterFromForm] public CreateUserDto? _registrationModel { get; set; }
    
        protected override Task OnInitializedAsync()
        {
            _registrationModel ??= new CreateUserDto
                {
                    Username = string.Empty,
                    Role = "User"
                };
    
            _editContext = new EditContext(_registrationModel);
    
            return Task.CompletedTask;
        }
    
        private async Task HandleValidation()
        {
            await Task.Yield();
        }
    
        private async Task HandleRegistration()
        {
            if (_registrationModel != null)
            {
                _registrationModel.Username = _registrationModel.Email;
                _registrationModel.Role = "User";
            }
    
            await Task.Yield();
        }
    
        public class CreateUserDto
        {
            [Required] public string? FirstName { get; set; }
            [Required] public string? LastName { get; set; }
            [Required] public string? Email { get; set; }
            [Required] public string? Password { get; set; }
            public string? Username { get; set; }
            public string? Role { get; set; }
        }
    }