dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

Blazor EditForm validation only partially works when using a 'Validator component' (within a 'business logic validation' function) #30740

Closed RichardBr closed 3 years ago

RichardBr commented 3 years ago

Describe the bug

The issue is I'm not able to get a validation message to show against a field. Although the error message does appear in the validation summary.

blazorValidationIssue8Mar21

This issue is also further restricted to only complex model properties. The approach of using "ValidateComplexType", "ObjectGraphDataAnnotationsValidator" to resolve complex model property validation limitation does not help in this instance.

To Reproduce

The sample code below which I have created to demonstrate the problem is virtually ALL code from the Microsoft docs website (https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation). I have attached the complete source code solution. Simply rebuild the solution and run it to test. BlazorTest1.zip

Index.razor


@page "/"
@using System.ComponentModel.DataAnnotations;
<h1>Starfleet Starship Database</h1>

New Ship Entry Form

@**@ @* does not work!*@

Captain's Name

@message

Star Trek, ©1966-2019 CBS Studios, Inc. and Paramount Pictures

@code { private bool disabled; private string message; private string messageStyles = "visibility:hidden"; private CustomValidator customValidator; private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };

protected override void OnInitialized()
{
    base.OnInitialized();

    customValidator = new CustomValidator();

    starship = new Starship()
    {
        ProductionDate = DateTime.UtcNow,
        Identifier = "1",
        Classification = "Defense",
        MaximumAccommodation = 100,
        IsValidatedDesign = true,
        CaptainsName = new PersonsName { Firstname = "John", Lastname = "Kirk" }
    };
}

private async Task HandleValidSubmit(EditContext editContext)
{
    bool isValid = editContext.Validate(); // Data Annotations validation

    if (isValid)
    {
        BusinessLogicValidation();
    }
}

private bool BusinessLogicValidation()
{
    customValidator.ClearErrors();

    var errors = new Dictionary<string, List<string>>();

    if (starship.Classification == "Defense" &&
            string.IsNullOrEmpty(starship.Description))
    {
        errors.Add(nameof(starship.Description),
            new List<string>() { "For a 'Defense' ship classification, 'Description' is required." });
    }

    if (starship.CaptainsName.Firstname != "James")
    {
        //errors.Add(nameof(starship.CaptainsName.Firstname), new List<string>() { "Firstname must be James" });  // does not work (as expected)!
        errors.Add("CaptainsName.Firstname", new List<string>() { "Firstname must be James" });
    }

    if (errors.Count() > 0)
    {
        customValidator.DisplayErrors(errors);
        return true;
    }
    return false;
}

}

> Starship.cs

using System; using System.ComponentModel.DataAnnotations;

namespace BlazorTest1.Client.Shared { public class Starship { [Required] [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")] public string Identifier { get; set; }

    public string Description { get; set; }

    [Required]
    public string Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }

    // [ValidateComplexType]      // does not work!
    public PersonsName CaptainsName { get; set; }
}

}

> PersonsName.cs

namespace BlazorTest1.Client.Shared { public class PersonsName { public string Firstname { get; set; } public string Lastname { get; set; } } }



### Further technical details
- .NET 5

- The output of `dotnet --info`

.NET SDK (reflecting any global.json):
 Version:   5.0.200
 Commit:    70b3e65d53

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19042
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\5.0.200\

Host (useful for support):
  Version: 5.0.3
  Commit:  c636bbdc8a

.NET SDKs installed:
  2.1.202 [C:\Program Files\dotnet\sdk]
  2.1.521 [C:\Program Files\dotnet\sdk]
  3.1.100-preview2-014569 [C:\Program Files\dotnet\sdk]
  3.1.200-preview-014883 [C:\Program Files\dotnet\sdk]
  3.1.300 [C:\Program Files\dotnet\sdk]
  3.1.406 [C:\Program Files\dotnet\sdk]
  5.0.100-rc.1.20452.10 [C:\Program Files\dotnet\sdk]
  5.0.102 [C:\Program Files\dotnet\sdk]
  5.0.103 [C:\Program Files\dotnet\sdk]
  5.0.200-preview.20601.7 [C:\Program Files\dotnet\sdk]
  5.0.200-preview.21077.7 [C:\Program Files\dotnet\sdk]
  5.0.200 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.All 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.22 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.23 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.24 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.22 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.23 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.24 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-preview7.19365.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-rc1.19457.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.0-preview2.19528.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.18 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.22 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.23 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.24 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.25 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-rc1-19456-20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.0-preview2.19525.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.0-preview2.19525.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.9 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.12 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET runtimes or SDKs:
  https://aka.ms/dotnet-download
mkArtakMSFT commented 3 years ago

Thanks for contacting us. It looks like you're missing some logic to add the validation messages in the right place in Validation Context. Here is a sample which would help you with that: https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#validator-components

SteveSandersonMS commented 3 years ago

Specifically, please see the part where it calls messageStore.Add.

RichardBr commented 3 years ago

@SteveSandersonMS and @mkArtakMSFT - I am still unable to see what you are suggesting, I'm doing wrong/missing!

My validator component code, as shown below (CustomValidator.cs), is copied straight out from https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#validator-components. It is also the only place on that web page where code messageStore.Add is mentioned

CustomValidator.cs


using System;
using System.Collections.Generic;

using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms;

namespace BlazorTest1.Client.Shared { public class CustomValidator : ComponentBase { private ValidationMessageStore messageStore;

    [CascadingParameter]
    private EditContext CurrentEditContext { get; set; }

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

        messageStore = new ValidationMessageStore(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();
    }
}

}

My next chunk of code, in `index.razor` (shown above in the original bug log), is **heavily based** on the code from [https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#business-logic-validation](url) . The only real difference is that I have added an additional validation check...

...

    if (starship.CaptainsName.Firstname != "James")
    {
        //errors.Add(nameof(starship.CaptainsName.Firstname), new List<string>() { "Firstname must be James" });  // does not work (as expected)!
        errors.Add("CaptainsName.Firstname", new List<string>() { "Firstname must be James" });
    }

...



When I don't use a complex model property, my solution works!
Please explain before closing this issue.
Thank you.
SteveSandersonMS commented 3 years ago

errors.Add("CaptainsName.Firstname", ... ... messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);

This is a problem. This adds the message under a field called CaptainsName.Firstname, which is absolutely wrong since none of the properties have that name (with a dot in the middle - it's not even a valid C# property name). The code should look like the following:

messageStore.Add(FieldIdentifier.Create(starship.CaptainsName, "Firstname"), "Your message goes here");

... or:

messageStore.Add(FieldIdentifier.Create(() => starship.CaptainsName.Firstname), "Your message goes here");

The field identifier is an objectInstance, propertyName pair. This is how it gets matched up with an input that reads the same property from the same object instance.

RichardBr commented 3 years ago

@SteveSandersonMS - Beautiful solution that works perfectly. I'm so impressed with your answer, I am planning on asking the people, who are responsible for the doc at https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#business-logic-validation to modify it to incorporate this approach. I feel it would help a lot of community members.