ASP.NET MVC 3: DefaultModelBinder con herencia / polymorphism

En primer lugar, perdón por la gran publicación (primero traté de investigar) y por la combinación de tecnologías en la misma pregunta (ASP.NET MVC 3, Ninject y MvcContrib).

Estoy desarrollando un proyecto con ASP.NET MVC 3 para manejar algunas órdenes de clientes.

En resumen: tengo algunos objetos heredados de y Order clase abstracta y necesito analizarlos cuando se realiza una solicitud POST a mi controlador. ¿Cómo puedo resolver el tipo correcto? ¿Debo anular la clase DefaultModelBinder o hay alguna otra forma de hacerlo? ¿Alguien puede proporcionarme algún código u otros enlaces sobre cómo hacer esto? ¡Cualquier ayuda sería genial! Si la publicación es confusa, puedo hacer cualquier cambio para aclararla.

Entonces, tengo el siguiente árbol de herencia para las órdenes que necesito manejar:

 public abstract partial class Order { public Int32 OrderTypeId {get; set; } /* rest of the implementation ommited */ } public class OrderBottling : Order { /* implementation ommited */ } public class OrderFinishing : Order { /* implementation ommited */ } 

Todas estas clases son generadas por Entity Framework, así que no las modificaré porque tendré que actualizar el modelo (sé que puedo extenderlas). Además, habrá más pedidos, pero todos derivados de Order .

Tengo una vista genérica ( Create.aspx ) para crear un pedido y esta vista llama a una vista parcial fuertemente OrderBottling para cada uno de los pedidos heredados (en este caso, OrderBottling y OrderFinishing ). OrderController un método Create() para una solicitud GET y otra para una solicitud POST en la clase OrderController . El segundo es como el siguiente:

 public class OrderController : Controller { /* rest of the implementation ommited */ [HttpPost] public ActionResult Create(Order order) { /* implementation ommited */ } } 

Ahora el problema: cuando recibo la solicitud POST con los datos del formulario, la carpeta predeterminada de MVC intenta crear una instancia de un objeto Order , lo cual está bien ya que el tipo de método es ese. Pero debido a que el Order es abstracto, no puede ser instanciado, que es lo que se supone que debe hacer.

La pregunta: ¿cómo puedo descubrir qué tipo de Order concreto es enviado por la vista?

Ya he buscado aquí en Stack Overflow y busqué en Google mucho sobre esto (¡estoy trabajando en este problema durante aproximadamente 3 días!) Y encontré algunas formas de resolver algunos problemas similares, pero no pude encontrar nada como mi verdadero problema. Dos opciones para resolver esto:

  • anula ASP.NET MVC DefaultModelBinder y usa Direct Injection para descubrir qué tipo es el Order ;
  • cree un método para cada orden (no es bello y sería problemático mantenerlo).

No he probado la segunda opción porque no creo que sea la forma correcta de resolver el problema. Para la primera opción, probé Ninject para resolver el tipo de orden y crear una instancia. Mi módulo Ninject es como el siguiente:

 private class OrdersService : NinjectModule { public override void Load() { Bind().To(); Bind().To(); } } 

He intentado obtener uno de los tipos en el método Get() Ninject, pero me dice que hay más de una forma de resolver el tipo. Entonces, entiendo que el módulo no está bien implementado. También intenté implementar esto para ambos tipos: Bind().To().WithPropertyInject("OrderTypeId", 2); , pero tiene el mismo problema … ¿Cuál sería la forma correcta de implementar este módulo?

También intenté usar MvcContrib Model Binder. He hecho esto:

 [DerivedTypeBinderAware(typeof(OrderBottling))] [DerivedTypeBinderAware(typeof(OrderFinishing))] public abstract partial class Order { } 

y en Global.asax.cs he hecho esto:

 protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(Order), new DerivedTypeModelBinder()); } 

Pero esto arroja una excepción: System.MissingMethodException: no se puede crear una clase abstracta . Por lo tanto, supongo que la carpeta no es o no puede resolver el tipo correcto.

