Asp.Net MVC 2: enlaza la propiedad de un modelo a un valor con nombre diferente

Actualización (21 de septiembre de 2016) – Gracias a Digbyswift por comentar que esta solución aún funciona en MVC5 también.

Actualización (30 de abril de 2012) – Nota para las personas que tropiezan con esta pregunta a partir de búsquedas, etc. – la respuesta aceptada no es cómo terminé haciendo esto – pero la dejé aceptada porque podría haber funcionado en algunos casos. Mi propia respuesta contiene la solución final que utilicé , que es reutilizable y se aplicará a cualquier proyecto.

También se confirma que funciona en v3 y v4 del framework MVC.

Tengo el siguiente tipo de modelo (los nombres de la clase y sus propiedades se han cambiado para proteger sus identidades):

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

Esta propiedad se enlaza a un grupo (> 150) de casillas de verificación, donde el nombre de cada entrada es, por supuesto, LongPropertyName .

El formulario se envía a la URL con un HTTP GET, y dice que el usuario selecciona tres de esas casillas de verificación: la url tendrá la cadena de consulta ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c

El gran problema es que si selecciono todas (¡o incluso un poco más de la mitad!) Las casillas de verificación, excedo la longitud máxima de cadena de consulta impuesta por el filtro de solicitud en IIS.

No quiero extender eso, así que quiero una forma de recortar esta cadena de consulta (sé que puedo cambiar a una POST), pero aún así quiero minimizar la cantidad de pelusa en los datos enviados por el cliente) .

Lo que quiero hacer es hacer que LongPropertyName limite a ‘L’ para que la cadena de consulta se convierta en ?L=a&L=b&L=c pero sin cambiar el nombre de la propiedad en el código .

El tipo en cuestión ya tiene una carpeta de modelo personalizada (derivada de DefaultModelBinder), pero está asociada a su clase base, por lo que no quiero poner código allí para una clase derivada. Todo el enlace de propiedad se realiza actualmente mediante la lógica estándar de DefaultModelBinder, que sé que usa TypeDescriptors y Property Descriptors, etc., de System.ComponentModel.

Tenía la esperanza de que podría haber un atributo que pudiera aplicar a la propiedad para que esto funcione, ¿verdad? ¿O debería estar buscando implementar ICustomTypeDescriptor ?

Puede usar BindAttribute para lograr esto.

 public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) { } 

Actualizar

Dado que el parámetro ‘longPropertyName’ es parte del objeto modelo, y no un parámetro independiente de la acción del controlador, tiene un par de otras opciones.

Puede mantener el modelo y la propiedad como parámetros independientes de su acción y luego combinar manualmente los datos en el método de acción.

 public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) { if(myModel != null) { myModel.LongPropertyName = longPropertyName; } } 

Otra opción sería implementar un Encuadernador de modelo personalizado que realice la asignación de valor de parámetro (como se indicó anteriormente) de forma manual, pero es muy probable que sea excesivo. Aquí hay un ejemplo de uno, si está interesado: Flags Enumeration Model Binder .

En respuesta a la respuesta y solicitud de michaelalm, esto es lo que terminé haciendo. Dejé la respuesta original marcada principalmente por cortesía ya que una de las soluciones sugeridas por Nathan habría funcionado.

El resultado de esto es un reemplazo para la clase DefaultModelBinder que puede registrarse globalmente (permitiendo así que todos los tipos de modelo aprovechen el aliasing) o heredar de forma selectiva para los enlazadores de modelo personalizados.

Todo comienza, previsiblemente con:

 ///  /// 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. ///  [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; } } 

Y luego obtenemos esta clase:

 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); } } 

Esto representa un PropertyDescriptor ‘apropiado’ que normalmente se encuentra en DefaultModelBinder pero presenta su nombre como el alias.

A continuación tenemos la nueva clase de carpetas de modelo:

 public class DefaultModelBinderEx : DefaultModelBinder { protected override System.ComponentModel.PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) { var toReturn = base.GetModelProperties(controllerContext, bindingContext); List additional = new List(); //now look for any aliasable properties in here foreach (var p in this.GetTypeDescriptor(controllerContext, bindingContext) .GetProperties().Cast()) { foreach (var attr in p.Attributes.OfType()) { 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().Concat(additional).ToArray()); } } 

Y, técnicamente, eso es todo. Ahora puede registrar esta clase DefaultModelBinderEx como la predeterminada utilizando la solución publicada como la respuesta en este SO: cambie la carpeta de modelo predeterminada en asp.net MVC , o puede usarla como base para su propia carpeta de modelo.

Una vez que haya seleccionado su patrón de cómo desea que se active el encuadernador, simplemente lo aplica a un tipo de modelo de la siguiente manera:

 public class TestModelType { [BindAlias("LPN")] //and you can add multiple aliases [BindAlias("L")] //.. ad infinitum public string LongPropertyName { get; set; } } 

La razón por la que elegí este código fue porque quería algo que funcionara con los descriptores de tipo personalizados, así como poder trabajar con cualquier tipo. Igualmente, quería que el sistema del proveedor de valor se usara aún para obtener los valores de propiedad del modelo. Así que cambié los metadatos que ve el DefaultModelBinder cuando comienza a enlazarse. Es un enfoque un poco más prolijo, pero conceptualmente lo está haciendo en el nivel de metadatos exactamente lo que quieres que haga.

Un efecto secundario potencialmente interesante y ligeramente molesto será si ValueProvider contiene valores para más de un alias, o un alias y la propiedad por su nombre. En este caso, solo se usará uno de los valores recuperados. Sin embargo, es difícil pensar en una forma de fusionarlos todos de una manera segura cuando solo estás trabajando con object . Sin embargo, esto es similar a proporcionar un valor tanto en una columna de formulario como en una cadena de consulta, y no estoy seguro de lo que hace exactamente MVC en ese escenario, pero no creo que sea una práctica recomendada.

Otro problema es, por supuesto, que no debe crear un alias que sea igual a otro alias, o incluso el nombre de una propiedad real.

Me gusta aplicar mis carpetas modelo, en general, usando la clase CustomModelBinderAttribute . El único problema con esto puede ser si necesita derivar del tipo de modelo y cambiar su comportamiento vinculante, ya que CustomModelBinderAttribute se hereda en la búsqueda de atributos realizada por MVC.

En mi caso, esto está bien, estoy desarrollando un nuevo marco de trabajo y puedo impulsar la nueva extensibilidad en mis carpetas base usando otros mecanismos para satisfacer estos nuevos tipos; pero ese no será el caso para todos.

¿Sería esta una solución similar a la tuya Andras? Espero que puedas publicar tu respuesta también.

método del controlador

 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; } } } } 

Atributo

 public class BindingNameAttribute : Attribute { public string Name { get; set; } public BindingNameAttribute() { } } 

ViewModel

 public class EmployeeViewModel { [BindingName(Name = "txtName")] public string TestProperty { get; set; } } 

luego usar el Binder en el controlador

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

el valor del formulario txtName debe establecerse en TestProperty.