Home > Blockchain >  Handling Errors inside OnValidSubmit, After Form Validation
Handling Errors inside OnValidSubmit, After Form Validation

Time:11-05

I am creating a custom registration form for my ASP.NET Blazor Server Application. I've got all the validation working like I want, including validating a date using a custom validation attribute, so I am familiar with that process.

The part I am getting stuck on is the uniqueness of the email address/username. The ASP.NET identity framework returns a succeeded property that is false if there is already a user with the provided email/username. There's even a helpful description. Unfortunately this is after the form validation and Blazor has already decided the form is valid.

var result = await userManager.CreateAsync(user, _tempPassword);
result.Succeeded = false
result.Errors.Count = 1
result.Errors[0].Description = "Username 'XXX' is already taken."

It seems crazy to me that I have to create a complex, custom validation attribute that includes dependency injection (for the identity and/or main databases) when the information is readily available "on the page".

Below is my Blazor page. See the else statement in the HandleValidSubmit method.

@page "/Employee/Create"

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Identity
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> userManager
@inject NavigationManager NavigationManager
@inject IDataAccess database

<AuthorizeView Roles="Admin" Context="authContext">
    <Authorized>
        <h3>New Employee</h3>
        <EditForm Model="_employee" Context="formContext" OnValidSubmit="@HandleValidSubmit">
            <DataAnnotationsValidator />
            <ValidationSummary />
            <p>First Name: <InputText id="firstName"  @bind-Value="_employee.FirstName" /></p>
            <p>Last Name: <InputText id="lastName"  @bind-Value="_employee.LastName" /></p>
            <p>Email: <InputText id="email"  @bind-Value="_employee.Email" /></p>

            <div id="newEmployeeButtons">
                <button type="submit" >Save</button>
            </div>
        </EditForm>
    </Authorized>
</AuthorizeView>

@code {
    private Employee _employee = new();

    protected override void OnInitialized()
    {
        _employee = new Employee();
    }

    private async Task HandleValidSubmit()
    {
        ApplicationUser user = new()
        {
            FirstName = _employee.FirstName,
            LastName = _employee.LastName,
            Email = _employee.Email
        };

        var result = await userManager.CreateAsync(user, _tempPassword);
        if (result.Succeeded)
        {
            // Save to main DB
            _employee.Id = await userManager.GetUserIdAsync(user);
            await database.CreateEmployeeAsync(_employee);
            _employee = new(); // Clear form
        }
        else
        {
            // Microsoft.AspNetCore.Identity.IdentityResult returns an error here,
            // NOT an exception. The error has a description of "Username 'XXX'
            // is already taken."
            // HOW DO I TURN THIS INTO A VALIDATION MESSAGE ON THE PAGE?!
            // Currently it just fails silently
        }
    }
}

EDIT 2:

I decided that rather than posting code segments here, I would push a minimal reproducible example to GitHub.

The repository is located here

Branch: master

As previously stated, the EditContext_OnFieldChanged method is never raised. I set a break point at the beginning of the method, but the break point is never hit. Since I set emailExists = true, the validation should have always failed (for testing purposes).

Branch: noModelAttribute

Removes the Model attribute from the EditForm component, but only throws an exception when navigating to the /Employee/Create page.

Final Edit

The real issue seems to have been the declaration in the OnInitialized method. Using _editContext = new(_employee); works while EditContext _editContext = new(_employee); resulted in an exception.

CodePudding user response:

Here's one way to do that...

  <EditForm EditContext="EditContext" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
            <p>First Name: <InputText id="firstName"  @bind-Value="_employee.FirstName" /></p>
            <p>Last Name: <InputText id="lastName"  @bind-Value="_employee.LastName" /></p>
            <p>Email: <InputText id="email"  @bind-Value="_employee.Email" /></p>

            <div id="newEmployeeButtons">
                <button type="submit" >Save</button>
            </div>
</EditForm>

@code {
    private Employee _employee = new();
    private EditContext EditContext;
    ValidationMessageStore messages;

    protected override void OnInitialized()
    {
        EditContext = new EditContext(_employee);

        EditContext.OnFieldChanged  = EditContext_OnFieldChanged;

        messages = new ValidationMessageStore(EditContext);

    }
   

    private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
    {
            if (e.FieldIdentifier.FieldName == nameof(_employee.Email))
            {
               // Perform a database call to verify if the user 
               // name exists in the database
                var _emailExists = MyService.EmailExists( 
                                     _employee.Email);
                
 
                if (_emailExists)
                {
                    messages.Clear(e.FieldIdentifier);
                    messages.Add(e.FieldIdentifier, "is already taken.");
                
                }
                else
                {
                    messages.Clear(e.FieldIdentifier);

                }
            }
            EditContext.NotifyValidationStateChanged();

    
     //   var isValid = EditContext.Validate();


     //   if (isValid)
      //  {
      //      this.HandleValidSubmit();
     //   }

        InvokeAsync(() => StateHasChanged());
    }


   
}

Note: You should define the Exists method in a service. You must not inject the UserManager service into your components.

I wrote this code quick, but I hope you've got the point...

Update:

    @page "/Employee/Create"
    @using BlazorValidation.Data
    
    <h3>New Employee</h3>
<EditForm EditContext="_editContext" Context="formContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <p>First Name: <InputText id="firstName"  @bind-Value="_employee.FirstName" /></p>
    <p>Last Name: <InputText id="lastName"  @bind-Value="_employee.LastName" /></p>
    <p>Email: <InputText id="email"  @bind-Value="_employee.Email" /></p>

    <div id="newEmployeeButtons">
        <button type="submit" >Save</button>
    </div>
</EditForm>

@code {
    private Employee _employee = new();

    private EditContext _editContext;
    private ValidationMessageStore messages;

    protected override void OnInitialized()
    {
        _editContext = new(_employee);
        _editContext.OnFieldChanged  = EditContext_OnFieldChanged;
        messages = new ValidationMessageStore(_editContext);
    }

    private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs args)
    {
        if (args.FieldIdentifier.FieldName == nameof(_employee.Email))
        {
            bool emailExists = true;

            if (emailExists)
            {
                messages.Clear(args.FieldIdentifier);
                messages.Add(args.FieldIdentifier, "A user with this email address already exists.");
            }
            else
            {
                messages.Clear(args.FieldIdentifier);
            }
        }
        _editContext.NotifyValidationStateChanged();

       InvokeAsync(() => StateHasChanged());
    }

    private async Task HandleValidSubmit()
    {
        Console.WriteLine("Submitting...");
       await Task.CompletedTask;
    }

    public class Employee
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }
}
  • Related