I have noticed the following behavior and I wonder if anyone can explain why it is happening and how it can be prevented.
In my partial classes I have added various read-only properties that are not present in the database. When the object is created, these properties are accessed before we get to the controller's Create method. However, properties declared with just get; set; are not accessed.
To elaborate, using very simple examples, if I have the following properties:
public bool getBoolProperty {
get {
return true;
}
}
public bool getSetBoolProperty {
get; set;
}
If I then put breakpoints on the properties, when the object is created the breakpoint is hit for the first property but not the second. However, if I have a method:
public bool getBoolProperty() {
return true;
}
then this is not accessed.
I have tried all sorts of variations and annotations
// empty set
public bool getBoolProperty {
get {
return true;
}
set {}
}
// not mapped attribute
[NotMapped]
public bool getBoolProperty {
get {
return true;
}
}
// getting private variable
private bool _boolProperty = true;
public bool getPrivateBoolProperty {
get {
return _boolProperty;
}
}
I've tried declaring the properties virtual, yet all variations, except the get; set; variety, are accessed when the object gets created. This behaviour also occurs for integer properties
public virtual int getIntProperty {
get {
return 1;
}
}
and date/time properties,
public virtual DateTime getDateProperty {
get {
return DateTime.Now;
}
}
but not for string properties
public string getStringProperty {
get {
return "Hello String";
}
}
or other entity properties
public Item getItem {
get {
return new Item();
}
}
The problem I have is that some of these properties can involve some logic that might need database access eg
public bool HasChildren {
get {
return this.Children !== null && this.Children.Count > 0;
}
}
which, at the time of creation, the entity won't have.
Obviously I can get around this by making everything a method, but I would be interested to know why ASP.NET MVC accesses these properties on object creation (Some sort of internal validation perhaps), and I would be interested to know if there are any methods of preventing this, possibly by way of an annotation I haven't come across.
My project is database first, C#, EF 4.1, MVC
Edit
I should also point out I'm using POCO entities and accessing the database through stored procedures/function imports.
One thought occurred to me is that these properties are being accessed as part of the change tracking system as discussed here. It seems that what is probably happening is a snapshot is being taken of the newly created item.
I tried turning off proxy creation
dbContext.ContextOptions.ProxyCreationEnabled = false;
but the properties are still getting accessed on creation.
Edit 20130322
Following #ladislavmrnka's suggestion of exploring the stack trace I got this:
at System.ComponentModel.ReflectPropertyDescriptor.GetValue(Object component)
at System.Web.Mvc.AssociatedMetadataProvider.<>c__DisplayClassb.<GetPropertyValueAccessor>b__a()
at System.Web.Mvc.ModelMetadata.get_Model()
at System.Web.Mvc.DataAnnotationsModelValidator.<Validate>d__1.MoveNext()
at System.Web.Mvc.ModelValidator.CompositeModelValidator.<Validate>d__5.MoveNext()
at System.Web.Mvc.DefaultModelBinder.OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Object model)
at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)
at System.Web.Mvc.Controller.ExecuteCore()
at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext)
at System.Web.Mvc.MvcHandler.<>c__DisplayClass6.<>c__DisplayClassb.<BeginProcessRequest>b__5()
at System.Web.Mvc.Async.AsyncResultWrapper.<>c__DisplayClass1.<MakeVoidDelegate>b__0()
at System.Web.Mvc.Async.AsyncResultWrapper.<>c__DisplayClass8`1.<BeginSynchronous>b__7(IAsyncResult _)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.End()
at System.Web.Mvc.MvcHandler.<>c__DisplayClasse.<EndProcessRequest>b__d()
at System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f)
at System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action)
at System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult)
at System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
In it you will notice the calls to a validator or two. As a test I changed my bool property to a nullable bool property:
public bool? getBoolProperty {
get {
return true;
}
}
This time the property was not accessed when the object was created. This is my desired behaviour, however, I don't want to have to change all my custom properties to be nullable, so my question now becomes...
Is there any way to tell the framework not to validate a property? Perhaps via an attribute. This answer almost answers the question, but since I am using database first, I don't seem to have a ValidateOnSaveEnabled property to turn off. Perhaps I need to revisit my models.
As with most of EF's problems, the answer lies with view models.
Because most of the properties that were being accessed were not relevant until the model had been created and persisted to the db, I created a view model especially for the Create view which only contained the absolute minimum number of properties needed to create the model. I modified the controller's Create method to accept my new view model and voila, on creating this new model there were no accesses to any of the irrelevant properties on the main model.
So the answer to my original question appears to be that entity framework performs validation on all non-nullable scalar properties when an object is created and one way to avoid this is as explained above.
Related
I m trying to create a custom model binder with below code:
public class TransactionModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
Object Model = bindingContext.Model;
//Do custom logic
return Model;
}
}
In global.asax, I m adding:
ModelBinders.Binders.Add(typeof(TransViewModel), new TransactionModelBinder());
Issue:
I m not sure how to get Model. I tried bindingContext.Model but it is null.
Please also guide if my line of code in Global.asax is fine.
See this article:
public class HomeCustomDataBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(HomePageModels))
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string title = request.Form.Get("Title");
string day = request.Form.Get("Day");
string month = request.Form.Get("Month");
string year = request.Form.Get("Year");
return new HomePageModels
{
Title = title,
Date = day + "/" + month + "/" + year
};
//// call the default model binder this new binding context
//return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
Well, if you intend to write the entire binder from scratch, you will not have a model. Instead you'd actually be the one creating the model (since that's what the binder is for), from the form data. Such as:
return new SomeModel
{
OneProp = request.Form["OneProp"],
AnotherProp = request.Form["AnotherProp"]
}
Alternatively you can inherit from DefaultModelBinder instead of IModelBinder, which you can use to extend only certain behavior instead of actually handling the construction of the model.
EDIT:
From your comments I'm understanding that you are only wanting to process one property in several viewmodels you might have (possibly multiple viewmodels have decimals that come from the view in a different format than what MVC expects for decimals.
In that case I'd actually use a different approach. Instead of registering the ModelBinder in global.asax, I'd remove it from there and do it declaratively on the actual properties that need that special format.
Such as:
[PropertyBinder(typeof(MyDecimalBinder))]
public decimal SomePropInAViewModel {get; set;}
This is based on a common approach of creating the PropertyBindingAttribute:
http://www.prideparrot.com/blog/archive/2012/6/customizing_property_binding_through_attributes or https://stackoverflow.com/a/12683210/1373170
And with a ModelBinder similar to this:
public class MyDecimalBinder : DefaultModelBinder {
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
// use the propertyDescriptor to make your modifications, by calling SetProperty()
...
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
Now, if this is something you want applied to ALL decimals, you might also want to check out Phil Haack's full implementation on handling decimals using a custom binder:
http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx/
I have created a custom model binder to bind int, to handle when the string value is greater than int.maxvalue - we have a lot of legacy code using UpdateModel, and this method throws an exception if the string value to be converted to an int is greater than int.MaxValue. If I modify the call to UpdateModel to be TryUpdateModel, the update fails gracefully and an error is displayed in the view, but I want a more meaningful error message - hence the custom model binder. The model binder works fine, however I have an Action similar to the following
public ActionResult List(ListCriteria criteria, int page)
The class ListCriteria has a property on it as follows
public int Page { get; set;}
My custom model binder implements IModelBinder. Similar to quite a lot of samples, it is implemented similar to the following.
public class IntModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext) {
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try {
actualValue = Convert.ToInt32(valueResult.AttemptedValue,
CultureInfo.CurrentCulture);
}
catch (FormatException e) {
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
When the action is called, the model binder throws an exception on this line:
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
and the exception message is "An item with the same key has already been added". Obviously this is because the Page property on the ListCriteria object has already been bound, and added to the dictionary - when the page parameter is bound and added, the exception is thrown.
How should this be implemented? Shouldn't it be possible to have properties with the same name at different levels of the object hierarchy?
In my case it turns out that the Page property and the page parameter perform the same function, so the latter cna be deleted - however it is concerning that it might rear its head elsewhere...
I'm trying to create a tagging system for my project. I need the pass a string (for ex: "test1, test2, test3") which will be binded to an entity as a list.
I'm using EF and my view inherits an entity, defined in EF. Without creating a view model, is it possible to do that?
Quite honestly view models is the way to go here.
But because you asked I will try to answer. IIRC EF models are partial classes, meaning that you could add properties to them, like this:
public partial class MyEFModel
{
public IEnumerable<string> List
{
get
{
return SomeStringProperty.Split(',');
}
set
{
SomeStringProperty = string.Join(",", value.ToArray());
}
}
}
Another way to achieve this is to write a custom model binder, like this:
public class MyBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null)
{
return value.AttemptedValue.Split(',');
}
return base.BindModel(controllerContext, bindingContext);
}
}
public class HomeController : Controller
{
public ActionResult Index(
[ModelBinder(typeof(MyBinder))] IEnumerable<string> values
)
{
return View();
}
}
and then /home/index?values=val1,val2,val3 should bind correctly to the list.
There are couple of ways to achieve this:
Custom Action Filter
Custom Model Binder
These implementations can be found here:
Is it possible to change the querystring variable in ASP.NET MVC path before it hits the controller?
Update (21st Sept 2016) - Thanks to Digbyswift for commenting that this solution still works in MVC5 also.
Update (30th April 2012) - Note to people stumbling across this question from searches etc - the accepted answer is not how I ended up doing this - but I left it accepted because it might have worked in some cases. My own answer contains the final solution I used, which is reusable and will apply to any project.
It's also confirmed to work in v3 and v4 of the MVC framework.
I have the following model type (the names of the class and its properties have been changed to protect their identities):
public class MyExampleModel
{
public string[] LongPropertyName { get; set; }
}
This property is then bound to a bunch (>150) of check boxes, where each one's input name is of course LongPropertyName.
The form submits to url with an HTTP GET, and say the user selects three of those checkboxes - the url will have the query string ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c
Big problem then is that if I select all (or even just over half!) the checkboxes, I exceed the maximum query string length enforced by the request filter on IIS!
I do not want to extend that - so I want a way to trim down this query string (I know I can just switch to a POST - but even so I still want to minimize the amount of fluff in the data sent by the client).
What I want to do is have the LongPropertyName bound to simply 'L' so the query string would become ?L=a&L=b&L=c but without changing the property name in code.
The type in question already has a custom model binder (deriving from DefaultModelBinder), but it's attached to its base class - so I don't want to put code in there for a derived class. All the property binding is currently performed by the standard DefaultModelBinder logic, which I know uses TypeDescriptors and Property Descriptors etc from System.ComponentModel.
I was kinda hoping that there might be an attribute I could apply to the property to make this work - is there? Or should I be looking at implementing ICustomTypeDescriptor?
In response to michaelalm's answer and request - here's what I've ended up doing. I've left the original answer ticked mainly out of courtesy since one of the solutions suggested by Nathan would have worked.
The output of this is a replacement for DefaultModelBinder class which you can either register globally (thereby allowing all model types to take advantage of aliasing) or selectively inherit for custom model binders.
It all starts, predictably with:
/// <summary>
/// Allows you to create aliases that can be used for model properties at
/// model binding time (i.e. when data comes in from a request).
///
/// The type needs to be using the DefaultModelBinderEx model binder in
/// order for this to work.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class BindAliasAttribute : Attribute
{
public BindAliasAttribute(string alias)
{
//ommitted: parameter checking
Alias = alias;
}
public string Alias { get; private set; }
}
And then we get this class:
internal sealed class AliasedPropertyDescriptor : PropertyDescriptor
{
public PropertyDescriptor Inner { get; private set; }
public AliasedPropertyDescriptor(string alias, PropertyDescriptor inner)
: base(alias, null)
{
Inner = inner;
}
public override bool CanResetValue(object component)
{
return Inner.CanResetValue(component);
}
public override Type ComponentType
{
get { return Inner.ComponentType; }
}
public override object GetValue(object component)
{
return Inner.GetValue(component);
}
public override bool IsReadOnly
{
get { return Inner.IsReadOnly; }
}
public override Type PropertyType
{
get { return Inner.PropertyType; }
}
public override void ResetValue(object component)
{
Inner.ResetValue(component);
}
public override void SetValue(object component, object value)
{
Inner.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return Inner.ShouldSerializeValue(component);
}
}
This proxies a 'proper' PropertyDescriptor that is normally found by the DefaultModelBinder but presents its name as the alias.
Next we have the new model binder class:
UPDATED WITH #jsabrooke's suggestion below
public class DefaultModelBinderEx : DefaultModelBinder
{
protected override System.ComponentModel.PropertyDescriptorCollection
GetModelProperties(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var toReturn = base.GetModelProperties(controllerContext, bindingContext);
List<PropertyDescriptor> additional = new List<PropertyDescriptor>();
//now look for any aliasable properties in here
foreach (var p in
this.GetTypeDescriptor(controllerContext, bindingContext)
.GetProperties().Cast<PropertyDescriptor>())
{
foreach (var attr in p.Attributes.OfType<BindAliasAttribute>())
{
additional.Add(new AliasedPropertyDescriptor(attr.Alias, p));
if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
&& !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase)))
{
bindingContext.PropertyMetadata.Add(
attr.Alias,
bindingContext.PropertyMetadata[p.Name]);
}
}
}
return new PropertyDescriptorCollection
(toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
}
}
And, then technically, that's all there is to it. You can now register this DefaultModelBinderEx class as the default using the solution posted as the answer in this SO: Change the default model binder in asp.net MVC, or you can use it as a base for your own model binder.
Once you've selected your pattern for how you want the binder to kick in, you simply apply it to a model type as follows:
public class TestModelType
{
[BindAlias("LPN")]
//and you can add multiple aliases
[BindAlias("L")]
//.. ad infinitum
public string LongPropertyName { get; set; }
}
The reason I chose this code was because I wanted something that would work with custom type descriptors as well as being able to work with any type. Equally, I wanted the value provider system to be used still in sourcing the model property values. So I've changed the meta data that the DefaultModelBinder sees when it starts binding. It's a slightly more long-winded approach - but conceptually it's doing at the meta data level exactly what you want it to do.
One potentially interesting, and slightly annoying, side effect will be if the ValueProvider contains values for more than one alias, or an alias and the property by it's name. In this case, only one of the retrieved values will be used. Difficult to think of a way of merging them all in a type-safe way when you're just working with objects though. This is similar, though, to supplying a value in both a form post and query string - and I'm not sure exactly what MVC does in that scenario - but I don't think it's recommended practise.
Another problem is, of course, that you must not create an alias that equals another alias, or indeed the name of an actual property.
I like to apply my model binders, in general, using the CustomModelBinderAttribute class. The only problem with this can be if you need to derive from the model type and change it's binding behaviour - since the CustomModelBinderAttribute is inherited in the attribute search performed by MVC.
In my case this is okay, I'm developing a new site framework and am able to push new extensibility into my base binders using other mechanisms to satisfy these new types; but that won't be the case for everybody.
You can use the BindAttribute to accomplish this.
public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {
}
Update
Since the 'longPropertyName' parameter is part of the model object, and not an independent parameter of the controller action, you have a couple of other choices.
You could keep the model and the property as independent parameters to your action and then manually merge the data together in the action method.
public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
if(myModel != null) {
myModel.LongPropertyName = longPropertyName;
}
}
Another option would be implementing a custom Model Binder that performs the parameter value assignment (as above) manually, but that is most likely overkill. Here's an example of one, if you're interested: Flags Enumeration Model Binder.
would this be a solution similar to yours Andras? i hope you could post your answer as well.
controller method
public class MyPropertyBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
for (int i = 0; i < propertyDescriptor.Attributes.Count; i++)
{
if (propertyDescriptor.Attributes[i].GetType() == typeof(BindingNameAttribute))
{
// set property value.
propertyDescriptor.SetValue(bindingContext.Model, controllerContext.HttpContext.Request.Form[(propertyDescriptor.Attributes[i] as BindingNameAttribute).Name]);
break;
}
}
}
}
Attribute
public class BindingNameAttribute : Attribute
{
public string Name { get; set; }
public BindingNameAttribute()
{
}
}
ViewModel
public class EmployeeViewModel
{
[BindingName(Name = "txtName")]
public string TestProperty
{
get;
set;
}
}
then to use the Binder in the controller
[HttpPost]
public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel)
{
// do stuff here
}
the txtName form value should be set to the TestProperty.
This should probably be a shorter comment on Andras Zoltan's answer but don't have enough reputation, sorry.
Thanks for the solution, I've just used it and it still works great! However, some of my properties have an alias with the same name, but different case e.g.
[BindAlias("signature")]
public string Signature { get; set; }
These throw an error when the custom model binder tries to add the aliases to the
PropertyMetadata dictionary, as their main property name versions have already been added by the base model binder, and the model binding is case-insensitive.
To solve this, just do a case insensitive check -
replace
if (bindingContext.PropertyMetadata.ContainsKey(p.Name))
with
if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
&& !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase))
So I've spent most of the day trying to figure out why I couldn't get this to work. Since I'm making my calls from a System.Web.Http.ApiController turns out that you can't use the DefaultPropertyBinder solution as mentioned above but instead must us an IModelBinder class.
the class that I've wound up writing to replace #AndreasZoltan's foundational work as written above is as follows:
using System.Reflection;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using QueryStringAlias.Attributes;
namespace QueryStringAlias.ModelBinders
{
public class AliasModelBinder : IModelBinder
{
private bool TryAdd(PropertyInfo pi, NameValueCollection nvc, string key, ref object model)
{
if (nvc[key] != null)
{
try
{
pi.SetValue(model, Convert.ChangeType(nvc[key], pi.PropertyType));
return true;
}
catch (Exception e)
{
Debug.WriteLine($"Skipped: {pi.Name}\nReason: {e.Message}");
}
}
return false;
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Type bt = bindingContext.ModelType;
object model = Activator.CreateInstance(bt);
string QueryBody = actionContext.Request.Content.ReadAsStringAsync().Result;
NameValueCollection nvc = HttpUtility.ParseQueryString(QueryBody);
foreach (PropertyInfo pi in bt.GetProperties())
{
if (TryAdd(pi, nvc, pi.Name, ref model))
{
continue;
};
foreach (BindAliasAttribute cad in pi.GetCustomAttributes<BindAliasAttribute>())
{
if (TryAdd(pi, nvc, cad.Alias, ref model))
{
break;
}
}
}
bindingContext.Model = model;
return true;
}
}
}
In order to ensure that this runs as part of a WebAPI call you must also add config.BindParameter(typeof(TestModelType), new AliasModelBinder()); in the Regiser portion of your WebApiConfig.
If you are using this method, you also must remove [FromBody] from your method signature.
[HttpPost]
[Route("mytestendpoint")]
[System.Web.Mvc.ValidateAntiForgeryToken]
public async Task<MyApiCallResult> Signup(TestModelType tmt) // note that [FromBody] does not appear in the signature
{
// code happens here
}
Note that this work builds on the answer above, using the QueryStringAlias samples.
At the moment this would likely fail in the case where TestModelType had complex nested types. Ideally there are a few other things:
handle complex nested types robustly
enable an attribute on the class to activate the IModelBuilder as opposed to in the registration
enable the same IModelBuilder to work in both Controllers and ApiControllers
But for now I'm satisfied with this for my own needs. Hopefully someone finds this piece useful.
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.