Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.Net MVC 2 - Bind a model's property to a different named value

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?

like image 909
Andras Zoltan Avatar asked Nov 30 '10 16:11

Andras Zoltan


People also ask

What is bind property in MVC?

ASP.NET MVC framework also enables you to specify which properties of a model class you want to bind. The [Bind] attribute will let you specify the exact properties of a model should include or exclude in binding.

What is two way binding MVC?

So the two-way data binding means we can perform both read and write operations. In our previous article Data Binding in Spring MVC with Example, we have discussed how to write-to-variable task and in this article, we mainly focus on the read-from-a-variable task.


2 Answers

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.

like image 189
Andras Zoltan Avatar answered Oct 07 '22 23:10

Andras Zoltan


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.

like image 26
Nathan Taylor Avatar answered Oct 08 '22 01:10

Nathan Taylor