bucktronic
bucktronic

Reputation: 3067

How to display an custom ValidationMessage for a list property

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?

Upvotes: 0

Views: 1204

Answers (2)

bucktronic
bucktronic

Reputation: 3067

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

Upvotes: 1

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

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 class="validation-message">
        @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 class="form-control" @bind-Value=this.model.TemperatureC />
    <CustomValidationMessage Model="model" FieldName="TemperatureC" />

    <div class="m-2 p-2 bg-light">
        <ValidationSummary />
    </div>
</EditForm>

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

For reference the ValidationMessage code is here

enter image description here

Upvotes: 1

Related Questions