Blazor - Nested component does not highlight invalid fields or display ValidationMessages

1.1k Views Asked by At

I can't figure out how to highlight invalid fields and display individual ValidationMessages for nested components. The same code when added to the page works as expected, but when moved to a separate component the page's ValidationSummary displays errors for this component just fine, but component itself does not provide any validation results.

I did an extensive search and I only found topics about validation of complex models, but nothing about displaying validation information in nested components. This suggests that I am probably missing something simple, but I can't figure out what this is.

Here is the simplified code reproducing this behavior.

Product.cs

public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Added On Date is required")]
    public DateTime? AddedOn { get; set; }

    [Required(ErrorMessage = "Product Name is required")]
    public string Name { get; set; }

    public int BrandId { get; set; }

    public int ManufacturerId { get; set; }
}

ChildComponent.razor

<div class="card">
    <div class="card-header">
        @(new MarkupString(Caption))
    </div>
    <div class="card-body">
        <input type="text" @bind-value=@TextValue />
        <ValidationMessage For=@(() => TextValue) />
        
        <input type="datetime-local" @bind-value=@DateValue />
        <ValidationMessage For=@(() => DateValue) />
    </div>
</div>

@code {
    [Parameter]
    public string Caption { get; set; }

    [Parameter]
    public string TextValue { get; set; }

    [Parameter]
    public DateTime? DateValue { get; set; }
}

TestPage.razor

@page "/test"

<EditForm OnValidSubmit="HandleFormSubmit" Model="@ProductModel">
    <DataAnnotationsValidator />

    @*ValidationMessages are returned/displayed*@
    <div class="card">
        <div class="card-header">
            Control on the Page
        </div>
        <div class="card-body">
            <input type="text" @[email protected] />
            <ValidationMessage For=@(() => ProductModel.Name) />

            <input type="datetime-local" @[email protected] />
            <ValidationMessage For=@(() => ProductModel.AddedOn) />
        </div>
    </div>

    @*ValidationMessages are NOT returned/displayed*@
    <ChildComponent Caption="Nested Component"
                    [email protected]
                    [email protected] />

    @*ValidationSummary displays errors as expected*@
    <ValidationSummary />

    <input type="submit" value="Save" class="btn btn-primary" />
</EditForm>

@code {

    Product ProductModel = new Product();

    private async Task HandleFormSubmit(EditContext context) { }
}

EDIT:

Using info from the link provided by @NeilW I am now able to change the CSS of input controls (red border when invalid, green when valid), but I am still not able to display ValidationMessages for invalid controls. Any suggestions?

enter image description here

1

There are 1 best solutions below

0
On

This & this articles from Chris Sainty provide the solution. Thanks to @NeilW for sharing the link.

Here is the updated code that works as expected:

Product.cs (no changes)

public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Added On Date is required")]
    public DateTime? AddedOn { get; set; }

    [Required(ErrorMessage = "Product Name is required")]
    public string Name { get; set; }

    public int BrandId { get; set; }

    public int ManufacturerId { get; set; }
}

ChildComponent.razor

<div class="card">
    <div class="card-header">
        @(new MarkupString(Caption))
    </div>
    <div class="card-body">
        <input type="text" class="@textFieldCss"
            value=@TextValue @oninput="HandleTextInput" />
        @foreach (var message in CascadedEditContext?.GetValidationMessages(textFieldId))
        {
            <div class="validation-message">@message</div>
        }

        <input type="datetime-local" class="@dateFieldCss"
            value=@DateValue @oninput="HandleDateInput" />
        @foreach (var message in CascadedEditContext.GetValidationMessages(dateFieldId))
        {
            <div class="validation-message">@message</div>
        }
    </div>
</div>

@code {
    private FieldIdentifier textFieldId;
    private FieldIdentifier dateFieldId;

    private string textFieldCss => CascadedEditContext?.FieldCssClass(textFieldId) ?? "";
    private string dateFieldCss => CascadedEditContext?.FieldCssClass(dateFieldId) ?? "";

    [CascadingParameter]
    private EditContext CascadedEditContext { get; set; }

    [Parameter]
    public EventCallback<string> TextValueChanged { get; set; }

    [Parameter]
    public EventCallback<DateTime?> DateValueChanged { get; set; }

    [Parameter]
    public System.Linq.Expressions.Expression<Func<string>> TextValueExpression { get; set; }

    [Parameter]
    public System.Linq.Expressions.Expression<Func<DateTime?>> DateValueExpression { get; set; }

    protected override void OnInitialized()
    {
        textFieldId = FieldIdentifier.Create(TextValueExpression);
        dateFieldId = FieldIdentifier.Create(DateValueExpression);
    }

    private async Task HandleTextInput(ChangeEventArgs args)
    {
        await TextValueChanged.InvokeAsync(args.Value.ToString());
        CascadedEditContext?.NotifyFieldChanged(textFieldId);
    }

    private async Task HandleDateInput(ChangeEventArgs args)
    {
        await DateValueChanged.InvokeAsync(args.Value == null
                ? null : Convert.ToDateTime(args.Value));
        CascadedEditContext?.NotifyFieldChanged(dateFieldId);
    }

    [Parameter]
    public string Caption { get; set; }

    [Parameter]
    public string TextValue { get; set; }

    [Parameter]
    public DateTime? DateValue { get; set; }
}

TestPage.razor - notice @bind- added to the component's attributes.

@page "/test"

<EditForm Model="@ProductModel">
    <DataAnnotationsValidator />

    <div class="card">
        <div class="card-header">
            Control on the Page
        </div>
        <div class="card-body">
            <input type="text" @[email protected] />
            <ValidationMessage For=@(() => ProductModel.Name) />

            <input type="datetime-local" @[email protected] />
            <ValidationMessage For=@(() => ProductModel.AddedOn) />
        </div>
    </div>

    <ChildComponent Caption="Nested Component"
                    @[email protected]
                    @[email protected] />

    <ValidationSummary />

    <input type="submit" value="Save" class="btn btn-primary" />
</EditForm>

@code {
    Product ProductModel = new Product();
}

Result

enter image description here