Reputation: 311
I'm working on converting a RazorPages application to Blazor server rendered, the application is really just a UI, all data access/manipulation is done through an API. On CRUD operations the API will return a collection of error objects in a standardized format.
I have code in the RazorPages application to convert the collection of errors into items in the ModelStateDictionary and it works as expected. Messages display, and I can make changes and resubmit the form.
I've added similar code in the Blazor application to add to the EditContext, but I'm struggling to figure out how to clear the validation messages that were added by my extension method. I've been looking at this question and all the solutions suggested, but none seem to work for me at all: How to reset custom validation errors when using editform in blazor razor page
This is the method that actually does the manipulation of the EditContext:
private static void AddErrors(EditContext editContext, Dictionary<string, List<string>> errorDictionary)
{
if (errorDictionary != null && errorDictionary.Any())
{
var validationMessageStore = new ValidationMessageStore(editContext);
foreach (var error in errorDictionary)
{
validationMessageStore.Add(editContext.Field(error.Key), error.Value);
}
editContext.NotifyValidationStateChanged();
}
}
Here's the form definition:
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="loginModel" Context="CurrentEditContext">
<DataAnnotationsValidator />
<SummaryOnlyValidationSummary />
<div class="form-group">
<InputText id="inputUsername" class="form-control" @bind-Value="loginModel.Username" autofocus placeholder="Username" @onkeyup="@(q => ResetValidation(CurrentEditContext))" />
<ValidationMessage For="@(() => loginModel.Username)" />
</div>
<div class="form-group">
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="loginModel.Password" />
<ValidationMessage For="@(() => loginModel.Password)" />
</div>
<div class="form-group">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</div>
</EditForm>
This is the method signature for when the form is submitted, and the editContext passed in appears accurate based on the validation messages it contains (messages from the Required attributes on the model properties are correct):
protected async Task OnSubmit(EditContext editContext)
SummaryOnlyValidationSummary is a custom component that works just like the out of the box ValidationSummary except it excludes any message that is already displayed by the DataAnnotationsValidator
Am I adding the messages the wrong way? If so, is there any good guidelines of where I went wrong? If not, does anyone know how to properly clear or reset the validation messages? I've tried to always submit the form, not only when it's valid, but even forcing editContext.Validate() doesn't clear the validation message my code added. If it helps at all, in ResetValidation I create a new ValidationMessageStore instance off the current editContext, and it doesn't contain any messages (not sure what the expected behavior is though).
Upvotes: 4
Views: 11133
Reputation: 311
Found a solution that works, based on this:
https://remibou.github.io/Using-the-Blazor-form-validation/
I'm still debugging through to figure out why that works when I was doing the same steps, just in a different code structure. Obviously I overlooked something, but it's working now!
edit - took me a while to get caught up on things and come back to update.
Here's the component I made, the parts about parsing from an ApiResult are very specific to my use case, but the rest is pretty generic and could easily be re-used:
public class ServerModelValidator : ComponentBase
{
private ValidationMessageStore _messageStore;
[CascadingParameter] EditContext CurrentEditContext { get; set; }
/// <inheritdoc />
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(ServerModelValidator)} requires a cascading parameter " +
$"of type {nameof(EditContext)}. For example, you can use {nameof(ServerModelValidator)} inside " +
$"an {nameof(EditForm)}.");
}
_messageStore = new ValidationMessageStore(CurrentEditContext);
CurrentEditContext.OnValidationRequested += (s, e) => _messageStore.Clear();
CurrentEditContext.OnFieldChanged += (s, e) => _messageStore.Clear(e.FieldIdentifier);
}
public void DisplayErrors(Dictionary<string, List<string>> errors)
{
foreach (var err in errors)
{
_messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}
public void DisplayApiErrors<T>(ApiResult apiResult) where T : ValidatedModel
{
if (apiResult != null && apiResult.Errors != null && apiResult.Errors.Any())
{
var errorDictionary = new Dictionary<string, List<string>>();
foreach (var error in apiResult.Errors)
{
var errorKeyPair = error.Message.Count(c => c == '\'') == 2
? GetErrorKeyValuePair(error, typeof(T))
: GetErrorKeyValuePair(error);
// build a dictionary of property, list of validation messages
if (!errorDictionary.ContainsKey(errorKeyPair.Key))
{
errorDictionary.Add(errorKeyPair.Key, new List<string>());
}
errorDictionary[errorKeyPair.Key].Add(errorKeyPair.Value);
}
DisplayErrors(errorDictionary);
}
}
public void DisplayError(string field, string validationMessage)
{
var dictionary = new Dictionary<string, List<string>>
{
{ field, new List<string> { validationMessage } }
};
DisplayErrors(dictionary);
}
private KeyValuePair<string, string> GetErrorKeyValuePair(Error error)
{
return new KeyValuePair<string, string>(string.Empty, error.Message);
}
private KeyValuePair<string, string> GetErrorKeyValuePair(Error error, Type validatedModelType)
{
var splitMessage = error.Message.Split('\'', '\'');
// Find a matching property on T and get it's display name
// if no display name use the property name
// if the property wasn't found use the name from the original error message - this shouldn't ever happen unless there is a property mismatch
var properties = validatedModelType.GetProperties();
var matchedProperty = properties?.FirstOrDefault(p => p.Name == splitMessage[1]);
var displayAttribute = matchedProperty?.GetCustomAttributes(false).FirstOrDefault(a => a.GetType() == typeof(DisplayAttribute)) as DisplayAttribute;
var displayName = (displayAttribute?.Name ?? matchedProperty?.Name) ?? splitMessage[1];
var errorMessage = $"The {displayName} field {splitMessage[2]}.";
return new KeyValuePair<string, string>(splitMessage[1], errorMessage);
}
}
It gets placed into any EditForm:
<EditForm class="modal-form" OnValidSubmit="OnSubmit" Model="_season" Context="CurrentEditContext">
<DataAnnotationsValidator />
<ServerModelValidator @ref="_serverModelValidator" />
<SummaryOnlyValidationSummary />
<EditorWrapper For="@(() => _season.Year)">
<InputNumber id="inputName" class="form-control" @bind-Value="_season.Year" autofocus placeholder="@DisplayName.For(() => _season.Year)" />
</EditorWrapper>
<EditorWrapper For="@(() => _season.Name)">
<InputText id="inputName" class="form-control" @bind-Value="_season.Name" placeholder="@DisplayName.For(() => _season.Name)" />
</EditorWrapper>
<EditorWrapper For="@(() => _season.MaxEventsToScore)">
<InputNumber id="inputMaxEventsToScore" class="form-control" @bind-Value="_season.MaxEventsToScore" placeholder="@DisplayName.For(() => _season.MaxEventsToScore)" />
</EditorWrapper>
<SaveButtonComponent IsSaving="_isProcessing" />
</EditForm>
and declared in the @code block:
private ServerModelValidator _serverModelValidator;
and used like this:
ApiDataResult<ApiSeason> result;
if (SeasonId.HasValue)
{
// editing
result = await _apiClient.UpdateSeasonAsync(_season.ToApiModel());
}
else
{
// creating
result = await _apiClient.CreateSeasonAsync(_season.ToApiModel());
}
if (result == null)
{
// add an error
_serverModelValidator.DisplayError(string.Empty, "Unknown error occurred, please try again.");
}
else if (result != null && result.HasErrors)
{
// display errors
_serverModelValidator.DisplayApiErrors<SeasonModel>(result);
}
Upvotes: 4