'Blazor Complex Validation between two nested Objects

Let's say we have simple Object that contains two of another type

public class Parent
{
     [ValidateComplexType]
     public Child Child1 { get; set; }

     [ValidateComplexType]
     public Child Child2 { get; set; }
}
 
public class Child : IValidatableObject
{
     public String Name { get; set; } = String.Empty
     
     public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
     {
         return new ValidationResult("Error", new[] { nameof(Name) })
     }
}

I managed to do nested validation by using ObjectGraphDataAnnotationsValidator as suggested at https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#nested-models-collection-types-and-complex-types

Now let's say that I don't want Child2 to have the same Name as Child 1, so I need to compare their Name properties and display an error on the Child2 input field. If I do this by adding IValidatableObject to the Parent and in the Validate method return new ValidationResult("Error", new[] { nameof(Child2.Name) }) this doesn't actually set the field as invalid.

I thought about adding a Func<Child, Boolean> to each child and then set it when I Instantiate the Parent object, that looks like child => child == Child2 && Child2.Name == Child1.Name and it works but it is very confusing in my opinion. How to do this properly?



Solution 1:[1]

In my humble opinion, you need to use custom validation here to check if Child2 has the same Name as Child1. I did a test in a blazor server application. My model has 2 properties which are Name1 and Name2.

enter image description here

public class ExampleModel
{
    [Required]
    public UserTest userName1 { get; set; }
    [Required]
    public UserTest userName2 { get; set; }
}

public class UserTest {
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string userName { get; set; }
}


@page "/form-example-1"
@using BlazorAppServer.Model
<h3>FormExample1</h3>

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText id="name" @bind-Value="exampleModel.userName1.userName" />
    <InputText id="name" @bind-Value="exampleModel.userName2.userName" />

    <button type="submit">Submit</button>
</EditForm>

@code {
    private ExampleModel exampleModel = new() { userName1 = new UserTest { userName="asdfgh"}, userName2 = new UserTest { userName="hgfdsa"} };
    private CustomValidation customValidation;

    private void HandleValidSubmit()
    {
        customValidation.ClearErrors();
        var a = exampleModel.userName1.userName;
        var b = exampleModel.userName2.userName;
        var errors = new Dictionary<string, List<string>>();
        if (a == b)
        {
            errors.Add(nameof(exampleModel.userName2.userName), new() { "name2 can't be the same as name1" });
        }
        if (errors.Any())
        {
            customValidation.DisplayErrors(errors);
        }
    }
}
    
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Generic;

namespace BlazorAppServer
{
    public class CustomValidation : ComponentBase
    {
        private ValidationMessageStore messageStore;

        [CascadingParameter]
        private EditContext CurrentEditContext { get; set; }
        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidation)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidation)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            messageStore = new(CurrentEditContext);

            CurrentEditContext.OnValidationRequested += (s, e) =>
                messageStore.Clear();
            CurrentEditContext.OnFieldChanged += (s, e) =>
                messageStore.Clear(e.FieldIdentifier);
        }

        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            foreach (var err in errors)
            {
                messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }

        public void ClearErrors()
        {
            messageStore.Clear();
            CurrentEditContext.NotifyValidationStateChanged();
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1