Blazor ObjectGraphDataAnnotationsValidator issue - validation

I am using ObjectGraphDataAnnotationsValidator and ValidateComplexType to validate form.
After the focus out from InputText, the validation reports an error regardless of whether the field is filled in correctly.
invalid form
When I validate form with EditContext.Validate(), validation works as expected.
valid form
InputText binds Name property from dto object:
(For testing purposes, I have set the identifiers to the classes.)
public class TestDto
{
private string _name;
public string InstanceId { get; private set; }
public string ViewModelInstanceId { get; private set; }
Required(ErrorMessage = "Name fieild is required.")]
public string Name
{
// after focus out event on InputText, first call of this getter is from old empty (new) instance created on OnInitialized
// ViewModelInstanceId is always the same, as expected
get => _name;
set => _name = value;
}
public TestDto(string viewModelInstanceId)
{
ViewModelInstanceId = viewModelInstanceId;
InstanceId = Guid.NewGuid().ToString();
}
}
My razor page
<EditForm EditContext="EditContext">
<ObjectGraphDataAnnotationsValidator />
<ValidationSummary />
<p>
<InputText #bind-Value="ViewModel.TestDto.Name" />
<ValidationMessage For="()=>ViewModel.TestDto.Name" />
</p>
<p>
<button #onclick="()=>ViewModel.ValidateForm?.Invoke()">Validate form</button>
</p>
</EditForm>
#code{
protected EditContext EditContext { get; set; } = null!;
[ValidateComplexType]
protected TestViewModel ViewModel { get; private set; } = null!;
protected override void OnInitialized()
{
ViewModel = new TestViewModel();
//If this line is removed, everything works as expected
ViewModel.TestDto = new TestDto(ViewModel.InstanceIdId) //Instance1
{
Name = string.Empty//this makes validation to fail because it is required field
};
ViewModel.ValidateForm = () => EditContext.Validate();
EditContext = new EditContext(ViewModel);//Validates form as expected
base.OnInitialized();
}
protected override void OnAfterRender(bool firstRender)
{
if(firstRender){
ViewModel.LoadTestDto();//Instance2
StateHasChanged();
}
base.OnAfterRender(firstRender);
}
}
View model
public class TestViewModel
{
public string InstanceId { get; private set; }
public string PageTitle => "Test page";
public Func<bool> ValidateForm { get; set; }
[ValidateComplexType]
public TestDto TestDto { get; set; }
public TestViewModel()
=> InstanceId = Guid.NewGuid().ToString();
public void LoadTestDto()
{
TestDto = new TestDto(InstanceId)//Instance2
{
Name = "Loaded name"
};
}
}
So, I have decide to test TestDto.Name getter and setter.
After focus out from InputText, those were hitted breakpoints on Name getter and setter:
Name setter => setted new entered value to Instance2 (created on OnAfterRender)
Name getter => returns empty value from Instance1 ?!? (created on OnInitialized)
Name getter => returns new entered value from Instance2 (created on OnAfterRender)
...
Any ideas? I am brainwashed :D and probably overlooked something :/
P.S. In case when TestDto instance is setted only during OnAfterRendering event, everything works as expected, but that isn't desired scenario.
EDIT:
Why am I creating empty instance of TestDto on OnInitialized?
Because I can not set #bind-Value of nullable object.
Something like this:
<InputText #bind-Value="ViewModel?.TestDto?.Name" />
I know I can hide form like:
#if(ViewModel.TestDto != null)
{
<InputText #bind-Value="ViewModel.TestDto.Name" />
}
but I want to show empty form before data is loaded.

Reassigning EditContext after new TestDto instance is setted to TestViewModel.TestDto property fixes all..
So, on TestViewModel.TestDto setter I had to invoke method from razor base class to reassign EditContext property
TestViewModel
[ValidateComplexType]
public TestDto TestDto
{
get => _testDto;
set
{
testDto= value;
ReassignEditContext?.Invoke();
}
}
Razor.cs
private void ReassignEditContext()
=> EditContext = new EditContext(TestViewModel);
I just can't believe that is right way to do it...
Does anyone have better idea?
Similar test project can be found on https://github.com/CashPJ/EditFormValidationTest

Related

Xamarin forms crashed android project, when i use object property (get-set)

