Search code examples
c#validationinputblazorblazor-component

How do I get a variable to validate after being passed in from a class instance to a child component in Blazor


I have a generic component that is used by three different pages in the code. Its called Food:

<div>
    <label for="foodName">Food</label>
    <div class="select">
        <InputSelect id="foodName"
                     @bind-Value="FoodId"
                     @oninput="@(async (args) => await OnFoodSelectAsync(args?.Value?.ToString() ?? string.Empty))">
        </InputSelect>
        <span class="focus"></span>
    </div>
<ValidationMessage For="@(() => FoodId)"/>
</div>

@code {
    
    [Parameter]
    public  EventCallback<string>  OnFoodSelect { get; set; }

    [Parameter]
    public int? FoodId { get; set; }

    async Task OnFoodSelectAsync(string selectedFood)
    {       
        if (OnFoodSelect.HasDelegate)
        {
            await OnFoodSelect.InvokeAsync(selectedFood);
        }
    }
}

Those pages use it like so:

<Food  OnFoodSelect="OnFoodSelectAsync" FoodId="PerishableFoods.FoodId"/>

or

<Food  OnFoodSelect="OnFoodSelectAsync" FoodId="HotFoods.FoodId"/>

I want the validation of FoodId from HotFoods or PerishableFoods to work in the child component. The annotation in the classes look like:

public class HotFoods
{
    [Required(ErrorMessage = "Food selection required")] 
    public int? FoodId { get; set; }
}    

or

public class PerishableFoods
{
    [Required(ErrorMessage = "Food selection required")] 
    public int? FoodId { get; set; }
}    

However, the ValidationMessage doesn't work right now. The only thing that works is ValidationSummary and I don't want a list kind of functionality. Each error message should show up right after its input. Are there edits I can make to make this validation work?

I have also tried ValidateComplexType with ObjectGraphDataAnnotationsValidator but I can't get that to work either.


Solution

  • I've taken the liberty of rewriting your select component to demonstrate how you can incorporate binding so it has the standard Blazor input functionality. And you get back simple object validation. I've also added a little extra functionality for dealing with null/zero values.

    I'm not sure what the <span class="focus"></span> is for, and your InputSelect has no content.

    The modified component:

    @using System.Linq.Expressions;
    
    <div>
        <label for="foodName">Food</label>
        <InputSelect @attributes=AdditionalAttributes
                     TValue=int?
                     Value=this.Value
                     ValueChanged=this.OnChange
                     ValueExpression=this.ValueExpression>
    
            @if (this.Value is null || this.Value == 0)
            {
                <option selected disabled>@this.PlaceholderText</option>
            }
    
            @foreach (var option in SelectItems)
            {
                <option value="@option.Key">@option.Value</option>
            }
    
        </InputSelect>
        <ValidationMessage [email protected] />
    </div>
    
    @code {
        [Parameter] public int? Value { get; set; }
        [Parameter] public EventCallback<int?> ValueChanged { get; set; }
        [Parameter] public Expression<Func<int?>>? ValueExpression { get; set; }
    
        [Parameter] public IReadOnlyDictionary<int, string> SelectItems { get; set; } = new Dictionary<int, string>();
        [Parameter] public string PlaceholderText { get; set; } = " -- Select an Option -- ";
    
        [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
    
        private Task OnChange(int? value)
            => this.ValueChanged.InvokeAsync(value);
    }
    

    And a page to demostrate the control, editing and validation in action:

    @page "/"
    @using System.ComponentModel.DataAnnotations;
    
    <PageTitle>Index</PageTitle>
    
    <h1>Edit Form</h1>
    <h3>Hot Food</h3>
    <EditForm Model=_hotModel OnValidSubmit=this.OnValidSubmit>
        <DataAnnotationsValidator/>
        <div class="col mb-3">
            <YourSelect class="form-control" @bind-Value=_hotModel.FoodId SelectItems=_foods />
        </div>
        <div class="text-end">
            <button type="submit" class="btn btn-success">Submit</button>
        </div>
    </EditForm>
    
    <h3>Cold Food</h3>
    <EditForm Model=_coldModel OnValidSubmit=this.OnValidSubmit>
        <DataAnnotationsValidator />
        <div class="col mb-3">
            <YourSelect class="form-control" @bind-Value=_coldModel.FoodId SelectItems=_foods />
        </div>
        <div class="text-end">
            <button type="submit" class="btn btn-success">Submit</button>
        </div>
    </EditForm>
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>Hot Food ID: @_hotModel.FoodId </pre>
        <pre>Cold Food ID: @_coldModel.FoodId </pre>
    </div>
    
    @code {
        private HotFoods _hotModel = new();
        private PerishableFoods _coldModel = new();
    
        private IReadOnlyDictionary<int, string> _foods = new Dictionary<int, string> { { 1, "Bread Loaf" }, { 2, "Pasty"}, { 3, "Sausage Roll"} };
    
        public Task OnValidSubmit()
        {
            return Task.CompletedTask;
        }
    
        public class HotFoods
        {
            [Required(ErrorMessage = "Hot Food selection required")]
            public int? FoodId { get; set; }
        }
    
        public class PerishableFoods
        {
            [Required(ErrorMessage = "Cold Food selection required")]
            public int? FoodId { get; set; }
        }
    }
    

    enter image description here