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);
    };
}
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 components Commented Oct 27, 2022 at 2:16
  • 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. Commented Nov 18, 2022 at 20:25
  • 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 :( Commented Dec 4, 2022 at 14:39

2 Answers 2

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)""
Sign up to request clarification or add additional context in comments.

2 Comments

Can confirm this works, good one.
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 ValueExpression
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)"/>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.