Muchas muchas gracias de antemano!

Editar: en primer lugar, ¡gracias Martin y Jason por tus respuestas y disculpa el retraso! ¡Probé ambos enfoques y ambos funcionaron! Marqué la respuesta de Martin como correcta porque es más flexible y cumple con algunas de las necesidades de mi proyecto. Específicamente, los ID de cada solicitud se almacenan en una base de datos y ponerlos en la clase puede romper el software si cambio la ID solo en un lugar (base de datos o en la clase). El enfoque de Martin es muy flexible en ese punto.

@Martin: en mi código cambié la línea

 var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); 

a

 var concreteType = Assembly.GetAssembly(typeof(Order)).GetType(concreteTypeValue.AttemptedValue); 

porque mis clases estaban en otro proyecto (y por lo tanto, en un ensamble diferente). Estoy compartiendo esto porque parece ser más flexible que obtener solo el ensamblaje de ejecución que no puede resolver tipos en ensamblajes externos. En mi caso, todas las clases de orden están en el mismo ensamblaje. No es mejor ni una fórmula mágica, pero creo que es interesante compartir esto;)

Intenté hacer algo similar antes y llegué a la conclusión de que no hay nada integrado que pueda manejar esto.

La opción que escogí fue crear mi propia carpeta de modelos (aunque heredada de la predeterminada, por lo que no hay demasiados códigos). Buscó un valor posterior a la publicación con el nombre del tipo llamado xxxConcreteType, donde xxx era otro tipo al que estaba vinculado. Esto significa que un campo debe publicarse con el valor del tipo que está tratando de vincular; en este caso OrderConcreteType con un valor de OrderBottling u OrderFinishing.

Su otra alternativa es usar UpdateModel o TryUpdateModel y omitir el parámetro de su método. Tendrá que determinar qué tipo de modelo está actualizando antes de llamar a esto (ya sea por un parámetro o de otro modo) y crear una instancia de la clase de antemano, luego puede usar cualquiera de los métodos para popuplatarlo

Editar:

Aquí está el código …

 public class AbstractBindAttribute : CustomModelBinderAttribute { public string ConcreteTypeParameter { get; set; } public override IModelBinder GetBinder() { return new AbstractModelBinder(ConcreteTypeParameter); } private class AbstractModelBinder : DefaultModelBinder { private readonly string concreteTypeParameterName; public AbstractModelBinder(string concreteTypeParameterName) { this.concreteTypeParameterName = concreteTypeParameterName; } protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { var concreteTypeValue = bindingContext.ValueProvider.GetValue(concreteTypeParameterName); if (concreteTypeValue == null) throw new Exception("Concrete type value not specified for abstract class binding"); var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); if (concreteType == null) throw new Exception("Cannot create abstract model"); if (!concreteType.IsSubclassOf(modelType)) throw new Exception("Incorrect model type specified"); var concreteInstance = Activator.CreateInstance(concreteType); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, concreteType); return concreteInstance; } } } 

Cambie su método de acción para que se vea así:

 public ActionResult Create([AbstractBind(ConcreteTypeParameter = "orderType")] Order order) { /* implementation ommited */ } 

