ASP.NET model binding to base type - asp.net-mvc

I have a BaseViewModel that my View Models all inherit from.
public class MagazineViewModel : BaseOutputViewMode
{
public string TitleOfPublication { get; set; }
}
In my controller I use a factory method to give the corret View Model back based on an input:
// e.g. viewModel contains an instance of MagazineViewModel
BaseOutputViewModel viewModel = BaseOutputViewModel.GetOutputViewModel(output);
When I use TryUpdateModel to try and bind to a FormCollection which I know contains a "TitleOfPublication" key, its never set in my view model:
if (!TryUpdateModel(viewModel, form))
I think this is something to do with the DefaultModelBinder using the BaseOutputViewModel to bind FormCollection keys to - it doesn't contain a "TitleOfPublication", the derived MagazineViewModel does.
I'm trying to roll my own model binder, to override the DefaultModelBinder's BindModel behavior. Its all wired in correctly and I can debug into it straight after the TryUpdateModel call:
public class TestModelBinder : DefaultModelBinder, IFilteredModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Tried the following without success ....
// 1. Quick hardcoded test
// bindingContext.ModelType = typeof(MagazineViewModel);
// 2. Set ModelMetadata, hardcoded test again
// bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MagazineViewModel));
// 3. Replace the entire context
// ModelBindingContext context2 = new ModelBindingContext();
// context2.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MagazineViewModel));
// context2.ModelName = bindingContext.ModelName;
// context2.ModelState = bindingContext.ModelState;
// context2.ValueProvider = bindingContext.ValueProvider;
// bindingContext = context2;
}
}
But I'm not sure how to work with the bindingContext? What needs to be updated so that I can tell the DefaultModelBinder to bind using the derived View Model properties?
Or have I just totally mis-understood this!
I did try overriding CreateModel - much like the DerivedTypeModelBinder in MvcContrib, but I think because I'm giving the binder an instance of a model to work with, CreateModel is never called. Used Reflector on the Mvc DLL, theres a "BindComplexModel" that calls CreateModel only if the model is null:
if (model == null)
{
model = this.CreateModel(controllerContext, bindingContext, modelType);
}
Any pointers greatfully received!
Cheers

OK - finally got to the bottom of this!
In actual fact there was nothing wrong with my model binder, the problem ultimately led back to a couple of input tags that had no name/id:
<input id="" name="" type="text">
The crux was this test in DefaultModelBinder:
// Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
// or by seeing if a value in the request exactly matches the name of the model we're binding.
// Complex type = everything else.
if (!performedFallback) {
ValueProviderResult vpResult =
bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (vpResult != null) {
return BindSimpleModel(controllerContext, bindingContext, vpResult);
}
}
With no id/name, the form collection has a key of "" which means that the GetValue correctly returned the value for that field, continuing to bind as a simple model.
When an id/name are added, the form collection contains no key of "", (which is now the name of my model as we're using TryUpdateModel). This meant the DefaultModelBinder correctly treated my model as complexm successfully binding properties in my derived type!
Cheers

Related

asp.net mvc tryupdate failing on custom viewmodel

I have a viewmodel which is returned from a insert call on a MVC page.
It exposes properties according to the underlying model as well as some calculated properties.
Everything is fine except when I run an if (tryupdateModel(viewModel)). At which point it seems the calculated properties cause an error and it wont pass the if statement?
Is there an annotation I can put on this property to prevent it being checked in the tryupdate?
or how do i determine exactly what it is thats not allowing this to return true?
Is there an annotation I can put on
this property to prevent it being
checked in the tryupdate?
One way to prohibit updating a particular property is to use:
TryUpdateModel(model, null, null, new [] {"SecretProperty"}); // Blacklist
TryUpdateModel(model, new [] {"Prop1", "Prop2", "etc"}); // Whitelist - recommended
The other is to implement custom ModelBinder that reflects on the properties and ignores ones that should not be updated:
public class ApplicationModelBinder : DefaultModelBinder {
protected override PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var allProps = GetTypeDescriptor(controllerContext, bindingContext).GetProperties();
var resulting = new PropertyDescriptorCollection(null);
//Filter out the props with no scaffolding set
foreach(PropertyDescriptor prop in allProps) {
if (ShouldIncludeProperty(bindingContext, prop))
resulting.Add(prop);
}
return resulting;
}
private static bool ShouldIncludeProperty(ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
var doNotScaffold = propertyDescriptor.Attributes.OfType<ScaffoldColumnAttribute>().Any(x => !x.Scaffold);
return !doNotScaffold;
}
}
Then you can apply [ScaffoldColumn(false)] to the property that is not updateable.
I use this approach.
how do i determine exactly what it is thats not allowing this to return true?
After the call to TryUpdateModel you can inspect ModelState property of the controller and find where the errors are.
This will depend on how you are performing validation. If you are using data annotations you need to remove any attributes on the calculated properties. You also need to check which property is failing validation. You may execute the following command after TryUpdateModel:
var errors = ModelState
.Where(x => x.Value.Errors.Count > 0)
.Select(x => new { x.Key, x.Value.Errors })
.ToArray();
This will give you the list of errors and their corresponding properties and might help you understand what is happening and why validation is failing.

