Reputation: 375
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");
}
}
Upvotes: 8
Views: 2553
Reputation: 2367
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
EditContext
as CascadingParameter
. This parameter is automatically filled when your AddressComponent
is in the EditForm
ValueExpression
is now as argument inside ValidationMessage's
For
argumentIf 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();
Upvotes: 0
Reputation: 11
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 ValidationResult
s 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;
}
}
Upvotes: 1