Home > Software design >  How to display an custom ValidationMessage for a list property
How to display an custom ValidationMessage for a list property

Time:12-18

I'm having some trouble displaying a custom validation error message inline in a Blazor edit form.

On the model, we have a custom ValidationAttribute (OptionFieldsNotEmpty) which checks that a list is populated.

class Field {
[OptionFieldsNotEmpty]
public List<OptionFieldViewModel> Options { get; set; }
}

In the razor page I have a tried to use the standard ValidationMessage but nothing shows for the markup below (OptionEditor is a custom component rendering a list of inputs)

<OptionEditor OptionFields="@Field.Options"/>
<ValidationMessage For="() => Field.Options"/>

The custom validation definitely works but but the ValidationMessage doesn't show inline where I'd like it to. The message does appear in the ValidationSummary.

It seems like because Field.Options isn't actually bound to a form input, the framework doesn't know where to render it.

Does anyone know if this is possible or should I fall back to a ValidationSummary?

CodePudding user response:

The Blazor "EditContext" is a bit of a contradition: simplistic in it's concept and complex in it's implementation. ValidationSummary is a case in point. For defines an expression that it then needs to jump through hoops backwards to decode. Lots of CPU cycles wasted doing something that doesn't really need doing.

Here's an alternative ValidationMessage that simply accepts the Model and FieldName as parameters. No For: it constructs the FieldIdentifier based on the Model and FieldName provided. It's a lift of the ValidationSummary code without the expression untaggling and refection.

In your case just put in a break at a convenient place and find the Model and FieldName for your error message to enter as parameters.

@implements IDisposable

@foreach (var message in CurrentEditContext.GetValidationMessages(_fieldIdentifier))
{
    <div >
        @message
    </div>
}

@code {
    [CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;
    [Parameter, EditorRequired] public  object Model { get; set; } = default!;
    [Parameter, EditorRequired] public string FieldName { get; set; } = default!;

    private readonly EventHandler<ValidationStateChangedEventArgs>? _validationStateChangedHandler;
    private FieldIdentifier _fieldIdentifier => new FieldIdentifier(this.Model, this.FieldName);
    private EditContext? _previousEditContext;

    public CustomValidationMessage()
        => _validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();

    protected override void OnParametersSet()
    {
        if (CurrentEditContext == null)
            throw new InvalidOperationException($"{GetType()} requires a cascading parameter "  
                $"of type {nameof(EditContext)}. For example, you can use {GetType()} inside "  
                $"an {nameof(EditForm)}.");

        if (CurrentEditContext != _previousEditContext)
        {
            DetachValidationStateChangedListener();
            CurrentEditContext.OnValidationStateChanged  = _validationStateChangedHandler;
            _previousEditContext = CurrentEditContext;
        }
    }
    protected virtual void Dispose(bool disposing) {}

    void IDisposable.Dispose()
    {
        DetachValidationStateChangedListener();
        Dispose(disposing: true);
    }

    private void DetachValidationStateChangedListener()
    {
        if (_previousEditContext != null)
            _previousEditContext.OnValidationStateChanged -= _validationStateChangedHandler;
    }
}

To demo I've modified WeatherForecast:

public class WeatherForecast
{
    public DateOnly Date { get; set; }
    [Range(-40, 60, ErrorMessage = "Temperature must be between -40 and 60.")]
    public int TemperatureC { get; set; }
    public int TemperatureF => 32   (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}

And then a demo page:

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<EditForm Model=this.model>
    <DataAnnotationsValidator />
    <InputNumber  @bind-Value=this.model.TemperatureC />
    <CustomValidationMessage Model="model" FieldName="TemperatureC" />

    <div >
        <ValidationSummary />
    </div>
</EditForm>

@code {
    private WeatherForecast model = new();
}

For reference the ValidationMessage code is enter image description here

CodePudding user response:

After looking into the dotnet core code I saw that instead of using ValidationMessage you can use the ValidationSummary with a Model parameter set to show "inline" errors at a model level:

<ValidationSummary Model="Field"/>

Model level error message displayed inline

  • Related