Shazwazza

Shannon Deminick's blog all about .Net, Umbraco & Web development

MVC Attributes - ReadOnly vs Editable vs Bind & the DefaultModelBinder

December 6, 2010 21:13 by Shannon Deminick

I’ve been learning quite a lot about the DefaultModelBInder and it’s internal behaviour because of the complexities involved with some of the model binding in Umbraco v5 and wanted to point out the behaviour of the following attributes when it comes to how the DefaultModelBinder works:

Each of these attributes ‘seems’ like they serve the same purpose by making properties of your model read only, not-bindable, not-editable or vice versa. In my mind they all seem like they are the same but that’s just my opinion, as there could be some other hidden secret in the .Net framework which differentiates between these explicitly. The DefaultModelBinder definitely doesn’t treat each of these the same, in fact the DefaultModelBinder doesn’t check for the Editable attribute on properties. Before the DefaultModelBinder binds all of your model’s properties (If you have a complex model), it creates a new ModelBindingContext which contains a new PropertyFilter to exclude any properties that have been defined as ReadOnly or not bindable.

There’s a few internal/private methods of the DefaultModelBinder that do these lookups:

This one creates the new model binding context and checks if any BindAttributes have been declared for the model, if so, then it creates a new property filter which combines the BindAttribute filter and the standard model binder filter:

internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) { BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)]; Predicate<string> newPropertyFilter = (bindAttr != null) ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName) : bindingContext.PropertyFilter; ModelBindingContext newBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType), ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, PropertyFilter = newPropertyFilter, ValueProvider = bindingContext.ValueProvider }; return newBindingContext; }

This method is called to do the actual binding of each property, but as you can see it only binds properties that have been filtered:

private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) { IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext); foreach (PropertyDescriptor property in properties) { BindProperty(controllerContext, bindingContext, property); } }

The GetFilteredModelProperties method gets all of the unfiltered PropertyDescriptor’s of the model, and then calls ShouldUpdateProperty to perform the actual filtering (which passes in the new ModelBindingContext’s PropertyFilter which is now based on the Bind attribute if one was declared on the model):

protected IEnumerable<PropertyDescriptor> GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) { PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext); Predicate<string> propertyFilter = bindingContext.PropertyFilter; return from PropertyDescriptor property in properties where ShouldUpdateProperty(property, propertyFilter) select property; }

The ShouldUpdateProperty method does the following:

  1. Check if the PropertyDescriptor is read only. This is determined by whether or not the ReadOnly attribute was specified on the model property, the Editable attribute doesn’t seem to affect this value. The first check also makes a call to CanUpdateReadonlyTypedReference which checks for value types such as guid, int, string, and ensures they are excluded if IsReadOnly is true.
  2. Check if the property filter allows the property to be bound which is based on the Bind attribute (if supplied on the model)
private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) { if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) { return false; } // if this property is rejected by the filter, move on if (!propertyFilter(property.Name)) { return false; } // otherwise, allow return true; }

So the result of what can actually be bound in the DefaultModelBinder is:

  • Any property that doesn’t have [ReadOnly(true)] specified
  • Any property that is referenced as an Include using the Bind attribute on the model containing the property:
    • [Bind(Include = “MyProperty”)]
  • Any property that is NOT referenced as an Exclude using the Bind attribute on the model containing the property:
    • [Bind(Exclude = “NotThisProperty”)]

So it appears that even if you set the an editable attribute as [Editable(false)], this property will still be included for binding.

The DefaultModelBinder is quite a crazy beast and contains a lot of functionality under the hood. What I’ve mentioned above is based purely on experience and looking at the source code so if you have any feedback or feel that any of this is in error please comment!

Custom MVC ModelBinder with Complex Models/Objects/Interfaces using built in MVC Validation

September 28, 2010 22:49 by Shannon Deminick

I’ve been creating some cool stuff using ASP.Net MVC 3 lately and came across a situation where I’d like to have quite a complex model/object bound to an Action on my Controller based on a set of posted values from a form. In order to do this, a custom ModelBinder is necessary to collect the data from the posted values, turn it into my custom object, and bind that object to my Action’s parameter. The easy part is to write code to turn the posted values into my custom object and return it, the tricky part is trying to get the in-built back-end MVC validation working for my model… which is currently using DataAnnotations. I really didn’t feel like writing my own logic to validate my model against DataAnnotations and also didn’t want to write the logic to take into account that DataAnnotations might not be the current developers validation provider of choice. So after much digging through the source of MVC and using Reflector, I finally found the solution.  To better describe the concept, here’s an example of the issue:

