Created
September 4, 2019 10:48
-
-
Save SteveSandersonMS/090145d7511c5190f62a409752c60d00 to your computer and use it in GitHub Desktop.
Blazor + FluentValidation example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Customer | |
{ | |
public string FirstName { get; set; } | |
public string LastName { get; set; } | |
public Address Address { get; } = new Address(); | |
public List<PaymentMethod> PaymentMethods { get; } = new List<PaymentMethod>(); | |
} | |
public class Address | |
{ | |
public string Line1 { get; set; } | |
public string City { get; set; } | |
public string Postcode { get; set; } | |
} | |
public class PaymentMethod | |
{ | |
public enum Type { CreditCard, HonourSystem } | |
public Type MethodType { get; set; } | |
public string CardNumber { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CustomerValidator : AbstractValidator<Customer> | |
{ | |
public CustomerValidator() | |
{ | |
RuleFor(customer => customer.FirstName).NotEmpty().MaximumLength(50); | |
RuleFor(customer => customer.LastName).NotEmpty().MaximumLength(50); | |
RuleFor(customer => customer.Address).SetValidator(new AddressValidator()); | |
RuleFor(customer => customer.PaymentMethods).NotEmpty().WithMessage("You have to define at least one payment method"); | |
RuleForEach(customer => customer.PaymentMethods).SetValidator(new PaymentMethodValidator()); | |
} | |
} | |
public class AddressValidator : AbstractValidator<Address> | |
{ | |
public AddressValidator() | |
{ | |
RuleFor(address => address.Line1).NotEmpty(); | |
RuleFor(address => address.City).NotEmpty(); | |
RuleFor(address => address.Postcode).NotEmpty().MaximumLength(10); | |
} | |
} | |
public class PaymentMethodValidator : AbstractValidator<PaymentMethod> | |
{ | |
public PaymentMethodValidator() | |
{ | |
RuleFor(card => card.CardNumber) | |
.NotEmpty().CreditCard() | |
.When(method => method.MethodType == PaymentMethod.Type.CreditCard); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using FluentValidation; | |
using Microsoft.AspNetCore.Components; | |
using Microsoft.AspNetCore.Components.Forms; | |
using System; | |
namespace CustomValidationSample | |
{ | |
public class FluentValidator<TValidator> : ComponentBase where TValidator: IValidator, new() | |
{ | |
private readonly static char[] separators = new[] { '.', '[' }; | |
private TValidator validator; | |
[CascadingParameter] private EditContext EditContext { get; set; } | |
protected override void OnInitialized() | |
{ | |
validator = new TValidator(); | |
var messages = new ValidationMessageStore(EditContext); | |
// Revalidate when any field changes, or if the entire form requests validation | |
// (e.g., on submit) | |
EditContext.OnFieldChanged += (sender, eventArgs) | |
=> ValidateModel((EditContext)sender, messages); | |
EditContext.OnValidationRequested += (sender, eventArgs) | |
=> ValidateModel((EditContext)sender, messages); | |
} | |
private void ValidateModel(EditContext editContext, ValidationMessageStore messages) | |
{ | |
var validationResult = validator.Validate(editContext.Model); | |
messages.Clear(); | |
foreach (var error in validationResult.Errors) | |
{ | |
var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName); | |
messages.Add(fieldIdentifier, error.ErrorMessage); | |
} | |
editContext.NotifyValidationStateChanged(); | |
} | |
private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath) | |
{ | |
// This method parses property paths like 'SomeProp.MyCollection[123].ChildProp' | |
// and returns a FieldIdentifier which is an (instance, propName) pair. For example, | |
// it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses | |
// as far into the propertyPath as it can go until it finds any null instance. | |
var obj = editContext.Model; | |
while (true) | |
{ | |
var nextTokenEnd = propertyPath.IndexOfAny(separators); | |
if (nextTokenEnd < 0) | |
{ | |
return new FieldIdentifier(obj, propertyPath); | |
} | |
var nextToken = propertyPath.Substring(0, nextTokenEnd); | |
propertyPath = propertyPath.Substring(nextTokenEnd + 1); | |
object newObj; | |
if (nextToken.EndsWith("]")) | |
{ | |
// It's an indexer | |
// This code assumes C# conventions (one indexer named Item with one param) | |
nextToken = nextToken.Substring(0, nextToken.Length - 1); | |
var prop = obj.GetType().GetProperty("Item"); | |
var indexerType = prop.GetIndexParameters()[0].ParameterType; | |
var indexerValue = Convert.ChangeType(nextToken, indexerType); | |
newObj = prop.GetValue(obj, new object[] { indexerValue }); | |
} | |
else | |
{ | |
// It's a regular property | |
var prop = obj.GetType().GetProperty(nextToken); | |
if (prop == null) | |
{ | |
throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}."); | |
} | |
newObj = prop.GetValue(obj); | |
} | |
if (newObj == null) | |
{ | |
// This is as far as we can go | |
return new FieldIdentifier(obj, nextToken); | |
} | |
obj = newObj; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<EditForm Model="customer" OnValidSubmit="SaveCustomer"> | |
<FluentValidator TValidator="CustomerValidator" /> | |
<h3>Your name</h3> | |
<InputText placeholder="First name" @bind-Value="customer.FirstName" /> | |
<InputText placeholder="Last name" @bind-Value="customer.LastName" /> | |
<ValidationMessage For="@(() => customer.FirstName)" /> | |
<ValidationMessage For="@(() => customer.LastName)" /> | |
<h3>Your address</h3> | |
<div> | |
<InputText placeholder="Line 1" @bind-Value="customer.Address.Line1" /> | |
<ValidationMessage For="@(() => customer.Address.Line1)" /> | |
</div> | |
<div> | |
<InputText placeholder="City" @bind-Value="customer.Address.City" /> | |
<ValidationMessage For="@(() => customer.Address.City)" /> | |
</div> | |
<div> | |
<InputText placeholder="Postcode" @bind-Value="customer.Address.Postcode" /> | |
<ValidationMessage For="@(() => customer.Address.Postcode)" /> | |
</div> | |
<h3> | |
Payment methods | |
[<a href @onclick="AddPaymentMethod">Add new</a>] | |
</h3> | |
<ValidationMessage For="@(() => customer.PaymentMethods)" /> | |
@foreach (var paymentMethod in customer.PaymentMethods) | |
{ | |
<p> | |
<InputSelect @bind-Value="paymentMethod.MethodType"> | |
<option value="@PaymentMethod.Type.CreditCard">Credit card</option> | |
<option value="@PaymentMethod.Type.HonourSystem">Honour system</option> | |
</InputSelect> | |
@if (paymentMethod.MethodType == PaymentMethod.Type.CreditCard) | |
{ | |
<InputText placeholder="Card number" @bind-Value="paymentMethod.CardNumber" /> | |
} | |
else if (paymentMethod.MethodType == PaymentMethod.Type.HonourSystem) | |
{ | |
<span>Sure, we trust you to pay us somehow eventually</span> | |
} | |
<button type="button" @onclick="@(() => customer.PaymentMethods.Remove(paymentMethod))">Remove</button> | |
<ValidationMessage For="@(() => paymentMethod.CardNumber)" /> | |
</p> | |
} | |
<p><button type="submit">Submit</button></p> | |
</EditForm> | |
@code { | |
private Customer customer = new Customer(); | |
void AddPaymentMethod() | |
{ | |
customer.PaymentMethods.Add(new PaymentMethod()); | |
} | |
void SaveCustomer() | |
{ | |
Console.WriteLine("TODO: Actually do something with the valid data"); | |
} | |
} |
Adding validation just for a field:
protected override void OnInitialized()
{
validator = new TValidator();
var messages = new ValidationMessageStore(EditContext!);
// Revalidate when any field changes, or if the entire form requests validation
// (e.g., on submit)
EditContext!.OnFieldChanged += (sender, eventArgs)
=> ValidateField((EditContext)sender!, messages, eventArgs.FieldIdentifier); //<--- THIS
EditContext.OnValidationRequested += (sender, eventArgs)
=> ValidateModel((EditContext)sender!, messages);
}
private void ValidateField(EditContext editContext, ValidationMessageStore messages, FieldIdentifier fieldIdentifier) //<--- HERE
{
var properties = new[] { fieldIdentifier.FieldName };
var context = new ValidationContext<object>(editContext.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));
var validationResult = validator!.Validate(context);
messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, validationResult.Errors.Select(error => error.ErrorMessage));
editContext.NotifyValidationStateChanged();
}
Any solution for OnFieldChange with field of item in list ?
With code from @ctrl-alt-d we can only valid field in object. fieldIdentifier.FieldName
just is field name. I need field path
Any solution for OnFieldChange with field of item in list ?
With code from @ctrl-alt-d we can only valid field in object.
fieldIdentifier.FieldName
just is field name. I need field path
What do you mean by field path here?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thank you for the info... it works