I ran into a problem while creating a project. If I use properties (get;set;), the android application crashes at the point of assigning a value to the property.
For example: I created a clean xamarin project to remove the influence of my code.
Property in my class:
public class Item
{
public string Id
{
get { return Id; }
set { Id = value; }
}
}
Property use:
public AboutPage()
{
Item gg = new Item();
gg.Id = "test";
InitializeComponent();
}
App crashes at line:
set { Id = value; }
Error not show.
Error
Help. This is the first time I've seen this. I have downgraded the platform. Used clean projects. What am I doing wrong?
UPD: link to my solution
You could try to change the property like below:
public class Item
{
public string Id { get; set; }
}
or
public class Item
{
private string id;
public string Id
{
get { return id; }
set { id = value; }
}
}
when you impement the INotifyPropertyChanged interface:
public class Item : INotifyPropertyChanged
{
private string id;
public string Id
{
get { return id; }
set { id = value; OnPropertyChanged("Id"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
When you do this:
public string Id
{
get { return Id; }
set { Id = value; }
}
You are essentially creating an infinite loop. This is why your App crashes. You are infinitely calling the setter.
Instead you would either make it an auto property:
public string Id { get; set; }
or add a backing field for the property:
private string _id;
public string Id
{
get => _id;
set => _id = value;
}

Custom remote validations for complex models in blazor?

I am currently using <ObjectGraphDataAnnotationsValidator/> to validate complex models.
So far so good, except that there is also a requirement to check against the database to see if a record with the same value already exists.
I have tried implementing the <CustomValidator/> as per advised in https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#validator-components
However, it seems to only work for the top level properties.
And the <ObjectGraphDataAnnotationsValidator/> does not work with remote validations (or does it!?)
So say that I have:
*Parent.cs*
public int ID {get;set;}
public List<Child> Children {get;set;}
*Child.cs*
public int ID {get;set;}
public int ParentID {get;set}
public string Code {get;set;}
<EditForm Model="#Parent">
.
.
.
Child.Code has a unique constraint in the database.
I want to warn users "This 'Code' already exists! Please try entering a different value.", so that no exceptions will be thrown.
For now, I am a bit lost as to where my next step is.
In the past with asp.net core mvc, I could achieve this using remote validations.
Is there an equivalent to remote validations in blazor?
If not, what should I do to achieve the same result, to remotely validate the sub properties for complex models?
Any advises would be appreciated. Thanks!
[Updated after #rdmptn's suggestion 2021/01/24]
ValidationMessageStore.Add() accepts the struct FieldIdentifier, meaning that I can simply add a overload of the CustomValidator.DisplayErrors to make it work:
public void DisplayErrors(Dictionary<FieldIdentifier, List<string>> errors)
{
foreach (var err in errors)
{
messageStore.Add(err.Key, err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}
Full example below:
#using Microsoft.AspNetCore.Components.Forms
#using System.ComponentModel.DataAnnotations
#using System.Collections.Generic
<EditForm Model="parent" OnSubmit="Submit">
<ObjectGraphDataAnnotationsValidator></ObjectGraphDataAnnotationsValidator>
<CustomValidator #ref="customValidator"></CustomValidator>
<ValidationSummary></ValidationSummary>
#if (parent.Children != null)
{
#foreach (var item in parent.Children)
{
<div class="form-group">
<label>Summary</label>
<InputText #bind-Value="item.Code" class="form-control"></InputText>
</div>
}
}
<input type="submit" value="Submit" class="form-control"/>
</EditForm>
#code{
private CustomValidator customValidator;
private Parent parent;
public class Parent
{
public int Id { get; set; }
[ValidateComplexType]
public List<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public string Code { get; set; }
}
protected override void OnInitialized()
{
parent = new Parent()
{
Id = 1,
Children = new List<Child>()
{
new Child()
{
Id = 1,
ParentId = 1,
Code = "A"
},
new Child()
{
Id = 1,
ParentId = 1,
Code = "B"
}
}
};
}
public void Submit()
{
customValidator.ClearErrors();
var errors = new Dictionary<FieldIdentifier, List<string>>();
//In real operations, set this when you get data from your db
List<string> existingCodes = new List<string>()
{
"A"
};
foreach (var child in parent.Children)
{
if (existingCodes.Contains(child.Code))
{
FieldIdentifier fid = new FieldIdentifier(model: child, fieldName: nameof(Child.Code));
List<string> msgs = new List<string>() { "This code already exists." };
errors.Add(fid, msgs);
}
}
if (errors.Count() > 0)
{
customValidator.DisplayErrors(errors);
}
}
}
The [Remote] validation attribute is tied to MVC and is not usable for Blazor.
ObjectGraphDataAnnotationsValidator is not enough. In addition, each property, that represents an object with possible validation needs to be decorated with a [ValidateComplexType] attribute.
In your CustomValidatior, you can see DI to get your API service to call your API and validate your constraint.
public class Parent
{
...other properties...
[ValidateComplexType]
public List<Child> Children {get; set; }
}
public class Child
{
...other properties...
[Required]
[IsUnique(ErrorMessage = "This 'Code' already exists! Please try entering a different value.")]
public String Code {get; set;}
}
public class IsUniqueAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var service = (IYourApiService)validationContext.GetService(typeof(IYourApiService));
//unfortunately, no await is possible inside the validation
Boolean exists = service.IsUnique((String)value);
if(exists == false)
{
return ValidationResult.Success;
}
return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
}
}
You might want to check out FluentValidation as this library provide features for asynchronous validation. I'm not sure if this validator can be used inside Blazor WASM.

swagger swashbuckle does not support nested class as action method parameter

I am using asp.net 5
I have two model class, which are nested, both of the inner class are named Command
public class EditModel
{
public class Command
{
public int Id { get; set; }
public string Info { get; set; }
}
}
and
public class CreateModel
{
public class Command
{
public int Id { get; set; }
public string Info { get; set; }
}
}
In my Controller class has two methods
[HttpPost]
public IActionResult PutData(CreateModel.Command model)
{
return Ok();
}
[HttpPut]
public IActionResult PostData(EditModel.Command model)
{
return Ok();
}
Since for both Put and Post's query I am using nested class both name Command, Swagger will return the following error
An unhandled exception has occurred while executing the request.
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Conflicting method/path combination "PUT Test" for actions -
TestSwagger.Controllers.TestController.PutData
(TestSwagger),TestSwagger.Controllers.TestController.PostData
(TestSwagger). Actions require a unique method/path combination for
Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable1 apiDescriptions, SchemaRepository schemaRepository) at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable1
apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String
documentName, String host, String basePath)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext
httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext
context)
Swagger will work, if I change one of the Command model name to something different.
Yet, I believe this nested class model name is legit and should work with swagger also. If there a way to work around this. Thanks
By adding c.CustomSchemaIds(x => x.FullName);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TestSwagger", Version = "v1" });
c.CustomSchemaIds(x => x.FullName);
});
solved the schemaId conflict. Thanks to this question

