.net-corefluentvalidationmudblazor

Mudblazor Select with multiselect and Fluentvalidation For-Expression


I am binding to a select field in multiselect mode and I ran into a problem with the "For" property of the select field".

Here is a code snippet

When using a select field an options type must be set and in this example it will be string. To make validation work the "For"-Property needs to be set and pointing to a valid property of the same type as the select fields option (and thats string). But I am expecting a multiselect, so I am binding to an IEnumerable<string> in my model and the validation code is also set for this property. I don`t have the necessary property to bind to and even if I did, the validation would not work as expected.

How do I make this work? I tried building a custom expression which would point to the first element of the array, but I am bad with expressions and couldn`t make it work.

@using FluentValidation

<MudCard>
    <MudForm Model="@model" @ref="@form" Validation="@(testValidator.ValidateValue)" ValidationDelay="0">
        <MudCardContent>
                <MudSelect T="string" Label="Name"               
                    HelperText="Pick your favorite name" MultiSelection="false" @bind-Value="model.Name" For="() => model.Name">
                        @foreach (var name in _names)
                        {
                            <MudSelectItem T="string" Value="@name">@name</MudSelectItem>
                        }
                </MudSelect>

                <MudSelect T="string" Label="Names"                 
                    HelperText="Pick your favorite names" MultiSelection="true" @bind-SelectedValues="model.Names"
                    @* For="() => model.Names" This needs to be set to make validation work *@
                     >
                        @foreach (var name in _names)
                        {
                            <MudSelectItem T="string" Value="@name">@name</MudSelectItem>
                        }
                </MudSelect>
        </MudCardContent>
    </MudForm>
    <MudCardActions>
        <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="@(async () => await Submit())">Order</MudButton>
    </MudCardActions>
</MudCard>

@code {    
    [Inject] ISnackbar Snackbar { get; set; }
    
    private string[] _names = new string[] {
        "Toni", "Matthew", "David"
    };

    MudForm form;

    TestModelFluentValidator testValidator = new TestModelFluentValidator();

    TestModel model = new TestModel();

    public class TestModel
    {
        public string Name { get; set; }
        public IEnumerable<string> Names { get; set; }
    }

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

        if (form.IsValid)
        {
            Snackbar.Add("Submited!");
        }
    }

    /// <summary>
    /// A standard AbstractValidator which contains multiple rules and can be shared with the back end API
    /// </summary>
    /// <typeparam name="OrderModel"></typeparam>
    public class TestModelFluentValidator : AbstractValidator<TestModel>
    {
        public TestModelFluentValidator()
        {
            RuleFor(x => x.Name)
                .NotEmpty();

            RuleFor(x => x.Names).Must((parent, property) => property.Contains("Toni"))
                .WithMessage("Toni not found in those names!");
        }

        public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
        {
            var result = await ValidateAsync(ValidationContext<TestModel>.CreateWithOptions((TestModel)model, x => x.IncludeProperties(propertyName)));
            if (result.IsValid)
                return Array.Empty<string>();
            return result.Errors.Select(e => e.ErrorMessage);
        };
    }
}

Edit: Added code sample and trimmed unecessary code.


Solution

  • Mudblazor snippet.

    Ok, so you can trick the component by introducing a dummy property and binding the multi-select component to it then testing its name during validation.

    When the form component passes the dummy property name to the validation method, you change the passed dummy name to the name of your collection so it's matched when fluent validation kicks in.

    Something like this:

    @using FluentValidation
    @using System.Reflection
    
    <MudCard>
        <MudForm Model="@model" @ref="@form" Validation="@(testValidator.ValidateValue)" ValidationDelay="0">
            <MudCardContent>
                    <MudSelect T="string" Label="Name"               
                        HelperText="Pick your favorite name" MultiSelection="false" @bind-Value="model.Name" For="() => model.Name">
                            @foreach (var name in _names)
                            {
                                <MudSelectItem T="string" Value="@name">@name</MudSelectItem>
                            }
                    </MudSelect>
    
                    <MudSelect T="string" Label="Names"
                        
                        HelperText="Pick your favorite names" MultiSelection="true" @bind-Value="model.NameCollection" @bind-SelectedValues="model.Names"
                        For="@(() => model.NameCollection)"
                         >
                            @foreach (var name in _names)
                            {
                                <MudSelectItem T="string" Value="@name">@name</MudSelectItem>
                            }
                    </MudSelect>
            </MudCardContent>
        </MudForm>
        <MudCardActions>
            <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="@(async () => await Submit())">Order</MudButton>
        </MudCardActions>
    </MudCard>
    
    @code {
        [Inject] ISnackbar Snackbar { get; set; }
    
        private string[] _names = new string[] {
            "Toni", "Matthew", "David"
        };
    
        MudForm form;
    
        TestModelFluentValidator testValidator = new TestModelFluentValidator();
    
        TestModel model = new TestModel();
    
        public class TestModel
        {
            public string Name { get; set; }
            public string NameCollection { get; set; }
            public IEnumerable<string> Names { get; set; }
        }
    
        private async Task Submit()
        {
            await form.Validate();
    
            if (form.IsValid)
            {
                Snackbar.Add("Submited!");
            }
        }
    
        /// <summary>
        /// A standard AbstractValidator which contains multiple rules and can be shared with the back end API
        /// </summary>
        /// <typeparam name="OrderModel"></typeparam>
        public class TestModelFluentValidator : AbstractValidator<TestModel>
        {
            public TestModelFluentValidator()
            {
                RuleFor(x => x.Name)
                    .NotEmpty();
    
                RuleFor(x => x.Names).Must((parent, property) => property.Contains("Toni"))
                .WithMessage("Toni not found in those names!");
            }
    
            private async Task<bool> IsUniqueAsync(string email)
            {
                // Simulates a long running http call
                await Task.Delay(2000);
                return email.ToLower() != "test@test.com";
            }
    
            public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
            {        
                propertyName = propertyName == nameof(TestModel.NameCollection) ? nameof(TestModel.Names) : propertyName;
    
                var result = await ValidateAsync(ValidationContext<TestModel>.CreateWithOptions((TestModel)model, x => x.IncludeProperties(propertyName)));
                if (result.IsValid)
                    return Array.Empty<string>();
                return result.Errors.Select(e => e.ErrorMessage);
            };
        }
    }