I'm testing out Blazor and I've run into a validation issue. When validating a simple class I can just use annotations. If I have my own custom class inside though validation doesn't run for everything inside my custom class. The issue seems to be specific to Blazor since I can use this validation in ASP.
Here are my two simple models:
public class TestModel
{
[Required]
[Range(12, 400, ErrorMessage = "This works")]
public int Count { get; set; }
public KeyValue KeyValues { get; set; }
public TestModel()
{
Count = 4;
KeyValues = new KeyValue()
{
Key = 5,
Value = "str"
};
}
}
And the KeyValue class
public class KeyValue
{
[Required]
[Range(10, 300, ErrorMessage = "This number check doesn't")]
public int Key { get; set; }
[Required]
[StringLength(10, MinimumLength = 5, ErrorMessage = "Nor the string one")]
public string Value { get; set; }
}
And that is my component. It validates the Model.Count property, but doesn't validate the nested class.
<EditForm Model="@Model" OnValidSubmit="@DoStuff">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-md-4">
<input type="number" bind="@Model.Count" class="form-control" placeholder="Condition property name" />
</div>
<div class="col-md-4">
<input type="number" bind="@Model.KeyValues.Key" class="form-control" placeholder="Condition property name" />
</div>
<div class="col-md-4">
<InputText bind-Value="@Model.KeyValues.Value"></InputText>
</div>
</div>
<div class="row">
<div class="col-md-12">
<button type="submit" class="btn btn-info">Create</button>
</div>
</div>
</EditForm>
This is a known limitation of Blazor, but you can work around it.
First, use the OnSubmit
event on <EditForm>
instead of OnValidSubmit
. The method is passed an EditContext
like so...
private void FormSubmitted(EditContext context)
{
...
}
If you use the following extension you can use the following code in your FormSubmitted
method and it will not only validate your entire object tree but also update your UI according to the result.
{
if (context.ValdiateObjectTree())
{
... do whatever
}
}
The extension...
using Microsoft.AspNetCore.Components.Forms;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace PeterLeslieMorris.Blazor.Validation.Extensions
{
public static class EditContextExtensions
{
static PropertyInfo IsModifiedProperty;
static MethodInfo GetFieldStateMethod;
/// <summary>
/// Validates an entire object tree
/// </summary>
/// <param name="editContext">The EditContext to validate the Model of</param>
/// <returns>True if valid, otherwise false</returns>
public static bool ValidateObjectTree(this EditContext editContext)
{
var validatedObjects = new HashSet<object>();
ValidateObject(editContext, editContext.Model, validatedObjects);
editContext.NotifyValidationStateChanged();
return !editContext.GetValidationMessages().Any();
}
public static void ValidateProperty(this EditContext editContext, FieldIdentifier fieldIdentifier)
{
if (fieldIdentifier.Model == null)
return;
var propertyInfo = fieldIdentifier.Model.GetType().GetProperty(
fieldIdentifier.FieldName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
var validatedObjects = new HashSet<object>();
ValidateProperty(editContext, fieldIdentifier.Model, propertyInfo, validatedObjects);
}
private static void ValidateObject(
EditContext editContext,
object instance,
HashSet<object> validatedObjects)
{
if (instance == null)
return;
if (validatedObjects.Contains(instance))
return;
if (instance is IEnumerable && !(instance is string))
{
foreach (object value in (IEnumerable)instance)
ValidateObject(editContext, value, validatedObjects);
return;
}
if (instance.GetType().Assembly == typeof(string).Assembly)
return;
validatedObjects.Add(instance);
var properties = instance.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (PropertyInfo property in properties)
ValidateProperty(editContext, instance, property, validatedObjects);
}
private static void ValidateProperty(
EditContext editContext,
object instance,
PropertyInfo property,
HashSet<object> validatedObjects)
{
NotifyPropertyChanged(editContext, instance, property.Name);
object value = property.GetValue(instance);
ValidateObject(editContext, value, validatedObjects);
}
private static void NotifyPropertyChanged(
EditContext editContext,
object instance,
string propertyName)
{
if (GetFieldStateMethod == null)
{
GetFieldStateMethod = editContext.GetType().GetMethod(
"GetFieldState",
BindingFlags.NonPublic | BindingFlags.Instance);
}
var fieldIdentifier = new FieldIdentifier(instance, propertyName);
object fieldState = GetFieldStateMethod.Invoke(editContext, new object[] { fieldIdentifier, true });
if (IsModifiedProperty == null)
{
IsModifiedProperty = fieldState.GetType().GetProperty(
"IsModified",
BindingFlags.Public | BindingFlags.Instance);
}
object originalIsModified = IsModifiedProperty.GetValue(fieldState);
editContext.NotifyFieldChanged(fieldIdentifier);
IsModifiedProperty.SetValue(fieldState, originalIsModified);
}
}
}
You can find the extension source here. You could alternatively use Blazor-Validation, which also allows you to use FluentValidation.
If you want a more in-depth understanding of how Blazor forms / validation works, you can read about it in this section of Blazor University.
In case someone else stumbles into this issue it isn't possible right now. It should be available in 3.0.0-preview8 according to this post https://github.com/aspnet/AspNetCore/issues/10896
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With