Retrieve model name in custom display name attribute

Here is my development requirement,
My label values are stored in the database, and I still want to use the data annotation in a declarative way, this is to make my model more readable.
And here is my approach,
I decided to write custom DisplayNameAttribute, where the default value provided by my model will be overwritten by the value retrieved from the database.
Here is the property defined in the model,
[CustomDisplay(Name: "First Name")]
[CustomRequired(ErrorMessage: "{0} is required")]
public String FirstName { get; set; }
Here is the custom display name attribute class,
public class CustomDisplayAttribute : DisplayNameAttribute
{
private string _defaultName;
private string _displayName;
public CustomDisplayAttribute(string Name)
{
_defaultName = Name;
}
public override string DisplayName
{
get
{
if (String.IsNullOrEmpty(_displayName))
{
_displayName = DAO.RetrieveValue(**ModelName**, _defaultName);
}
return _displayName;
}
}
}
Now, you can see in the above code, ModelName is something I need, but I don't have!!
While debugging, I dig into ModelMetadataProviders.Current and can see the availability of the current model in action. But, as it is part of non-public static members I am unable to access it through my code.
I have written the below method to retrieve the model name through reflection,
private static string GetModelName()
{
var modelName = String.Empty;
FieldInfo info = typeof(CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>)
.GetField("_typeIds", BindingFlags.NonPublic | BindingFlags.Static);
var types = (ConcurrentDictionary<Type, string>)info.GetValue(null);
modelName = types.FirstOrDefault().Key.Name;
return modelName;
}
But the problem is, the types collection provides me entries for all the models (visited at least once by the user). And there is no clue to know, which is currently in action!!
IMHO Attributes should not be used to make database calls. Attributes should be used to add metadata to Classes/Properties etc...
So If you're willing to change your code to be more like the Microsoft architecture for MVC then you'd have your custom Attribute and a custom ModelMetadataProvider:
public class CustomDisplayAttribute : Attribute
{
public CustomDisplayAttribute(string name)
{
Name = name;
}
public string Name { get; private set; }
}
Then a new ModelMetadataProvider:
public class DatabaseModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public DatabaseModelMetadataProvider()
{
}
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var displayAttribute = containerType == null
? null as CustomDisplayAttribute
: containerType.GetProperty(propertyName)
.GetCustomAttributes(false)
.OfType<CustomDisplayAttribute>()
.FirstOrDefault();
if (displayAttribute != null)
{
var displayValue = DAO.RetrieveValue(containerType.ToString(), displayAttribute.Name)
metadata.DisplayName = displayValue;
}
return metadata;
}
}
Where
public class MyViewModel
{
public MyPropertyType PropertyName { get; set; }
}
containerType = MyViewModel
modelType = MyPropertyType
propertyName = PropertyName
Then register the provider (global.asax or whatever):
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider();
Also you can take a look at the ModelMetadata it has a few other things you might want to change in the future.