How to return an overall Model's value as 'null' from ASP.NET MVC model binder

I have an action method that takes several optional parameters.
This ASP.NET MVC actionmethod looks simple enough but isn't working as I want....
[HttpPost]
public ActionResult UpdateOrder(OrderItem OrderItem, Address ShippingAddress)
{
if (ShippingAddress != null) {
// we have a shipping address
}
}
An Address object is always created for ShippingAddress because - well - thats the way model binders work. Even if ShippingAddress.Address1, ShippingAddress.City etc. fields are absent from the Form an object will still be created and passed to the action.
I want a way to make a model binder that returns null for the model if it is deemed to be empty.
A first attempt goes as follows
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
// get the address to validate
var address = (Address)bindingContext.Model;
// if the address is quintessentially null then return null for the model binder
if (address.Address1 == null && address.CountryCode == null && address.City == null)
{
bindingContext.Model = null;
}
}
Unfortunately this simple solution doesn't work and I get the following error:
InvalidOperationException -This property setter is obsolete, because its value is derived from ModelMetadata.Model now.
Is there a way I can make the overall 'Model' from a custom ModelBinder to return null?
Have you tried setting the default parameter to null? You may also need to set the type to nullable as well, but I'm not 100% sure if it's needed, but that's how I use it.
For example:
public ActionResult UpdateOrder(OrderItem OrderItem, Address? shippingAddress = null)
I should probably note that this requires .NET 4, but then, you didn't specify which version you're running on.

MVC binding form data problem

I am using an object that matches all the fields in my form. I then use the default binding to populate the object in my action, like this;
public ActionResult GetDivisionData(DivisionObj FormData)
My DivisionObj initializes all it's values to string.empty in the constructor.
The problem is that when the binder populates the model from the posted form data, any data that is not posted is set to null in the object, eventhough I initialized the object to contain empty strings.
Is there a way to change this so that unposted data will be an empty string.
This is default behavior of the DefaultModelBinder, more specifically the DataAnnotations framework. ConvertEmptyStringToNull is by default, set to true. You can create your own model binder and replace the default model binder.
public class EmptyStringModelBaseBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
return base.BindModel(controllerContext, bindingContext);
}
}
Then in global...
ModelBinders.Binders.DefaultBinder = new EmptyStringModelBaseBinder();
Though I do wish they had a static way of setting this for the default modelbinder. Maybe in v3 :)
Alternatively, You can also use the [DisplayFormat(ConvertEmptyStringToNull=false)] attribute to set this on a per-property basis.
You can always use [Bind(Exclude="PropertyName1,PropertyName2,PropertyName3")] to exclude some properties from binding:
public ActionResult GetDivisionData([Bind(Exclude="PropertyName1,PropertyName2,PropertyName3")]DivisionObj FormData)
If you really have to have String.Empty in String properties, you can use this binder:
public class EmptyStringModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
if (propertyDescriptor.PropertyType == typeof(String))
propertyDescriptor.SetValue(bindingContext.Model,propertyDescriptor.GetValue(bindingContext.Model) ?? String.Empty);
}
}
You'll also have to run this in global.asax:
ModelBinders.Binders.DefaultBinder = new EmptyStringModelBinder();
I can only confirm that I see the same results as you. Your options are:
One way is to exclude properties as LukLed explained. But this will lead to code duplication and you will have to do it on every controller action every time DivisionObj (or any other Model class that you wish to decorate) appears as action parameter. Soo it's a bit cumbersome...
I am currently in the process of having multiple issues with various custom properties, some that need to be instantiated in contructors, some that need value only known at runtime and others that are also special in some way.
I have determined that for me it's best to go with Custom Model Binder and do most of this stuff there.

