MVC Attributes - ReadOnly vs Editable vs Bind & the DefaultModelBinder
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:
- 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.
- 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!