IValidatableObject passes validation but StringLength is Invalid

I have a test class with a couple tests that check to see if the entity IsValid. I moved to using IValidatableObject from having my own custom validation but I'm stuck with the correct validation technique.
This is my Test class:
[TestFixture]
public class StudentTests {
private static Student GetContactWithContactInfo()
{
return new Student(new TestableContactRepository())
{
Phone = "7275551111"
};
}
private static Student GetContactWithoutContactInfo()
{
return new Student(new TestableContactRepository());
}
[Test]
public void Student_Saving_StudentHasInfo_IsValid ()
{
// Arrange
Student student = GetContactWithContactInfo();
// Act
student.Save();
// Assert
Assert.IsTrue(student.IsValid);
}
[Test]
public void Student_Saving_StudentDoesNotHaveInfo_IsNotValid ()
{
// Arrange
Student student = GetContactWithoutContactInfo();
// Act
student.Save();
// Assert
Assert.IsFalse(student.IsValid);
}
}
This is my entity:
public class Student : IValidatableObject
{
private readonly IContactRepository contactRepository;
public Student(IContactRepository _contactRepository)
{
contactRepository = _contactRepository;
Contacts = new List<Student>();
}
[Required]
public int Id { get; private set; }
[StringLength(10, MinimumLength = 10)]
public string Phone { get; set; }
public List<Student> Contacts { get; private set; }
public bool IsValid { get; private set; }
public void Save()
{
if (IsValidForPersistance())
{
IsValid = true;
Id = contactRepository.Save();
}
}
private bool IsValidForPersistance()
{
return Validator.TryValidateObject(this, new ValidationContext(this), null, true);
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrEmpty(Phone) && Contacts.All(c => string.IsNullOrEmpty(c.Phone)))
yield return new ValidationResult("The student or at least one contact must have a phone number entered", new[] { "Phone Number" });
}
}
As you can see the tests test for IsValid by calling the IsValidForPersistance. Validate will eventually have more validation .
The above tests all pass using this method but this test below also passes but should not.
[Test]
public void Student_Saving_HasContactInfoWithInvalidLength_IsNotValid()
{
// Arrange
Contact student = GetContactWithoutContactInfo();
student.Phone = "string";
// Act
student.Save();
// Assert
Assert.IsFalse(student.IsValid);
}
Here I'm setting my own Phone value of an invalid length string. I expect validation to fail because of the StringLength annotation set at min and max 10 characters.
Why is this passing?
Update
There was a problem with the custom validation, updated the code with the change. Along with the suggestion from nemesv about not having a private modifier on the Phone property it now works. I've updated all the code to working.
Validator.TryValidateObject only checks the RequiredAttributes (and also other things like type level attributes and IValidatableObject implementation) by default.
If you need to validate all the attributes like StringLength etc. you need to set the validateAllProperties parameter of the method to true
private bool IsValidForPersistance() {
return Validator.TryValidateObject(this,
new ValidationContext(this),
null,
true /* validateAllProperties */);
}

Resources