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.
Related
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 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.
#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; }
}
}
I have the following viewModel:
namespace Flashcard.Models
{
public class CreateCardViewModel
{
[HiddenInput(DisplayValue = false)]
public int SetId { get; set; }
[Required]
public ICollection<Side> Sides { get; set; }
}
}
I use this ViewModel against the Card Controller:
public class CardController : Controller
{
//
// GET: /Card/
public ActionResult Create(int setId)
{
var model = new CreateCardViewModel();
var side = new Side() {Content = "Blank Side"};
model.SetId = setId;
model.Sides.Add(side);
return View(model);
}
}
However when I call the Create action, I get a nullReferenceException because model.Sides is null, which does not seem to be the same as empty. I believe I created an empty ICollection Sides in the ViewModel - why is it null in the controller?
For some context - a Card can have one or several Sides. I'm trying to always add a Side whenever a Card is created.
you need to initiate a Collection and assign it to the property of your object as follows:
public class CardController : Controller
{
//
// GET: /Card/
public ActionResult Create(int setId)
{
var model = new CreateCardViewModel();
var side = new Side() {Content = "Blank Side"};
model.SetId = setId;
model.Sides = new List<Side>();
model.Sides.Add(side);
return View(model);
}
}
Your collection is null. make one and assign to your prop.
I am building a test application using MVC3, Razor, and Entity Framework 4.1 with a schema-first approach (as apposed to a code-first approach), in a repository pattern. I would like to avoid accessing data objects in my view, and access a model instead, but I am having a problem. As far as I can tell, the data objects are being returned from the data layer as ObjectSet, but my View needs IEnumerable, and I don't know how to cast one to the other.
Here is some code, to help clarify.
Model ...
namespace TestSolution.Models
{
public class ProjectModel
{
[HiddenInput]
public int Id { get; set; }
[Required]
[StringLength(255, ErrorMessage = "The name cannot be more than 255 characters long.")]
[Display(Name = "Name")]
public string Name { get; set; }
[Required]
[Display(Name = "Description")]
public string Description { get; set; }
}
}
Repository ...
public IQueryable<ProjectModel> GetProjects()
{
return Db.Project;
}
Entities ...
public ObjectSet<Project> Project
{
get
{
if ((_Project == null))
{
_Project = base.CreateObjectSet<Project>("Project");
}
return _Project;
}
}
Controller ...
public ActionResult Index()
{
IEnumerable<TestSolution.Models.ProjectModel> model = _projectRepository.GetProjects();
return View(model);
}
View ...
#model IEnumerable<TestSolution.Models.ProjectModel>
Error I am getting when building ...
Cannot implicitly convert type 'System.Data.Objects.ObjectSet<TestSolution.Project>' to 'System.Linq.IQueryable<TestSolution.Models.ProjectModel>'. An explicit conversion exists (are you missing a cast?)
Does this question make sense? I am just not sure where go from here ... any advise you guys can give me would be awesome. :)
EDIT: I was able to solve this with Kyle's suggestion by changing my Repository code to ...
public IQueryable<ProjectModel> GetProjects()
{
return Db.Project.Select(i => new ProjectModel() { Id = i.Id, Name = i.Name, Description = i.Description });
}
The problem isn't converting from ObjectSet<T> to IEnumerable<T> (ObjectSet<T> implements IEnumerable<T>).
The problem is converting from TestSolution.Project to TestSolution.Models.ProjectModel. You will need to write some conversion code, maybe something similar to the below:
model.Select(i => new ProjectModel() { /* Set properties here. */ });