In our Forms and Form Validation in Blazor article, we have learned how to validate a form using a non-complex model. But what about a complex model validation, is it the same process? Well, in this article, we are going to answer that question. We will learn how to implement complex model validation in Blazor and also how to apply a compare validation. Both of these validation processes require a bit different logic comparing to what we learned in the mentioned article.
Let’s start.
Inspecting the Starting Project with Form Validation Prepared
In the start folder, we are going to find a starting project for this article with a basic validation implemented following the steps from the mentioned article – Forms and Form Validation in Blazor.
Let’s just inspect the project so it could be easier to follow along with the rest of the article:
We can see two projects – ComplexModelValidation
and Shared
. In the Shared
project, we have the Product
model class and installed System.ComponentModel.Annotations
library to support our form validation process:
public class Product { [Required(ErrorMessage = "Name is required")] public string Name { get; set; } [Required(ErrorMessage = "Supplier is required")] public string Supplier { get; set; } }
We have added a reference from the Shared
project to the main project, modified the Index.razor
file, and added the Index.razor.cs
partial class:
@page "/" <EditForm Model="_product" OnValidSubmit="Submit" style="width:600px;"> <DataAnnotationsValidator /> <div class="form-group row"> <label for="name" class="col-md-2 col-form-label">Name:</label> <div class="col-md-10"> <InputText id="name" class="form-control" @bind-Value="_product.Name" /> <ValidationMessage For="@(() => _product.Name)" /> </div> </div> <div class="form-group row"> <label for="supplier" class="col-md-2 col-form-label">Supplier:</label> <div class="col-md-10"> <InputText id="supplier" class="form-control" @bind-Value="_product.Supplier" /> <ValidationMessage For="@(() => _product.Supplier)" /> </div> </div> <div class="row"> <div class="col-md-12 text-right"> <button type="submit" class="btn btn-success">Submit</button> </div> </div> </EditForm>
public partial class Index { private Product _product = new Product(); private void Submit() => Console.WriteLine($"{_product.Name}, {_product.Supplier}"); }
If you start the app and click the submit button, you are going to see that the validation works:
So, everything works fine, but what about a complex model validation?
Complex Model Validation in Blazor
As you can see, we have a simple Product
model and with it, our validation works without a problem. But let’s see, what is going to happen if we modify our Product
class to be a complex model.
For that, we are going to create another class in the Shared
folder:
public class ProductDetails { [Required(ErrorMessage = "Description is required")] public string Description { get; set; } }
For our example, this is going to be just enough.
Now, we can modify the Product
class:
public class Product { [Required(ErrorMessage = "Name is required")] public string Name { get; set; } [Required(ErrorMessage = "Supplier is required")] public string Supplier { get; set; } public ProductDetails ProductDetails { get; set; } }
Finally, let’s modify the Index.razor
file, to include the input field for this new property:
@page "/" <EditForm Model="_product" OnValidSubmit="Submit" style="width:600px;"> <DataAnnotationsValidator /> <div class="form-group row"> <label for="name" class="col-md-2 col-form-label">Name:</label> <div class="col-md-10"> <InputText id="name" class="form-control" @bind-Value="_product.Name" /> <ValidationMessage For="@(() => _product.Name)" /> </div> </div> <div class="form-group row"> <label for="supplier" class="col-md-2 col-form-label">Supplier:</label> <div class="col-md-10"> <InputText id="supplier" class="form-control" @bind-Value="_product.Supplier" /> <ValidationMessage For="@(() => _product.Supplier)" /> </div> </div> <div class="form-group row"> <label for="description" class="col-md-2 col-form-label">Description:</label> <div class="col-md-10"> <InputText id="description" class="form-control" @bind-Value="_product.ProductDetails.Description" /> <ValidationMessage For="@(() => _product.ProductDetails.Description)" /> </div> </div> <div class="row"> <div class="col-md-12 text-right"> <button type="submit" class="btn btn-success">Submit</button> </div> </div> </EditForm>
Once we are done, we can test this.
Let’s start the app.
As soon as we do that, we are going to get an error message that object reference is not set to an instance of an object. That’s because our Product
model contains an additional property of the ProductDetails
type and for the successful Blazor validation process, we need that instance as well. So, to solve this, we can instantiate this class in the Product.cs
file or in the Index.razor.cs
file. We prefer doing this in the file where the validation logic is, so, let’s modify the Index.razor.cs
file:
public partial class Index { private Product _product = new Product { ProductDetails = new ProductDetails() }; private void Submit() => Console.WriteLine($"{_product.Name}, {_product.Supplier}"); }
Now, we can start our app again, and click the Submit
button:
As a result, we can see that the Description
field hasn’t been validated at all.
So, what is the problem?
First of all, the DataAnnotationsValidator
component doesn’t support complex model validation. To support this, we have to use another component named ObjectGraphDataAnnotationsValidator
. This brings us to the second point, which is that our System.ComponentModel.Annotations
library doesn’t support the complex model validation.
That said, let’s install a new library in the Shared
project that supports this behavior:
PM> Install-Package Microsoft.AspNetCore.Components.DataAnnotations.Validation -Version 3.2.0-rc1.20223.4
This library fills the validation gaps for the Annotations library that we already have installed. Even though it is still in the experimental release, Microsoft suggests using it currently as is. This might change in future releases. Also, this library supports .NET Standard version 2.1 so if you have a class library project with the 2.0 version, just increase the version of the project.
After the package installation, we can replace the DataAnnotationsValidator
component with the ObjectGraphDataAnnotationsValidator
component:
<EditForm Model="_product" OnValidSubmit="Submit" style="width:600px;"> <ObjectGraphDataAnnotationsValidator /> <div class="form-group row"> <label for="name" class="col-md-2 col-form-label">Name:</label> <div class="col-md-10"> <InputText id="name" class="form-control" @bind-Value="_product.Name" /> <ValidationMessage For="@(() => _product.Name)" /> </div> </div> ...
Finally, we have to decorate the ProductDetails
property in the Product
class:
public class Product { [Required(ErrorMessage = "Name is required")] public string Name { get; set; } [Required(ErrorMessage = "Supplier is required")] public string Supplier { get; set; } [ValidateComplexType] public ProductDetails ProductDetails { get; set; } }
That is it. We can start our app now, and test it:
At this point, we can see that our validation works for all the fields.
Compare Validation in Blazor
Since we have installed a new library to support our validation, we can use another functionality it brings to the table. That’s the compare validation.
Microsoft suggests that for the Blazor form validation, we shouldn’t be using the regular [Compare]
attribute to compare values of different properties. Well, let’s just quote Microsoft’s official statement: “The CompareAttribute doesn’t work well with the DataAnnotationsValidator component because it doesn’t associate the validation result with a specific member. This can result in inconsistent behavior between field-level validation and when the entire model is validated on a submit“.
What they suggest is using a new attribute named CompareProperty
, which is a direct replacement for the Compare
attribute.
So, let’s see how it works.
Let’s add a new property in the Product
class:
public class Product { [Required(ErrorMessage = "Name is required")] public string Name { get; set; } [Required(ErrorMessage = "Supplier is required")] public string Supplier { get; set; } [CompareProperty("Supplier", ErrorMessage = "Comparation failed")] public string ConfirmSupplier { get; set; } [ValidateComplexType] public ProductDetails ProductDetails { get; set; } }
And, let’s modify the Index.razor
file by adding another section right bellow the Supplier section:
<div class="form-group row"> <label for="confirm" class="col-md-2 col-form-label">Confirm:</label> <div class="col-md-10"> <InputText id="confirm" class="form-control" @bind-Value="_product.ConfirmSupplier" /> <ValidationMessage For="@(() => _product.ConfirmSupplier)" /> </div> </div>
That’s all it takes.
If we enter different values for the Supplier and Confirm:
We can see our validation error. Of course, if we make them the same, the error message will disappear:
Excellent.
Everything works as expected.
Conclusion
In this article, we have learned:
- How to implement a complex model validation in Blazor
- Which component to use to validate complex models
- What attribute to use for the compare validation
If you combine this article with our Form Validation article and Custom Form Validation article, you are going to have quite good knowledge about form validations in Blazor applications.
Until the next article.
Best regards.
Hi, great article! I stumble upon a case where i need the ObjectGraphDataAnnotationsValidator.
But what if we want to compare the properties of two nested objects, the ones with [ValidateComplexType] ?
Let’s we put two ProductDetails object in Product.
Now I want to compare ProductDetails1.Description and ProductDetails2.Description.
Do you have an elegant way to share? I did this but it involved setting Funcs inside the nested Objects or setting the field as invalid manually (just the css border as red in my case).
I really didn’t have such a use case. So, I am not currently sure what would be the best way to solve it. It would probably take some sort of custom validation logic.