Validating Blazor sub-components?
Asked Answered
K

2

7

I had a Blazor component containing a persons name and their address. I have split out the address so I can reuse it. I am using 2 way data binding between the person and address to ensure that data is passed to the address and the person can receive address changes.

I cannot get validation to work though. The person full name and the Address line 1 cannot be blank. When I use VaidationSummary then it correctly reports that both fields cannot be blank. But when I use ValidationMessage only the person full name reports a validation message. I am using Fluent validation but I believe the issue is that ValidationMessage does not report when in a complex type.

I think it is because the For() attribute for the Address line 1 ValidationMessage does not match the field name in the master form (Person) data model. The master data model has the address class as Address but the address component has it as Value. However, if I am to reuse the component then this is likely to happen!

Separating components like addresses seems a reasonable thing to do and you might have more than one address object on a form (delivery and billing for example) so I just need to know how to do it.

Has anyone done this? Is a custom ValidationMessage needed or a different For() implementation?

Thanks for your help with this. Here is the source.

Form:

<EditForm Model=@FormData>
    <FluentValidator/>
    <ValidationSummary/>
    <InputText @bind-Value=FormData.FullName />
    <ValidationMessage For="@(() => FormData.FullName)"/>
    <ComponentAddress @bind-Value=FormData.Address />
    <input type="submit" value="Submit" class="btn btn-primary" />
</EditForm>

@code{
    PersonDataModel FormData = new PersonDataModel();
}

Address Component:

<InputText @bind-Value=Value.Address1 @onchange="UpdateValue" />
<ValidationMessage For="@(() => Value.Address1)" />
@code{
    [Parameter] public AddressDataModel Value { get; set; }
    [Parameter] public EventCallback<AddressDataModel> ValueChanged { get; set; }

    protected async Task UpdateValue()
    {
        await ValueChanged.InvokeAsync(Value);
    }
}

Person model:

   public class PersonDataModel
    {
        [Required]
        public string FullName { get; set; }
        public AddressDataModel Address { get; set; }

        public PersonDataModel()
        {
            Address = new AddressDataModel();
        }
    }

Address model:

public class AddressDataModel
{
    [Required]
    public string Address1 { get; set; }
}

Person Fluent Validator:

public class PersonValidator : AbstractValidator<PersonDataModel>
{
    public PersonValidator()
    {
        RuleFor(r => r.FullName).NotEmpty().WithMessage("You must enter a name");
        RuleFor(r => r.Address.Address1).NotEmpty().WithMessage("You must enter Address line 1");
    }
}
Keep answered 29/6, 2021 at 14:23 Comment(2)
You can take a look at this.Divergence
So I have found a new questions about this. The issue seems to be that FieldIdentifier is defined by the field name. In a child component, this is the local name but to the parent form, it will be ChildComponentName.FieldName. The validation is performed correctly against the local field. ValidationSummary works because it takes all validation errors. The ValidationMessage does not work because it looks for the field name of the EditContext. As the EditContext is set against the parent, the names are different and no messages are found.Keep
T
1

The problem is that the ValidationContext for validating your Component is your Component's Value property - not the model that the parent page is using.

I struggled to get something to get component validation working until I figured out a bit of a hack using another Validation Attribute that I apply to the Component's Value property. When validating Value, I use the Component's EditContext which is a property that is set via a Cascading Parameter. I can get the property's name via reflection and that means I can get the correct FieldIdentifier, notifiy that the field has changed and then get the ValidationResults from the parent's EditContext. I can then return the same error details.

Validation Attribute

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using ValueBinding.Shared;

