Open JasonRodman opened 4 years ago
In the last preview, I enabled setting the Status
on a Validation
component. In this way, you might be able to define the initial state of validation. eg.
<Validation Status="@resultFromYourApi">
..
</Validation>
It's the closest to your scenario I can think of.
@stsrki That seems useful to light a field red, but I actually need a way to insert a message into the feedback for a field. Maybe I could propose a new feature that allows you to specify a validation error for specific field by name? Something along the lines of this?
Validations.AddModelError("FirstName", "Some validation message...");
This would look similar to the AddModelError method on the ModelState class which exists for this very reason.
Have you finally found a solution to this problem?
You could always inherit from the Validations
or Validation
component and implement the AddModelError.
@JasonRodman
It's probably too late by now. But I think you can do this with the ValidationSummary
component. https://blazorise.com/docs/components/validation/#validation-summary
You can inject custom messages by using the Errors parameter
<ValidationSummary Errors="@stringArrayOfMessages" />
Thank you for your answer. ValidationSummary is not what I want to do. But I found another way to be able to do it more elegant, inspired by the code provided by Microsoft, by deriving from DataAnnotationsValidator component, I can add custom errors in the context.
Cool. Any chance you could share some parts of your code?
Edit* we might add it as part of community articles.
Of course! That's why we're here.
/// <summary>
/// Adds <see cref="IOperationResult"/> Data Annotations validation support to an <see cref="EditContext"/>.
/// </summary>
public class DataAnnotationsValidatorExtended : DataAnnotationsValidator
{
private ValidationMessageStore _validationMessageStore = default!;
[CascadingParameter]
private EditContext AnnotationEditContext { get; set; } = default!;
///<inheritdoc/>
protected override void OnInitialized()
{
base.OnInitialized();
_validationMessageStore = new(AnnotationEditContext);
AnnotationEditContext.OnValidationRequested += (s, e) => _validationMessageStore.Clear();
AnnotationEditContext.OnFieldChanged += (s, e) => _validationMessageStore.Clear(e.FieldIdentifier);
}
/// <summary>
/// Applies <see cref="IOperationResult"/> errors to the context.
/// </summary>
/// <param name="result">The operation result to act with.</param>
/// <remarks>Only available for failed result.</remarks>
/// <exception cref="ArgumentNullException">The <paramref name="result"/> is null.</exception>
public virtual void ValidateModel(IOperationResult result)
{
_ = result ?? throw new ArgumentNullException(nameof(result));
if (result.Succeeded)
return;
foreach (var error in result.Errors)
{
_validationMessageStore.Add(AnnotationEditContext.Field(error.Key), error.ErrorMessages);
}
AnnotationEditContext.NotifyValidationStateChanged();
}
/// <summary>
/// Removes the notifications from the context and notify changes.
/// </summary>
public virtual void ClearModel()
{
_validationMessageStore.Clear();
AnnotationEditContext.NotifyValidationStateChanged();
}
}
IOperationResult represents the status of an operation. The result contains "Succeeded" and "Failed" which determines operation exit state, "StatusCode" that returns the HTTP status code and "Errors" which shows errors for failing operation execution.
You can replace IOperationResult with IDictionary<string, string[]> :
public virtual void ValidateModel(IDictionary<string, string[]> errors)
{
_ = errors ?? throw new ArgumentNullException(nameof(errors));
foreach (var error in errors)
{
_validationMessageStore.Add(AnnotationEditContext.Field(error.Key), error.Value);
}
AnnotationEditContext.NotifyValidationStateChanged();
}
And in your razor component SignIn.razor :
<EditForm Model="@model" OnValidSubmit="SignInSubmitAsync">
<DataAnnotationsValidatorExtended @ref="@Validator" />
<div class="form-group">
<label for="Email" class="col-md-12 col-form-label">Email</label>
<div class="col-md-12">
<InputTextOnInput @bind-Value="model.Email" type="email" class="form-control" />
<ValidationMessage For="@(() => model.Email)" />
</div>
</div>
...
</EditForm>
InputTextOnInput.razor An input component for editing "string" values on input key and also use the css style is-valid and is-invalid.
In the code behind :
public partial class SignIn
{
private readonly SignInModel model = new();
...
protected DataAnnotationsValidatorExtended Validator { get; set; } = default!;
[Inject] // For demo
protected IAuthenticationService AuthenticationService { get; set; } = default!;
protected async Task SignInSubmitAsync()
{
...
var authResponse = await AuthenticationService.SignInAsync(...);
// AuthenticationService returns an IOperationResult
// You can use ValidationProblemDetails as specified in the first message.
if (authResponse.Failed)
{
// Add errors to the context and notifies the component that its state has changed.
Validator.ValidateModel(authResponse);
}
...
}
}
Excellent work. Thank you!
I was just looking for how to display web api validation error results in a Blazorise form. Looks like the code shared is not meant to work with Blazorise, but with the raw Blazor form component. Any chance this could be worked into Blazorise form validation component?
You could always inherit from the
Validations
orValidation
component and implement the AddModelError.
Hi, I have the same problem, I managed to resolve it by adding this code to Validations.razor.cs:
public void ProcessErrors( IDictionary<string, string[]> errors )
{
foreach ( var error in errors )
{
var validator = this.validations.Where( x => x.FieldIdentifier.FieldName == error.Key ).FirstOrDefault();
if ( validator != null )
{
validator.NotifyValidationStatusChanged( ValidationStatus.Error, error.Value );
}
else
{
RaiseStatusChanged( ValidationStatus.Error, error.Value );
}
}
}
The problem is I cannot use inherited component, as lots of members are private or internal, so I had to actually include the whole modified blazorise into my solution to make it work. How would you suggest to resolve this issue?
@simelis You can register your custom component with a Dependency Injection and it will be picked by the framework
eg.
serviceCollection.AddTransient( typeof( Blazorise.Validation ), typeof( YourCustomValidation ) );
@stsrki Unfortunatelly, I cannot get it to work. Are you sure the registration would ensure MyCustomValidations was passed with [CascadingParameter]
?
I'm concerned about this line in Validation.razor.cs:
[CascadingParameter] public Validations ParentValidations { get; protected set; }
Yes, it should be picked if MyCustomValidations was inherited from the Validations.
@simelis
I've used this to some success in my code. Would be a little easier if the validation list was visible on the Validations, but eh.
You also have to override the dependency injections for Blazorise.
builder.Services.AddTransient<Validations, ApiExtendedValidations>();
builder.Services.AddTransient<Validation, ApiExtendedValidation>();
public class ApiExtendedValidations : Validations
{
public void AddMessages(string memberName, IEnumerable<string> errors)
{
var val = ValidationFields.FirstOrDefault(i =>
i.FieldIdentifier.FieldName.Equals(memberName, StringComparison.CurrentCultureIgnoreCase));
if (val == null)
return;
var messages = val.Messages?.ToList()??new List<string>();
messages.AddRange(errors);
val.NotifyValidationStatusChanged(ValidationStatus.Error, messages);
}
private List<ApiExtendedValidation> ValidationFields { get; set; } = new List<ApiExtendedValidation>();
public void AddValidation(ApiExtendedValidation field)
{
ValidationFields.Add(field);
}
public void RemoveValidation(ApiExtendedValidation field)
{
ValidationFields.Remove(field);
}
}
public class ApiExtendedValidation : Validation, IDisposable
{
protected override Task OnInitializedAsync()
{
if (ParentValidations is ApiExtendedValidations validations)
{
validations.AddValidation(this);
}
return base.OnInitializedAsync();
}
public new void Dispose()
{
if (ParentValidations is ApiExtendedValidations validations)
{
validations.RemoveValidation(this);
}
base.Dispose();
}
}
public static class ValidationsHelper
{
public static void AddMessages(this Validations validations, string memberName, params string[] messages)
{
if (!(messages?.Any()??false))
return;
if (validations is ApiExtendedValidations val)
{
val.AddMessages(memberName, messages);
}
}
}
The extensions are mostly so I didn't have to go back and change all my existing Validations to reference the correct thing.
I am currently using data annotations validations for my models, and that is working beautifully on the client. I also enforce validation on the server side in the API once my models are pass off to business logic, and I am trying to bridge the gap between the two. My business logic raises exceptions when a validation rule is broken which I am returning in the standardized ValidationProblemDetails from the API. I am trying to find a way to get those messages back into the your validation system to they can be shown on the fields they relate to. My business logic does not trust anything from the outside so it has its own validation, even though it may repeat some of the UI validation in the models. This is done purely to improve the user experience. I need a way to get that feedback fed into the UI the same way data annotations are handled on the client. I want the user's experience to be seamless. I was hoping there was some method on the component that would allow me to add custom feedback for a field. Any ideas how this can be accomplished? Below is an example of submitting a form to the API and back and reading the errors back. This is where I would need to pass them back to the validation system.
@code { [Parameter] public int? Id { get; set; }
}