Calling UpdateModel with a collection of complex data types reset all non-bound values?

I'm not sure if this is a bug in the DefaultModelBinder class or what.
But UpdateModel usually doesn't change any values of the model except the ones it found a match for.
Take a look at the following:
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Edit(List<int> Ids)
{
// Load list of persons from the database
List<Person> people = GetFromDatabase(Ids);
// shouldn't this update only the Name & Age properties of each Person object
// in the collection and leave the rest of the properties (e.g. Id, Address)
// with their original value (whatever they were when retrieved from the db)
UpdateModel(people, "myPersonPrefix", new string[] { "Name", "Age" });
// ...
}
What happens is UpdateModel creates new Person objects, assign their Name & Age properties from the ValueProvider and put them in the argument List<>, which makes the rest of the properties set to their default initial value (e.g. Id = 0)
so what is going on here?
UPDATE:
I stepped through mvc source code (particularly DefaultModelBinder class) and here is what I found:
The class determines we are trying to bind a collection so it calls the method: UpdateCollection(...) which creates an inner ModelBindingContext that has a null Model property. Afterwards, that context is sent to the method BindComplexModel(...) which checks the Model property for null and creates a new instance of the model type if that is the case.
That's what causes the values to be reset.
And so, only the values that are coming through the form/query string/route data are populated, the rest remains in its initialized state.
I was able to make very few changes to UpdateCollection(...) to fix this problem.
Here is the method with my changes:
internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) {
IModelBinder elementBinder = Binders.GetBinder(elementType);
// build up a list of items from the request
List<object> modelList = new List<object>();
for (int currentIndex = 0; ; currentIndex++) {
string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
if (!DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, subIndexKey)) {
// we ran out of elements to pull
break;
}
// **********************************************************
// The DefaultModelBinder shouldn't always create a new
// instance of elementType in the collection we are updating here.
// If an instance already exists, then we should update it, not create a new one.
// **********************************************************
IList containerModel = bindingContext.Model as IList;
object elementModel = null;
if (containerModel != null && currentIndex < containerModel.Count)
{
elementModel = containerModel[currentIndex];
}
//*****************************************************
ModelBindingContext innerContext = new ModelBindingContext() {
Model = elementModel, // assign the Model property
ModelName = subIndexKey,
ModelState = bindingContext.ModelState,
ModelType = elementType,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
object thisElement = elementBinder.BindModel(controllerContext, innerContext);
// we need to merge model errors up
VerifyValueUsability(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
modelList.Add(thisElement);
}
// if there weren't any elements at all in the request, just return
if (modelList.Count == 0) {
return null;
}
// replace the original collection
object collection = bindingContext.Model;
CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
return collection;
}
Rudi Breedenraed just wrote an excellent post describing this problem and a very helpful solution. He overrides the DefaultModelBinder and then when it comes across a collection to update, it actually updates the item instead of creating it new like the default MVC behavior. With this, UpdateModel() and TryUpdateModel() behavior is consistent with both the root model and any collections.
You just gave me an idea to dig into ASP.NET MVC 2 source code.
I have been struggling with this for two weeks now. I found out that your solution will not work with nested lists. I put a breakpoint in the UpdateCollection method ,and it never gets hit. It seems like the root level of model needs to be a list for this method to be called
This is in short the model I have..I also have one more level of generic lists, but this is just a quick sample..
public class Borrowers
{
public string FirstName{get;set;}
public string LastName{get;set;}
public List<Address> Addresses{get;set;}
}
I guess that, I will need to dig deeper to find out what is going on.
UPDATE:
The UpdateCollection still gets called in asp.net mvc 2, but the problem with the fix above is related to this HERE

Modelbinding database entities in ASPNET MVC

I'm having trouble trying to think what the best way is to recreate a database object in a controller Action.
I want to make use of ModelBinders so in my action I have access to the object via a parameter, rather than having to repeat code to get an object from the database based on an identifier parameter. So I was thinking of having a ModelBinder that performs a call to the dataaccess layer to obtain the original object (or creates a new one if it doesn't exist in the database), then binds any properties to the database object to update it. However I've read that the ModelBinders shouldn't make database queries (first comment of this article).
If the ModelBinder shouldn't perform a database query (so just using the DefaultModelBinder) then what about database objects that have properties that are other db objects? These would never get assigned.
Saving an object after the user has edited it (1 or 2 properties are editable in the view) the ModelBinded object would be missing data, so saving it as it is would result in data in the database being overwritten with invalid values, or NOT-NULL constraints failing.
So, whats the best way to get an object in a controller action from the database bound with the form data posted back from the view?
Note im using NHibernate.
I get the model object from the database, then use UpdateModel (or TryUpdateModel) on the object to update values from the form parameters.
public ActionResult Update( int id )
{
DataContext dc = new DataContext();
MyModel model = dc.MyModels.Where( m => m.ID == id ).SingleOrDefault();
string[] whitelist = new string[] { "Name", "Property1", "Property2" };
if (!TryUpdateModel( model, whitelist )) {
... model error handling...
return View("Edit");
}
ViewData.Model = model;
return View("Show");
}
Unfortunately you don't have control over the construction of the model binder, so you can't inject any repository implementation.
You can reach out directly into a service locator to pull in your repository & fetch the item:
public class ProductBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext, Type modelType)
{
if(modelType != typeof(Product))
return null;
var form = controllerContext.HttpContext.Request.Form;
int id = Int32.Parse(form["Id"]);
if(id == 0)
return base.CreateModel(controllerContext, bindingContext, modelType);
IProductRepository repository = ServiceLocator.Resolve<IProductRepository>();
return repository.Fetch(id);
}
}
You might even make this work for all of your entities if you can use a base class or interface that provides the Id of the class.
You'll have to set this up in Global.asax:
ModelBinders.Binders.Add(typeof(Product), new ProductBinder());
and then you can do this:
public ActionResult Save([Bind] Product product)
{
....
_repository.Save(product);
}
Let me first state that I don't recommend to access database from ModelBinders, as from perspective of Separation Of Concern ModelBinders should only be responsible of interpretting client request, obviously database is not.
If you dont want to repeat your self (DRY), use repositories/services
However if u really want to do it like that, then
In global.asax.cs Register a custom MyModelBinderProvider to MVC
ModelBinderProviders.BinderProviders.Add(new EntityModelBinderProvider
{
ConnectionString = "my connection string"
));
Cunstruct the custom ModelBinderProvider to contain database settings
public class EntityBinderProvider: IModelBinderProvider
{
public string ConnectionString { get; set; }
public IModelBinder GetBinder(Type modelType)
{
if (Is known entity)
return new EntityBinder(ConnectionString);
else
return null;
}
}
Follow further instructions from Ben Scheirman
You don't actually have to hit the database. Simply setting the Id of the objects will be enough to set the relationship up, but watch your cascades. Make sure your cascde settings won't update the related object as it will clear the values.

Resources