Each IMyObject has many properties: IProperty. Each IProperty is of a type: IType and each IType has a model which is used to render out the editor in MVC (EditorFor)

public interface IMyObject { int Id { get; } string Name { get; } IEnumerable<IProperty> Properties { get; } } public interface IProperty { Guid Id { get; set; } string Alias { get; set; } string Name { get; set; } IType DataType { get; } } public interface IType { Guid Id { get; } string Name { get; } string Alias { get; } object EditorModel { get; } }

So my controller’s Action looks something like this:

public override ActionResult Edit(IMyObject obj) { if (!ModelState.IsValid) return View(obj); //Save some data and do other stuff... }

Initially my model binder looks like this which is the ‘easy’ part that converts the posted values into an IMyObject object with all of it’s values filled in:

[ModelBinderType(typeof(IMyObject))] public class EditorModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.Equals(typeof(IMyObject))) { //get the id from the posted values var id = (int)controllerContext .RouteData .Values .GetRequiredObject("id"); //get the object from my data repository by id var model = GetMyObjectFromMyDataRepository(id); foreach (var p in item.Properties) { //it's the editor model that created the editor //for each property in mvc which means that //it's this data that is being posted back var editorModel = p.DataType.EditorModel; // ... Go convert all of the posted values using the bindingContext // ValueProvider and build up the MyObject object created above // ... (A bunch of code goes here to do the conversion) // Except, now that it's created, how the heck do i run it through // MVC validation? } return model; } return base.CreateModel(controllerContext, bindingContext, modelType); } }

In order for the call in your controller to check if your ModelState.IsValid, something needs to do the validation of the model and put the validation results inside the ModelState object. This of course already exists in the MVC framework and is done with the DefaultModelBinder. The DefaultModelBinder is pretty smart and can figure out how to automagically parse and transform the posted values into the specified model and also run it through the MVC validators so long as the model is simple enough to figure out. When your model consists of interfaces, it generally can’t do much about it because it doesn’t know how to create your interface. It also has problems when the model is complex and contains sub objects of sub objects (like the IMyObject). So how do we tap in to this underlying functionality?

[ModelBinderType(typeof(IMyObject))] public class EditorModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.Equals(typeof(IMyObject))) { //get the id from the posted values var id = (int)controllerContext .RouteData .Values .GetRequiredObject("id"); //get the object from my data repository by id var model = GetMyObjectFromMyDataRepository(id); foreach (var p in item.Properties) { //it's the editor model that created the editor //for each property in mvc which means that //it's this data that is being posted back var editorModel = p.DataType.EditorModel; //get a binder for the EditorModel IModelBinder binder = this.Binders .GetBinder(model.GetType()); //create a new context for it ModelBindingContext customBindingContext = new ModelBindingContext(); //get the meta data for it customBindingContext.ModelMetadata = ModelMetadataProviders .Current .GetMetadataForType(() => model, model.GetType()); //ensure we use our correct field 'prefix' //(this is optional and depends on if you are using a custom prefix) customBindingContext.ModelName = p.Id.ToString(); //use our existing model state customBindingContext.ModelState = bindingContext.ModelState; //use our existing value provider customBindingContext.ValueProvider = bindingContext.ValueProvider; //do the binding! this will also validate and put the errors into the ModelState for us. model = (object)binder.BindModel(controllerContext, customBindingContext); } return model; } return base.CreateModel(controllerContext, bindingContext, modelType); } }

The concept above is pretty much how a Controller’s TryUpdateModel method works and how it does the underlying validation.

Recent Tweets

Twitter May 14, 14:14
blogged: script to fix media paths when changing vdirs or media root paths in #umbraco http://t.co/w1csEtceu1

Twitter May 10, 18:27
@darrenferguson probably better discussed on a forum thread I think :-) @sitereactor

Twitter May 10, 17:55
@darrenferguson and won't work in LB cuz a servers file will b stale when it goes to sleep @sitereactor

Twitter May 10, 17:53
@darrenferguson sorry, bad wording... You'd have to write that feature but it'd be pretty simple @drobar @sitereactor

Twitter May 10, 08:34
@sitereactor ... wouldn't work for LB scenarios though @darrenferguson @drobar

Follow me