Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor validation for custom class

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>
like image 676
Георги Димитров Avatar asked Jun 04 '19 15:06

Георги Димитров


2 Answers

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.

like image 60
Peter Morris Avatar answered Oct 25 '22 03:10

Peter Morris


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

like image 30
Георги Димитров Avatar answered Oct 25 '22 03:10

Георги Димитров