I have the following controller action:
[HttpPost]
public ViewResult DoSomething(MyModel model)
{
// do something
return View();
}
Where MyModel
looks like this:
public class MyModel
{
public string PropertyA {get; set;}
public IList<int> PropertyB {get; set;}
}
So DefaultModelBinder should bind this without a problem. The only thing is that I want to use special/custom binder for binding PropertyB
and I also want to reuse this binder. So I thought that solution would be to put a ModelBinder attribute before the PropertyB which of course doesn't work (ModelBinder attribute is not allowed on a properties). I see two solutions:
To use action parameters on every single property instead of the whole model (which I wouldn't prefer as the model has a lot of properties) like this:
public ViewResult DoSomething(string propertyA, [ModelBinder(typeof(MyModelBinder))] propertyB)
To create a new type lets say MyCustomType: List<int>
and register model binder for this type (this is an option)
Maybe to create a binder for MyModel, override BindProperty
and if the property is "PropertyB"
bind the property with my custom binder. Is this possible?
Is there any other solution?
override BindProperty and if the
property is "PropertyB" bind the
property with my custom binder
That's a good solution, though instead of checking "is PropertyB" you better check for your own custom attributes that define property-level binders, like
[PropertyBinder(typeof(PropertyBBinder))]
public IList<int> PropertyB {get; set;}
You can see an example of BindProperty override here.
I actually like your third solution, only, I would make it a generic solution for all ModelBinders, by putting it in a custom binder that inherits from DefaultModelBinder
and is configured to be the default model binder for your MVC application.
Then you would make this new DefaultModelBinder
automatically bind any property that is decorated with a PropertyBinder
attribute, using the type supplied in the parameter.
I got the idea from this excellent article: http://aboutcode.net/2011/03/12/mvc-property-binder.html.
I'll also show you my take on the solution:
My DefaultModelBinder
:
namespace MyApp.Web.Mvc
{
public class DefaultModelBinder : System.Web.Mvc.DefaultModelBinder
{
protected override void BindProperty(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor)
{
var propertyBinderAttribute = TryFindPropertyBinderAttribute(propertyDescriptor);
if (propertyBinderAttribute != null)
{
var binder = CreateBinder(propertyBinderAttribute);
var value = binder.BindModel(controllerContext, bindingContext, propertyDescriptor);
propertyDescriptor.SetValue(bindingContext.Model, value);
}
else // revert to the default behavior.
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
IPropertyBinder CreateBinder(PropertyBinderAttribute propertyBinderAttribute)
{
return (IPropertyBinder)DependencyResolver.Current.GetService(propertyBinderAttribute.BinderType);
}
PropertyBinderAttribute TryFindPropertyBinderAttribute(PropertyDescriptor propertyDescriptor)
{
return propertyDescriptor.Attributes
.OfType<PropertyBinderAttribute>()
.FirstOrDefault();
}
}
}
My IPropertyBinder
interface:
namespace MyApp.Web.Mvc
{
interface IPropertyBinder
{
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, MemberDescriptor memberDescriptor);
}
}
My PropertyBinderAttribute
:
namespace MyApp.Web.Mvc
{
public class PropertyBinderAttribute : Attribute
{
public PropertyBinderAttribute(Type binderType)
{
BinderType = binderType;
}
public Type BinderType { get; private set; }
}
}
An example of a property binder:
namespace MyApp.Web.Mvc.PropertyBinders
{
public class TimeSpanBinder : IPropertyBinder
{
readonly HttpContextBase _httpContext;
public TimeSpanBinder(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
MemberDescriptor memberDescriptor)
{
var timeString = _httpContext.Request.Form[memberDescriptor.Name].ToLower();
var timeParts = timeString.Replace("am", "").Replace("pm", "").Trim().Split(':');
return
new TimeSpan(
int.Parse(timeParts[0]) + (timeString.Contains("pm") ? 12 : 0),
int.Parse(timeParts[1]),
0);
}
}
}
Example of the above property binder being used:
namespace MyApp.Web.Models
{
public class MyModel
{
[PropertyBinder(typeof(TimeSpanBinder))]
public TimeSpan InspectionDate { get; set; }
}
}
@jonathanconway's answer is great, but I would like to add a minor detail.
It's probably better to override the GetPropertyValue
method instead of BindProperty
in order to give the validation mechanism of the DefaultBinder
a chance to work.
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
PropertyBinderAttribute propertyBinderAttribute =
TryFindPropertyBinderAttribute(propertyDescriptor);
if (propertyBinderAttribute != null)
{
propertyBinder = CreateBinder(propertyBinderAttribute);
}
return base.GetPropertyValue(
controllerContext,
bindingContext,
propertyDescriptor,
propertyBinder);
}
It has been 6 years since this question was asked, I would rather take this space to summarize the update, instead of providing a brand new solution. At the time of writing, MVC 5 has been around for quite a while, and ASP.NET Core has just come out.
I followed the approach examined in the post written by Vijaya Anand (btw, thanks to Vijaya): http://www.prideparrot.com/blog/archive/2012/6/customizing_property_binding_through_attributes. And one thing worth noting is that, the data binding logic is placed in the custom attribute class, which is the BindProperty method of the StringArrayPropertyBindAttribute class in Vijaya Anand's example.
However, in all the other articles on this topic that I have read (including @jonathanconway's solution), custom attribute class is only a step stone that leads the framework to find out the correct custom model binder to apply; and the binding logic is placed in that custom model binder, which is usually an IModelBinder object.
The 1st approach is simpler to me. There may be some shortcomings of the 1st approach, that I haven't known yet, though, coz I am pretty new to MVC framework at the moment.
In addition, I found that the ExtendedModelBinder class in Vijaya Anand's example is unnecessary in MVC 5. It seems that the DefaultModelBinder class which comes with MVC 5 is smart enough to cooperate with custom model binding attributes.