I initially posted this issue to GitHub here: https://github.com/aspnet/Mvc/issues/8723
There's a GitHub repository with a repro of the problem here:
https://github.com/Costo/aspnetcore-binding-bug
I'm using ASP.NET Core 2.2 Preview 3.
When using a custom model binder (with the [ModelBinder] attribute) on properties of an array of "child" models, the model binding phase of the request goes into an infinite loop. See this screenshot:
The custom model binder works well if used on top level model properties, but I'd like to understand why it doesn't work when used in an array of child models. Any help with this would be appreciated.
Thank you !
Here's the code of the model, controller, view and custom binder:
The Model:
public class TestModel
{
public TestInnerModel[] InnerModels { get; set; } = new TestInnerModel[0];
[ModelBinder(BinderType = typeof(NumberModelBinder))]
public decimal TopLevelRate { get; set; }
}
public class TestInnerModel
{
public TestInnerModel()
{
}
[ModelBinder(BinderType = typeof(NumberModelBinder))]
public decimal Rate { get; set; }
}
The custom model binder (intentionally simplified to do nothing special):
public class NumberModelBinder : IModelBinder
{
private readonly NumberStyles _supportedStyles = NumberStyles.Float | NumberStyles.AllowThousands;
private DecimalModelBinder _innerBinder;
public NumberModelBinder(ILoggerFactory loggerFactory)
{
_innerBinder = new DecimalModelBinder(_supportedStyles, loggerFactory);
}
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
return _innerBinder.BindModelAsync(bindingContext);
}
}
The controller:
public class HomeController : Controller
{
public IActionResult Index()
{
return View(new TestModel
{
TopLevelRate = 20m,
InnerModels = new TestInnerModel[]
{
new TestInnerModel { Rate = 2.0m },
new TestInnerModel { Rate = 0.2m }
}
});
}
[HttpPost]
public IActionResult Index(TestModel model)
{
return Ok();
}
}
The Razor view:
#model TestModel;
<form asp-controller="Home" asp-action="Index" method="post" role="form">
<div>
<input asp-for="#Model.TopLevelRate" type="number" min="0" step=".01" />
</div>
<div>
#for (var i = 0; i < Model.InnerModels.Length; i++)
{
<input asp-for="#Model.InnerModels[i].Rate" type="number" min="0" step=".01" />
}
</div>
<input type="submit" value="Go" />
</form>
A solution was posted to the GitHub issue:
#Costo The problem is you're not informing the model binding system that the binder uses value providers. The ComplexTypeModelBinder always believes data is available for the next TestInnerModel instance and the outermost binder (CollectionModelBinder) keeps going -- forever. To fix this,
[MyModelBinder]
public decimal Rate { get; set; }
private class MyModelBinderAttribute : ModelBinderAttribute
{
public MyModelBinderAttribute()
: base(typeof(NumberModelBinder))
{
BindingSource = BindingSource.Form;
}
}
Put another way, the BindingSource.Custom default [ModelBinder] uses isn't correct in this scenario. Fortunately custom model binders on properties of POCO types in containers should be one of the very few cases where this matters.
Related
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
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.
#model IEnumerable<SportsStore.Domain.Entities.Product>.....this is my view List.cshtml
#{
ViewBag.Title = "Products";
}
#foreach (var j in Model) //getting error at Model
{
<div class="item">
<h3>#j.Name</h3>
#j.Description
<h4>#j.Price.ToString("C")</h4>
</div>
}
the sportstore example in pro Asp.Net MVC3 book chapter 7 before the part where i can prepare a database
this is my controller
public class ProductController : Controller
{
public ProductController()
{
}
//
// GET: /Product/
private IProductRepository repository;
public ProductController(IProductRepository productrepository)
{
repository=productrepository;
}
public ViewResult List()
{
return View();
}
when you pass IEnumerable you should pass it as List or something like that, for example your Index Action Method should be like :
public ActionResult Index()
{
return View(_dbContext.Product.ToList());
}
You need to initialize the Model I suppose, just like we do in C#:
Suppose we have a class, then we initialize the same like:
Model m1= new Model();
Make sure you check the null also for all items you are passing.
In the EFDbContext class i inherited DbConntext and that worked for me, hope it'll help you too:
using SportsStore.Domain.Entities;
using System.Data.Entity;
namespace SportsStore.Domain.Concrete
{
public class EFDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
}
}
This seems simple enough but I am missing something:
Model:
public class MainModel
{
public SubModel oSubmodel = new Submodel();
....
}
View:
#model myApp.Models.MainModel
#using (Html.BeginForm("Index","Account", FormMethod.Post, new { id = "form"})
{
#Html.LabelFor(m => m.oSubmodel.prop1
#Html.TextBoxFor(m => m.oSubmodel.prop1
}
Controller:
[HttpPost]
public ActionResult Index(MainModel oModel)
{
....
string prop = oModel.prop <-----------ok
string prop1 = oModel.oSubmodel.prop1 <----------null
}
The m.oSubmodel.prop1 data is display correctly in the view. When the data is posted back to the controller MainModel values are passed correctly, however - all submodel values are null.
Anybody give any insight?
Right OK. My Bad. The subModel need to be exposed as property off the main model for binding to work correctly on post:
So
public class MainModel
{
public SubModel oSubmodel = new Submodel();
....
}
becomes:
public class MainModel
{
public SubModel oSubmodel { get; set; }
....
}
Binding then works great. Thanks to those that responded.
I have this model:
public class QuestionSimple
{
public string Body { get; set; }
public bool IsSingleChoice { get; set; }
public List<String> Answers { get; set; }
public string Difficutly { get; set; }
public string Explanation { get; set; }
}
Which I try to bind using this line in the Global.asax.cs
ModelBinders.Binders.Add(typeof(QuestionSimple), new AddQuestionSimpleBinder());
...With this binder
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Get the raw attempted value from the value provider
string key = bindingContext.ModelName;
ValueProviderResult val = bindingContext.ValueProvider.GetValue(key);
//val is ALWAYS NULL
return null;
}
But val is alsways null.
Here is the View which should return (and actually does return) list of answers, when I'm not using my binder.
#using (Html.BeginForm("AddQuestionSimple", "Topic", FormMethod.Post, new { #id = "mainForm" }))
{
<input type="text" name="questionToBeAdded.Answers[0]" value="ff" />
<input type="text" name="questionToBeAdded.Answers[1]" value="ddds" />
<input type="text" name="questionToBeAdded.Answers[2]" value="ff" />
<input type="text" name="questionToBeAdded.Answers[3]" value="ddds" />
<input type="text" name="questionToBeAdded.Answers[4]" value="ff" />
<input type="text" name="questionToBeAdded.Answers[5]" value="ddds" />
<input value="Add question" type="submit" style="position: static; width: 10em; height: 3em;
font-size: 1em;" />
}
The default model-binder does get my values when I post them, but my val is always null.
Why is that so?
(should mention this is a try of solving this bigger problem).
Edit 1:
Here is the supposed to be bound action
[HttpPost]
public ActionResult AddQuestionSimple(PMP.WebUI.Models.UserInteractionEntities.UserInput.QuestionSimple questionToBeAdded)
{
return View("AddQuestion");
}
Thanks.
First, you have to override the BindModel, so your method would begin with:
public override object BindModel
Next, you won't find a value with the key "questionToBeAdded" in your bindingContext.ValueProviders. Debug and look at that collection, bindingContext.ValueProviders[1] has a FormValueProvider that has all the properties of your model in there. The way you're doing it, you might have to iterate through that manually. Another way to do it is just to override the BindProperty method, just type:
protected override void BindProperty
In your model binder class and you'll get the method signature filled in (assuming you're inheriting from DefaultModelBinder). In this method you'll get more detail about each property, the piping is done for you.