Asp.Net MVC 2 - bind a model property to another name

Update (September 21, 2016) . Thanks to Digbyswift for comments that this solution still works in MVC5.

Update (April 30, 2012) . Note to people who stumble about this issue from searches, etc. - the accepted answer is not how I did it, but I left it accepted because it may have worked in some cases. My own answer contains the final solution that I used , which can be reused and will be applied to any project.

He also confirmed the work in v3 and v4 of the MVC framework.

I have the following model type (class names and its properties have been changed to protect their identifiers):

public class MyExampleModel { public string[] LongPropertyName { get; set; } } 

Then this property is bound to a set (> 150) of flags, where each one input name is, of course, LongPropertyName .

The form submits an HTTP GET URL and says that the user selects three of these flags - will the URL contain a query string ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c

The big problem is that if I select all (or even just over half!) Of the flags, I will exceed the maximum length of the query string set by the query filter in IIS!

I don’t want to distribute this - so I want to trim this query string (I know I can just switch to POST), but even then I still want to minimize the amount of fluff in the data sent to the client).

What I want to do is bind LongPropertyName to a simple "L" so that the query string becomes ?L=a&L=b&L=c , but without changing the name of the property in the code .

The type in question already has a custom mediator (coming from DefaultModelBinder), but it is bound to its base class, so I don’t want to put the code for the derived class there. Currently, all property bindings are done by the standard DefaultModelBinder logic, which, as I know, uses TypeDescriptors and property descriptors, etc. From System.ComponentModel.

I was hoping there might be an attribute that I can apply to the property to make this work - is there? Or should I look at the implementation of ICustomTypeDescriptor ?

+49
c # asp.net-mvc asp.net-mvc-2
Nov 30 '10 at 16:55
source share
4 answers

You can use 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 controller action parameter, you have several other options.

You can save the model and property as independent parameters for your action, and then manually combine 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 to implement a custom binder that manually assigns a parameter value (as described above), but this is most likely redundant. Here is an example of one of them, if you are interested: Checkbox Enumerated binding model .

+20
Nov 30 '10 at 16:58
source share

In response to michaelalm, the answer and request is what I ended up doing. I left the original answer, noted mainly out of politeness, as one of the solutions proposed by Nathan would work.

The result of this is the replacement of the DefaultModelBinder class, which you can either register globally (thereby allowing all types of models to take advantage of anti-aliasing) or selectively inherit for custom model bindings.

It all starts, predictably with:

 /// <summary> /// Allows you to create aliases that can be used for model properties at /// model binding time (ie 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 the "native" PropertyDescriptor, which is usually found in DefaultModelBinder , but presents its name as an alias.

Next we have a new middleware class:

 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)) bindingContext.PropertyMetadata.Add(attr.Alias, bindingContext.PropertyMetadata[p.Name]); } } return new PropertyDescriptorCollection (toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray()); } } 
And, then technically, that everything is there. Now you can register this default DefaultModelBinderEx class using the solution sent as an answer in this SO: Change the standard model binding in asp.net MVC , or you can use it as a base for your own connecting device.

Once you have selected your template for how you want the insert to be clicked, you simply apply it to the 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 the ability to work with any type. Equally, I wanted the value provider system to be used when searching for model property values. So I changed the metadata that DefaultModelBinder sees when it starts to bind. This is a slightly longer approach, but conceptually it does exactly what you want at the metadata level.

One potentially interesting and slightly annoying side effect would be if the ValueProvider contains values ​​for more than one alias or alias and a property by its name. In this case, only one of the obtained values ​​will be used. It's hard to think of a way to merge them all with a safe type when you are only working with object . It seems, however, to supply a value in both the form column and the query line - and I'm not sure what exactly MVC does in this scenario, but I don't think he recommended the practice.

Another problem, of course, is that you should not create an alias equal to another alias, or really the name of the actual property.

I like to apply my model bindings, in general, using the CustomModelBinderAttribute class. The only problem with this might be if you need to get the type of the model and change its binding behavior - since CustomModelBinderAttribute inherited in the attribute lookup performed by MVC.

In my case, this is normal; I am developing a new site infrastructure and can push new extensibility into my base binders, using other mechanisms to satisfy these new types; but this does not apply to everyone.

+84
Nov 30 '10 at 16:58
source share

Will this solution look like your Andras? I hope you can 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 use binder in the controller

 [HttpPost] public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel) { // do stuff here } 

the value of the txtName form must be set to TestProperty.

+4
Jan 18 '11 at 3:27
source share

So I spent most of the day trying to figure out why I couldn't get this to work. Since I make my calls from System.Web.Http.ApiController , it turns out that you cannot use the DefaultPropertyBinder solution, as mentioned above, but should use the IModelBinder class IModelBinder .

the class that I wrote to replace the fundamental work of @AndreasZoltan, as described 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; } } } 

To ensure that this runs as part of the WebAPI call, you must also add config.BindParameter(typeof(TestModelType), new AliasModelBinder()); into the Regiser part of your WebApiConfig .

If you use this method, you must also remove [FromBody] from the signature of your method.

  [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 is based on the answer above using QueryStringAlias examples.

At the moment, this will most likely fail if TestModelType has complex nested types. Ideally, there are a few more things:

  • handle complex nested types reliably
  • enable an attribute in the class to activate IModelBuilder, as opposed to registering
  • enable the same IModelBuilder to work with both controllers and ApiControllers

But for now, I am satisfied with this for my needs. Hope someone finds this piece useful.

+1
Aug 28 '19 at 20:38 on
source share



All Articles