An example, if I have a class named Order with a field referencing a Customer, and then an Order form with an drop down list (<%= Html.DropDownListFor(e => e.Customer.ID, new SelectList(...)) %>) for setting the Customer the model binder will create an empty Customer with only the ID set. This works fine with NHibernate but when validation is added to some of the fields of the Customer class, the model binder will say these fields are required. How could I prevent the model binder from validating these references?
Thanks!
Ages old question, but I figured I'd answer anyways for posterity. You need a custom model binder in this situation to intercept that property before it attempts to bind it. The default model binder will recursively attempt to bind properties using their custom binder, or the default one if not set.
The override you're looking for in DefaultModelBinder is GetPropertyValue. This is called over all properties in the model, and by default it calls back to DefaultModelBinder.BindModel - the entry point to the whole process.
Simplified model:
public class Organization
{
public int Id { get; set; }
[Required]
public OrganizationType Type { get; set; }
}
public class OrganizationType
{
public int Id { get; set; }
[Required, MaxLength(30)]
public string Name { get; set; }
}
View:
<div class="editor-label">
#Html.ErrorLabelFor(m => m.Organization.Type)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.Organization.Type.Id, Model.OrganizationTypes, "-- Type")
</div>
Model Binder:
public class OrganizationModelBinder : DefaultModelBinder
{
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
if (propertyDescriptor.PropertyType == typeof(OrganizationType))
{
var idResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Id");
if (idResult == null || string.IsNullOrEmpty(idResult.AttemptedValue))
{
return null;
}
var id = (int)idResult.ConvertTo(typeof(int));
// Can validate the id against your database at this point if needed...
// Here we just create a stub object, skipping the model binding and
// validation on OrganizationType
return new OrganizationType { Id = id };
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
Note that in the View we create the DropDownListFor the model.foo.bar.Id. In the model binder, ensure that this is added to the model name as well. You can leave it off of both, but then DropDownListFor has a few issues finding the selected value without pre-selecting it in the SelectList you send it.
Finally, back in the controller, be sure to attach this property in your database context (if you're using Entity Framework, others might handle differently). Otherwise, it isn't tracked and the context will attempt to add it on save.
Controller:
public ActionResult Create(Organization organization)
{
if (ModelState.IsValid)
{
_context.OrganizationTypes.Attach(organization.Type);
_context.Organizations.Add(organization);
_context.SaveChanges();
return RedirectToAction("Index");
}
// Build up view model...
return View(viewModel);
}
Related
I have a PUT Rest API that I want to do binding from both body and route parameters.
Code
[HttpPut("{Id}/someStuffApi")]
public ActionResult UpdateStatus([FromBody] StatusRequest StatusRequest)
{
// code ...
}
And the model class is
public class StatusRequest
{
[FromRoute(Name = "Id")]
[Required(ErrorMessage = "'Id' attribute is required.")]
public string Id { get; set; }
[FromBody]
[Required(ErrorMessage = "'Status' attribute is required.")]
public string Status { get; set; }
}
When I made a request to this API, the Id is not mapped to the model even though I added the FromRoute attribute explicitly. Any suggestions?
The [FromBody] model binding will effectively override the [FromRoute] option in your model class. This is by design (why, I'm not sure, but an MS decision). See the "[FromBody] attribute" section of this doc: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding. As pointed out there: "When [FromBody] is applied to a complex type parameter, any binding source attributes applied to its properties are ignored." So adding the "[FromRoute]" attribute inside your model does nothing...it's ignored. You can remove both of those attributes from your model.
So the way around this is to put the route binding in the Put action as a method parameter and then manually add it to your model in the controller before using the model.
[HttpPut("{Id}/someStuffApi")]
public ActionResult UpdateStatus(int Id, [FromBody] StatusRequest StatusRequest)
{
StatusRequest.Id = Id;
// remaining code...
}
The downside to this method is that the Required attribute cannot remain on the Id parameter. It will be null at the time of model binding and if you have .Net Core 3.1 automatic model validation active, then that will always return a 422. So if you would have to manually check that yourself before adding it to the model.
If you want even more flexibility, you can look at something like the HybridModelBinding NuGet package that allows various combinations of model binding using attributes. But this is a 3rd party dependency that you may not want. (https://github.com/billbogaiv/hybrid-model-binding/)
You can use custom model binding,here is a demo:
TestModelBinderProvider:
public class TestModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> formatters;
private readonly IHttpRequestStreamReaderFactory readerFactory;
public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
this.formatters = formatters;
this.readerFactory = readerFactory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(StatusRequest))
return new StatusBinder(formatters, readerFactory);
return null;
}
}
Startup.cs:
services.AddMvc()
.AddMvcOptions(options =>
{
IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
});
StatusBinder:
public class StatusBinder: IModelBinder
{
private BodyModelBinder defaultBinder;
public StatusBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
defaultBinder = new BodyModelBinder(formatters, readerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// callinng the default body binder
await defaultBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
var data = bindingContext.Result.Model as StatusRequest;
if (data != null)
{
var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
data.Id = value.ToString();
bindingContext.Result = ModelBindingResult.Success(data);
}
}
}
}
StatusRequest:
public class StatusRequest
{
[Required(ErrorMessage = "'Id' attribute is required.")]
public string Id { get; set; }
[Required(ErrorMessage = "'Status' attribute is required.")]
public string Status { get; set; }
}
Action:
[HttpPut("{Id}/someStuffApi")]
public ActionResult UpdateStatus(StatusRequest StatusRequest)
{
return Ok();
}
result:
I'm new to ASP.net, MVC4 and EF and having trouble making the leap from relatively simple samples to something (only slightly) more complex. I have simplified my model for the question here so consider the following. Forgive me if I have some of the terminology incorrect.
I have an Employee object and a Division object. An employee can be in many divisions and a division can have many employees.
public class Employee
{
public int EmployeeId { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
public virtual ICollection<Division> Divisions { get; set; }
}
public class Division
{
public int DivisionId { get; set; }
public String DivisionName { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
}
I have these mapped in the context file via the following:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasMany(e => e.Divisions)
.WithMany(d => d.Employees)
.Map(m =>
{
m.ToTable("EmployeesDivisionsId");
m.MapLeftKey("EmployeeId");
m.MapRightKey("DivisionId");
});
}
For my View Controller I have the following. I have used a ViewBag (what a name) for many to one relationships and they have worked fine, so I'm trying to modify that to work with the many to many here:
public ActionResult Index()
{
return View(db.Employees).ToList());
}
//
// GET: /Employees/Details/5
public ActionResult Details(int id = 0)
{
Employee employee = db.Employees.Find(id);
if (employee == null)
{
return HttpNotFound();
}
return View(employee);
}
//
// GET: /Employees/Create
public ActionResult Create()
{
ViewBag.Divisions = new MultiSelectList(db.Divisions, "DivisionId", "DivisionName");
return View();
}
//
// POST: /Employees/Create
[HttpPost]
public ActionResult Create(Employee employee)
{
ViewBag.Divisions = new MultiSelectList(db.Divisions, "DivisionId", "DivisionName");
if (ModelState.IsValid)
{
db.Employees.Add(employee);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(employee);
}
Finally in the Edit View I have the following code. Again for a many to one relationship, a simple DropDownList works fine with the ViewBag in the controller, and with this many to many multiselect/ListBox method the 'Divisions' show up in the view, however when the Save button is hit, the validation of '1, 2' is not valid is displayed.
<div class="editor-label">
#Html.LabelFor(model => model.Divisions)
</div>
<div class="editor-field">
#Html.ListBox("Divisions")
#Html.ValidationMessageFor(model => model.Divisions)
</div>
So as I understand it, the list of 'Divisions' are being grabbed properly from the database and selected correctly in the view but when saving to the database the association through the mapped relationship isn't being made.
So my questions are:
How do I make this work so when I save, the correct 'Divisions' are saved to the Employee?
Also, I've heard people don't like using ViewBags, is there a (better?) alternate way?
This answer maybe a bit late. However, here's one or two things that might be helpful to others finding this question.
You are using ListBox() instead of ListBoxFor() so the data isn't being automatically bound to your model. So in your controller you need:
[HttpPost]
public ActionResult Create(Employee employee, string[] Divisions)
{
// iterate through Divisions and apply to your model
...
}
I have the following scenario.
I have the Edit/Employee view populated with a model from an Entity Framework entity (Employee)
I post from Edit/Employee to the Save/Employee controller action. The Save/Employee action expect another type (EmployeeSave) which has Employee as property
This is the Edit/Employee method
public ActionResult Edit(EmployeesEdit command)
{
var employee = command.Execute();
if (employee != null)
{
return View(employee);
}
return View("Index");
}
This is the Save/Employee method
public ActionResult Save(EmployeesSave command)
{
var result = command.Execute();
if (result)
{
return View(command.Employee);
}
return View("Error");
}
This is the EmployeeSave class
public class EmployeesSave
{
public bool Execute()
{
// ... save the employee
return true;
}
//I want this prop populated by my model binder
public Employee Employee { get; set; }
}
The MVC DefaultModelBinder is able to resolve both Employee and EmployeeSave classes.
You might need to use BindAttribute here. If your view contains the properties of the EmployeeSaveViewModel and Employee named like this (I made up property names)
<input type="text" name="EmployeeSaveViewModel.Property1" />
<input type="text" name="EmployeeSaveViewModel.Employee.Name" />
<input type="text" name="EmployeeSaveViewModel.Employee.SomeProperty" />
Then, your action could look like this:
[HttpPost]
public ActionResult Save([Bind(Prefix="EmployeeSaveViewModel")]
EmployeeSaveViewModel vm)
{
if(ModelState.IsValid)
{
// do something fancy
}
// go back to Edit to correct errors
return View("Edit", vm);
}
You could resolve it by passing the edited data back to Edit action that handles HttpPost. Inside create EmployeeSave object and assign its Employee property the value of Employee returned to yout Edit action. Call Save action by passing EmployeeSave object.
[HttpGet]
public ActionResult Edit()
{
return View();
}
[HttpPost]
public ActionResult Edit(Employee employee)
{
EmployeeSave employeeSave = new EmployeeSave { Employee = employee };
return View("Save", employeeSave);
}
Another method would be to use EmployeeSave instead of Employee as your model.
I'm using the NerdDinner MVC1 code. Created a viewmodel called DinnerFormViewModel:
public class DinnerFormViewModel
{
public Dinner Dinner { get; private set; }
public SelectList Countries { get; private set; }
public DinnerFormViewModel(Dinner dinner)
{
Dinner = dinner;
Countries = new SelectList(PhoneValidator.Countries, dinner.Country);
}
}
I pass to my edit view fine which uses strongly typed helpers in MVC2. Passing back however:
public ActionResult Edit(int id, FormCollection collection)
{
Dinner dinnerToUpdate = dr.GetDinner(id);
try
{
UpdateModel(dinnerToUpdate, "Dinner"); // using a helper becuase of strongly typed helpers to tell it what to update
// updates the properites of the dinnerToUpdate object using incoming form parameters collection.
// UpdateModel automatically populates ModelState when it encounters errors
// works by when trying to assign BOGUS to a datetime
// dinnerToUpdate.Country = Request.Form["Countries"];
dr.Save(); // dinner validation may fail here too due to hook into LINQ to SQL via Dinner.OnValidate() partial method.
return RedirectToAction("Details", new { id = dinnerToUpdate.DinnerID });
}
This works, however my Country doesn't get updated, becuase I'm only giving the hint to UpdateModel to update the Dinner.
Question: How to get the country saving?
Thanks.
I have searched like a fool but does not get much smarter for it..
In my project I use Entity Framework 4 and own PoCo classes and I want to use DataAnnotations for validation. No problem there, is how much any time on the Internet about how I do it. However, I feel that it´s best to have my validation in ViewModels instead and not let my views use my POCO classes to display data.
How should I do this smoothly? Since my repositories returns obejekt from my POCO classes I tried to use AutoMapper to get everything to work but when I try to update or change anything in the ModelState.IsValid is false all the time..
My English is really bad, try to show how I am doing today instead:
My POCO
public partial User {
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
And my ViewModel
public class UserViewModel {
public int Id { get; set; }
[Required(ErrorMessage = "Required")]
public string UserName { get; set; }
[Required(ErrorMessage = "Required")]
public string Password { get; set; }
}
Controller:
public ActionResult Edit(int id) {
User user = _userRepository.GetUser(id);
UserViewModel mappedUser = Mapper.Map<User, UserViewModel>(user);
AstronomiGuidenModelItem<UserViewModel> result = new AstronomiGuidenModelItem<UserViewModel> {
Item = mappedUser
};
return View(result);
}
[HttpPost]
public ActionResult Edit(UserViewModel viewModel) {
User user = _userRepository.GetUser(viewModel.Id);
Mapper.Map<UserViewModel, User>(viewModel, user);
if (ModelState.IsValid) {
_userRepository.EditUser(user);
return Redirect("/");
}
AstronomiGuidenModelItem<UserViewModel> result = new AstronomiGuidenModelItem<UserViewModel> {
Item = viewModel
};
return View(result);
}
I've noticed now that my validation is working fine but my values are null when I try send and update the database. I have one main ViewModel that looks like this:
public class AstronomiGuidenModelItem<T> : AstronomiGuidenModel {
public T Item { get; set; }
}
Why r my "UserViewModel viewModel" null then i try to edit?
If the validation is working, then UserViewModel viewModel shouldn't be null... or is it that the client side validation is working but server side isn't?
If that's the case it could be because of the HTML generated.
For instance, if in your view you have:
<%: Html.TextBoxFor(x => x.Item.UserName) %>
The html that gets rendered could possibly be:
<input name="Item.UserName" id="Item_UserName" />
When this gets to binding on the server, it'll need your action parameter to be named the same as the input's prefix (Item). E.g.
public ActionResult Edit(UserViewModel item) {
To get around this, do as above and change your action parameter to item OR you could encapsulate the form into a separate PartialView which takes the UserViewModel as it's model - that way the Html.TextBoxFor won't be rendered with a prefix.
HTHs,
Charles
Ps. If I'm totally off track, could you please post some code for the view.