Context
I have a simple Blazor server side form, which I use for account registration / sign up on a public web site.
The user is asked to input first name, last name, email, username, and password. I use Data Annotations to validate simple constraints such as MinLength, MaxLength, etc, which works fine.
The problem
I'm struggling to find a way of asynchronously validating that the username and email doesn't already exist.For instance, after the user has entered their email address and tabbed to the next field, I would like to trigger an asynchronous process to check if an account with such an email already exists and then display a validation error using the exisiting data validation summary component to avoid writing too much custom logic. If the account username or email address is already taken, it should block the user from going any further and provide an easy-to-understand error message next to the email and username TextInputs.
Current approach (which doesn't seem to work)
I followed a guide in the Blazor documentation, which suggests using EditContext.OnFieldChanged. The function EditContext_OnFieldChanged is indeed invoked when I enter a value in the email field. I can then add manual validation messages to the messageStore; however, it doesn't trigger the form from not validating.
Question
It seems quite cumbersome for something that happens so often on public web sites. Do any of you know a better / correct way of doing this?
Current Blazor form code:
@page "/register"
@using [XXX].Data
@inject AuthenticationService websiteAuthenticator
@inject NavigationManager navManager;
<h3>Register new account</h3>
<AuthorizeView>
<Authorized>
<div Context="Rubbish"><h1>Have to redirect somehow...</h1></div>
</Authorized>
<NotAuthorized>
<EditForm Context="Registration" EditContext="@EditContext" class="ui form" OnValidSubmit="@TryRegister">
<h4 class="ui dividing header">New user account</h4>
<DataAnnotationsValidator />
<ValidationSummary />
<div class="field">
<label>About you</label>
<small>Providing your name is optional. Your name or email will not be visible to other users.</small>
<div class="two fields">
<div class="field">
<InputText type="text" @bind-Value="RegisterFormModel.FirstName" id="firstname" placeholder="First name"></InputText>
</div>
<div class="field">
<InputText type="text" @bind-Value="RegisterFormModel.LastName" id="lastname" placeholder="Last name"></InputText>
</div>
</div>
</div>
<div class="field">
<label>Username</label>
<small>Choose your username wisely. The username will be visible to other users on the site.</small>
<InputText type="text" @bind-Value="RegisterFormModel.Username" id="username"></InputText>
<ValidationMessage For="() => RegisterFormModel.Username" />
</div>
<div class="field">
<label>Email</label>
<small>You must provide a working email address to register. Your email address will not be visible to other users. We will not share your email address with third parties.</small>
<InputText type="email" @bind-Value="RegisterFormModel.Email" id="email"></InputText>
<ValidationMessage For="() => RegisterFormModel.Email" />
</div>
<div class="field">
<label>Password</label>
<small>Passwords must be at least 8 characters long. Pick a secure one.</small>
<InputText type="password" @bind-Value="RegisterFormModel.Password" id="password"></InputText>
<ValidationMessage For="() => RegisterFormModel.Password" />
</div>
<div class="ui segment">
<div class="field">
<div class="ui toggle checkbox">
<InputCheckbox id="rememberMe" @bind-Value="RegisterFormModel.Above18" />
<label>
I am aged 18 or above
</label>
</div>
</div>
</div>
<button type="submit" class="ui button" disabled="@(!RegisterFormModel.Above18)">Register me</button>
</EditForm>
<XX.Shared.SemanticUI.Confirmation Show=@ShowConfirmation Header="Account created">
Your account has been registered, but needs to be activated. Please check your inbox for an email with instructions on how to activate the account and log in.
</XX.Shared.SemanticUI.Confirmation>
<XX.Shared.SemanticUI.Warning Show=@ShowError Header="Registration error">
An error occurred during registration of the account. Please contact us via <a href="/support">the support page.</a>
</XX.Shared.SemanticUI.Warning>
</NotAuthorized>
</AuthorizeView>
@code {
private EditContext? EditContext { get; set; }
private ValidationMessageStore? messageStore;
protected RegisterFormModel RegisterFormModel { get; set; } = new();
protected bool ShowError { get; set; } = false;
protected bool ShowConfirmation { get; set; } = false;
protected override void OnInitialized()
{
EditContext = new EditContext(RegisterFormModel);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
base.OnInitialized();
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
messageStore?.Clear();
var fieldname = e.FieldIdentifier.FieldName;
if (fieldname.ToLower().Equals("email"))
{
if (RegisterFormModel.Email.StartsWith("[TO BE REPLACED BY DATABASE LOOKUP LOGIC"))
{
messageStore?.Add(() => RegisterFormModel.Email, "Account already registered with email address");
}
}
public void Dispose()
{
if (EditContext is not null)
{
EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}
protected async void TryRegister()
{
var u = await websiteAuthenticator.CreateUserAsync(RegisterFormModel);
if (u.Item2.Succeeded)
{
ShowConfirmation = u.Item2.Succeeded;
StateHasChanged();
}
else
{
ShowError = true;
StateHasChanged();
}
}
}
Code below for RegisterFormModel
public class RegisterFormModel
{
[Required(ErrorMessage = "You must provide a user name")]
[MinLength(3, ErrorMessage = "User name must have a minimum of 3 characters")]
[RegularExpression(@"^\S*$", ErrorMessage = "No white space allowed in user name")]
[Display(Name = "User name")]
public string Username { get; set; }
[Display(Name = "First name")]
public string FirstName { get; set; } = string.Empty;
[Display(Name = "Last name")]
public string LastName { get; set; } = string.Empty;
[Required(ErrorMessage = "You must provide an email address to register.")]
[MinLength(3)]
[EmailAddress(ErrorMessage = "Invalid email address")]
[Display(Name = "Email address")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Required]
public bool Above18 { get; set; }
}