namespace ValueBinding.Data.Annotations
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class MyValidationContextCheckAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            EditContext ec = null;
            string propName = "NOT SET";

            // My StringWrapper is basic component with manual Value binding
            if (validationContext.ObjectInstance is MyStringWrapper)
            {
                var strComp = (MyStringWrapper)validationContext.ObjectInstance;
                ec = strComp.ParentEditContext; // Uses Cascading Value/Property
                propName = strComp.GetPropertyName();
            }

            if (ec != null)
            {
                FieldIdentifier fld = ec.Field(propName);
                ec.NotifyFieldChanged(in fld);

                // Validation handled in Validation Context of the correct field not the "Value" Property on component
                var errors = ec.GetValidationMessages(fld);

                if (errors.Any())
                {
                    string errorMessage = errors.First();
                    return new ValidationResult(errorMessage, new List<string> { propName });
                }
                else
                {
                    return null;
                }
            }
            else if (typeof(ComponentBase).IsAssignableFrom(validationContext.ObjectType))
            {
                return new ValidationResult($"{validationContext.MemberName} - Validation Context is Component and not data class", new List<string> { validationContext.MemberName });
            }
            else
            {
                return null;
            }
        }
    }
}

Component

@using System.Linq.Expressions
@using System.Reflection
@using Data
@using Data.Annotations

<div class="fld" style="border-color: blue;">
    <h3>@GetPropertyName()</h3>
    <InputText @bind-Value=@Value />
    <ValidationMessage For=@ValidationProperty />
    <div class="fld-info">@HelpText</div>
</div>

@code {
    [Parameter]
    public string Label { get; set; } = "NOT SET";

    [Parameter]
    public string HelpText { get; set; } = "NOT SET";

    [Parameter]
    public Expression<Func<string>> ValidationProperty { get; set; }

    private string stringValue = "NOT SET";
    [MyValidationContextCheck]
    [Parameter]
    public string Value
    {
        get => stringValue;
        set
        {
            if (!stringValue.Equals(value))
            {
                stringValue = value;
                _ = ValueChanged.InvokeAsync(stringValue);
            }
        }
    }

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

    [CascadingParameter]
    public EditContext ParentEditContext { get; set; }

    public string GetPropertyName()
    {
        Expression body = ValidationProperty.Body;
        MemberExpression memberExpression = body as MemberExpression;
        if (memberExpression == null)
        {
            memberExpression = (MemberExpression)((UnaryExpression)body).Operand;
        }
        PropertyInfo propInfo = memberExpression.Member as PropertyInfo;
        return propInfo.Name;
    }
} 
Thanks answered 8/7, 2021 at 13:18 Comment(1)
This structure is applied to our base form input components so that all the inherited components behave in the same way so that you don't have to apply the Attribute on all your form input components. We actually only have a Property Expression Parameter so that all value and all Display values are handled within the base component via Reflection and Linq and the page markup code is really clean.Thanks
Z
0

For this to work with FluentValidations you just need to update your AddressComponent.razor like this:

<InputText @bind-Value=Value.Address1 @onchange="UpdateValue" />
<ValidationMessage For="ValueExpression" />

@code{
    [Parameter] public AddressDataModel Value { get; set; }
    [Parameter] public EventCallback<AddressDataModel> ValueChanged { get; set; }
    [Parameter] public Expression<Func<AddressDataModel>> ValueExpression { get; set; } = default!;
    [CascadingParameter] public EditContext? EditContext { get; set; }

    protected async Task UpdateValue()
    {
        await ValueChanged.InvokeAsync(Value);
    }
}

So 3 changes were made:

  • ValueExpression parameter was added. This parameter is filled when you bind a property to it. You need is as input to ValidationMessage
  • added EditContext as CascadingParameter. This parameter is automatically filled when your AddressComponent is in the EditForm
  • ValueExpression is now as argument inside ValidationMessage's For argument

If you would like to have a condition checking if there is a ValidationMessage available, you can check it like:

private bool _hasValidationMessages => EditContext is null || ValueExpression is null
    ? false
    : EditContext.GetValidationMessages(FieldIdentifier.Create(ValueExpression)).Any();
Zosema answered 9/4 at 21:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.