Skip to content

Instantly share code, notes, and snippets.

@SteveSandersonMS
Created September 4, 2019 10:48
Show Gist options
  • Save SteveSandersonMS/090145d7511c5190f62a409752c60d00 to your computer and use it in GitHub Desktop.
Save SteveSandersonMS/090145d7511c5190f62a409752c60d00 to your computer and use it in GitHub Desktop.
Blazor + FluentValidation example
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; }
}
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);
}
}
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;
}
}
}
}
<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");
}
}
@ctrl-alt-d
Copy link

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();
}

@cvphat4796
Copy link

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

@maran-baskar
Copy link

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