(Mud)Blazor - Validate Nested Component with Fluent Validation
Asked Answered
H

2

6

I'm currently in the process of learning Blazor (with MudBlazor) using FluentValidation. I'm largely going off what's in the MudBlazor docs for patterns and practices. I've got a top-level form (Main Form) that contains some basic input fields and some select lists that are API driven. I've taken the select lists and turned them into components since I'll be reusing them elsewhere in the application.

I've successfully added FluentValidation to the main form and I'm seeing the fields get highlighted in red and the error message displayed on save when the validation fails. What I'm not able to figure out is how to validate and display the error on the controls in the nested/child components. I know it is based on my inexperience (dumbness) but I'm not finding much about this specific scenario on the internet.

On to the code. Here's a small subset of my code that demonstrates my problem. A working example can be found on Try MudBlazor.

Edit: If this is a poor pattern for components, I'm fine with that. Just let me know and I'll back off this approach.

MainForm.razor

<MudForm Model="@formData" @ref="@form" Validation="@(modelValidator.ValidateValue)">
    <MudGrid>
        <MudItem xs=12>
            <MudTextField @bind-Value="formData.LastName" Label="Last Name" For="(() => formData.LastName)"
                            Variant="Variant.Text" MaxLength="50"></MudTextField>
        </MudItem>
        <MudItem xs=12>
            <MudTextField @bind-Value="formData.FirstName" Label="First Name" For="(() => formData.FirstName)"
                            Variant="Variant.Text" MaxLength="50"></MudTextField>
        </MudItem>
        <MudItem xs=12>
            <RaceSelector @bind-SelectedRaceId="@selectedRaceId" />
        </MudItem>
        <MudItem xs=12>
            <span>The Selected Race ID is: @selectedRaceId</span>
        </MudItem>
        <MudItem xs=12>
            <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="async () => await Submit() ">Save</MudButton>
        </MudItem>
    </MudGrid>
</MudForm>

@code {
    private Model formData = new();
    private string selectedRaceId = string.Empty;
    private ModelValidator modelValidator = new();
    private MudForm form;

    private async Task Submit()
    {
        await form.Validate();

        if (form.IsValid)
        {
            // Post to API
        }
    }
}

RaceSelector.razor

<MudSelect @bind-Value="SelectedRaceId" Placeholder="" T="string" 
            Label="Race" Variant="Variant.Outlined">
    @foreach (var race in RaceList)
    {
        <MudSelectItem T="string" Value="@race.Id.ToString()">@race.Name</MudSelectItem>
    }
</MudSelect>


@code {
    private List<Race>? RaceList { get; set; }
    private string selectedRaceId;

    [Parameter]
    public string SelectedRaceId 
    {
        get
        {
            return selectedRaceId;
        }
        set
        { 
            // Wire-up two way binding
            if (selectedRaceId != value)
            {
                selectedRaceId = value;

                if (SelectedRaceIdChanged.HasDelegate)
                {
                    SelectedRaceIdChanged.InvokeAsync(value);
                }
            }
        }
    }

    [Parameter]
    public EventCallback<string> SelectedRaceIdChanged { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
        // Pretend this is a call to the API
        RaceList = new List<Race>
        {
            new Race(1, "American Ind/Alaskan"),
            new Race(2, "Asian or Pacific Isl"),
            new Race(3, "Black, not Hispanic"),
            new Race(4, "Hispanic"),
            new Race(5, "White, not Hispanic"),
            new Race(6, "Other"),
            new Race(7, "Multi-Racial"),
            new Race(8, "Unknown")
        };
    }   
}

Model.cs and Race.cs

public class Model
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public string RaceId {get; set;}
}

public class Race
{
    public Race() {}

    public Race(int id, string name)
    {
        Id = id;
        Name = name;            
    }

    public int Id {get; set;}
    public string Name {get; set;}
}

ModelValidator.cs

using FluentValidation;

public class ModelValidator : AbstractValidator<Model>
{
    public ModelValidator()
    {
        RuleFor(x => x.LastName)
            .NotEmpty();

        RuleFor(x => x.FirstName)
            .NotEmpty();

        RuleFor(x => x.RaceId)
            .NotEmpty();              
    }

    public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
    {
        var result = await ValidateAsync(ValidationContext<Model>.CreateWithOptions((Model)model, x => x.IncludeProperties(propertyName)));
        if (result.IsValid)
            return Array.Empty<string>();
        return result.Errors.Select(e => e.ErrorMessage);
    };
}
Harilda answered 4/10, 2022 at 14:59 Comment(3)
did you find a fix for this? I'm running into the same issue and basically am duplicating a lot of markup because I can't validate nested componentsMonied
I found a workaround but I didn't like it. I was able to get the validation to trigger if I passed in the formData model down into the component as a parameter. I didn't want my component to be tightly coupled with the model since I needed to use it with other models.Harilda
Hello, still no news on this matter? I face the same problem and figured the same workaround as Allen but I don't quite like it :(Reiners
M
3

This is late to the party, but maybe someone else will come looking for this. What you need to do is to forward the For parameter to your component. Something like this:

RaceSelector.razor, two changes:

  1. In the code section, add
[Parameter] public Expression<Func<T>> For<T> { get; set; }
  1. Inside the MudSelector tag, add
For="@For"

MainForm.razor, one change:

Now, inside the RaceSelector tag, you can use

For="(() => formData.RaceId)""
Marielamariele answered 21/2, 2023 at 23:38 Comment(2)
Can confirm this works, good one.Juvenescence
If you name the parameter {Name of value}Expression, you don't have to also supply the For parameter. (a)bind will do everything for you. So you can use [Parameter] public Expression<Func<T>> SelectedRaceId<T> { get; set; } instead. No need to manually set a For parameter. (a)bind hooks up Value, ValueChanged and ValueExpressionMalposition
S
0

in RaceSelector.razor:

1- Add new parameter:

    [Parameter]
    public Expression<Func<int>> ForSelectedRaceId  { get; set; }

2- Add For attribute to MudSelect and set it to ForSelectedRaceId parameter:

<MudSelect @bind-Value="SelectedRaceId" Placeholder="" T="string" 
            Label="Race" Variant="Variant.Outlined"
            For="ForSelectedRaceId">
    @foreach (var race in RaceList)
    {
        <MudSelectItem T="string" Value="@race.Id.ToString()">@race.Name</MudSelectItem>
    }
</MudSelect>

In your parent component MainForm.razor, set the ForSelectedRaceId parameter value ForSelectedRaceId="(() => formData.RaceId)":

 <RaceSelector @bind-SelectedRaceId="@selectedRaceId" ForSelectedRaceId="(() => formData.RaceId)"/>
Sizzle answered 23/6, 2023 at 10:21 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.