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.
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.
I'm struggling with adding products into product table for a specific user, whiich is logged via log method. The issue is that the attribue userLogin loses his value and is not equal to the user who logged into. So the actual value for attribute userLogin in addProduct method is null, hence there is an exception.
#RequestMapping("/home")
public String home(Model model) {
model.addAttribute("customer", new Customer());
model.addAttribute("userLogin", new Customer());
return "register";
}
#PostMapping("/save")
public String save(#ModelAttribute(value = "customer") #Valid Customer customer, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
throw new NotValidInputException();
}
if (customerService.checkIfCustomerIsInDb(customer)) {
throw new CustomerAlreadyExists();
}
customerService.saveCustomerIntoDb(customer);
return "saved";
}
#ExceptionHandler(NotValidInputException.class)
public String notValidInputExceptionHandler() {
return "databaseError";
}
#ExceptionHandler(CustomerAlreadyExists.class)
public String customerAlreadyInDbHandler() {
return "customerError";
}
#RequestMapping("/log")
public String login(Model model, #ModelAttribute(name = "userLogin") Customer customerFromLoginForm) {
if (Objects.isNull(customerService.getUserByLoggingDetails(customerFromLoginForm))) {
return "userNotFound";
}
model.addAttribute("product", new Product());
return "logged";
}
#PostMapping(value = "/addProduct")
public String addProduct(#ModelAttribute("userLogin") Customer customerFromLoginForm, #ModelAttribute(value = "product") Product product) {
// customer is null
customerFromLoginForm = customerService.findCustomerById(customerFromLoginForm.getId());
product.setCustomer(customerFromLoginForm);
customerFromLoginForm.setProducts(Arrays.asList(product));
productService.addProduct(product);
return "logged";
}
The form in logged. html
<form th:method="post" th:action="#{addProduct}" th:object="${product}">
<input type ="text" th:field="*{name}" placeholder="Name" /><br />
<input type ="text" th:field="*{category}" placeholder="Category" /><br />
<input type="number" th:field="*{price}" placeholder="Price" /><br />
<input style="text-align: center" type="submit" value="Add Product" /> </form>
Not sure what I'm missing here
See this answer: Not able to pass model Attribute passed from controller to thymeleaf html back to another controller .
In general, I would use a hidden input for it, e.g:
<input type ="hidden" th:name="userLogin" th:value="${userLogin}" />
Then your controller method should get it. Let me know if this helps.
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.
Hello I am trying to to use spring-form.tld + spring MVC but I can't figure out how to solve this problem. Let's say I have two classes:
public class Person {
private String name;
public String getname() {...}
public void setname(String name) {...}
}
public class City {
private String name;
public String getname() {...}
public void setname(String name) {...}
}
In both of them is property with same name - "name".
Now I got a jsp with two forms:
...
<form:form name="person" modelAttribute="person">
<form:label path="name">Person</form:label>
<form:input path="name" />
<input type="submit" value="send"/>
</form:form>
<form:form name="city" modelAttribute="city" method="post">
<form:label path="name">City</form:label>
<form:input path="name" />
<input type="submit" value="send"/>
</form:form>
...
and controller which serves my requests:
...
#RequestMapping(method = { RequestMethod.POST })
public ModelAndView handle(#ModelAttribute City city,
#ModelAttribute Person person) {
ModelAndView mav = new ModelAndView("test.jsp");
mav.addObject("city", city);
mav.addObject("person", person);
return mav;
}
...
Problem is that if I post person form the attribute name is inserted into person object but also into city. This example is nonsense but it illustrates my problem. I would like to somehow "bind" the person form with person object.
Thanks for any advice!