Debería poner lo siguiente en su vista:

 @Html.Hidden("orderType, "Namespace.xxx.OrderBottling") 

Puede crear un módulo ModelBinder que funcione cuando su acción acepte un determinado tipo, y puede crear un objeto de cualquier tipo que desee devolver. El método CreateModel () toma un ControllerContext y ModelBindingContext que le dan acceso a los parámetros pasados ​​por route, url querystring y post que puede usar para poblar su objeto con valores. La implementación del enlazador de modelo predeterminado convierte los valores de las propiedades del mismo nombre para ponerlos en los campos del objeto.

Lo que hago aquí es simplemente verificar uno de los valores para determinar qué tipo crear, luego llamar al método DefaultModelBinder.CreateModel () cambiando el tipo que se va a crear al tipo apropiado.

 public class OrderModelBinder : DefaultModelBinder { protected override object CreateModel( ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { // get the parameter OrderTypeId ValueProviderResult result; result = bindingContext.ValueProvider.GetValue("OrderTypeId"); if (result == null) return null; // OrderTypeId must be specified // I'm assuming 1 for Bottling, 2 for Finishing if (result.AttemptedValue.Equals("1")) return base.CreateModel(controllerContext, bindingContext, typeof(OrderBottling)); else if (result.AttemptedValue.Equals("2")) return base.CreateModel(controllerContext, bindingContext, typeof(OrderFinishing)); return null; // unknown OrderTypeId } } 

Configúrelo para usarlo cuando tenga un parámetro Order en sus acciones agregando esto a Application_Start () en Global.asax.cs:

 ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder()); 

También puede construir un ModelBinder genérico que funcione para todos sus modelos abstractos. Mi solución requiere que agregue un campo oculto a su vista llamado ‘ModelTypeName’ con el valor establecido para el nombre del tipo concreto que desea. Sin embargo, debería ser posible hacer esto más inteligente y elegir un tipo concreto al hacer coincidir las propiedades de tipo con los campos de la vista.

En su Global.asax.cs Application_Start ():

 ModelBinders.Binders.DefaultBinder = new CustomModelBinder(); 

CustomModelBinder:

 public class CustomModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.IsAbstract) { var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ModelTypeName"); if (modelTypeValue == null) throw new Exception("View does not contain ModelTypeName"); var modelTypeName = modelTypeValue.AttemptedValue; var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName); if(type == null) throw new Exception("Invalid ModelTypeName"); var concreteInstance = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, type); return concreteInstance; } return base.CreateModel(controllerContext, bindingContext, modelType); } } 

Mi solución para ese problema admite modelos complejos que pueden contener otras clases abstractas, herencia múltiple, colecciones o clases genéricas.

 public class EnhancedModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { Type type = modelType; if (modelType.IsGenericType) { Type genericTypeDefinition = modelType.GetGenericTypeDefinition(); if (genericTypeDefinition == typeof(IDictionary<,>)) { type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments()); } else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) || (genericTypeDefinition == typeof(IList<>))) { type = typeof(List<>).MakeGenericType(modelType.GetGenericArguments()); } return Activator.CreateInstance(type); } else if(modelType.IsAbstract) { string concreteTypeName = bindingContext.ModelName + ".Type"; var concreteTypeResult = bindingContext.ValueProvider.GetValue(concreteTypeName); if (concreteTypeResult == null) throw new Exception("Concrete type for abstract class not specified"); type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.IsSubclassOf(modelType) && t.Name == concreteTypeResult.AttemptedValue); if (type == null) throw new Exception(String.Format("Concrete model type {0} not found", concreteTypeResult.AttemptedValue)); var instance = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type); return instance; } else { return Activator.CreateInstance(modelType); } } } 

Como ve, debe agregar un campo (de nombre Tipo ) que contenga información sobre qué clase concreta heredará de la clase abstracta. Por ejemplo, clases: contenido de clase abstracta , clase de contenido de texto , el contenido debe tener Tipo establecido en “Contenido de texto”. Recuerde cambiar la carpeta de modelo predeterminada en global.asax:

 protected void Application_Start() { ModelBinders.Binders.DefaultBinder = new EnhancedModelBinder(); [...] 

Para obtener más información y ver el proyecto de muestra siguiente enlace .

Cambiar la línea:

 var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); 

A esto:

  Type concreteType = null; var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in loadedAssemblies) { concreteType = assembly.GetType(concreteTypeValue.AttemptedValue); if (null != concreteType) { break; } } 

Esta es una implementación ingenua que verifica cada ensamblaje para el tipo. Estoy seguro de que hay formas más inteligentes de hacerlo, pero esto funciona bastante bien.