c# inheritance asp.net-mvc-3 mvccontrib ninject-2

c# - ASP.NET MVC 3: DefaultModelBinder con herencia/polimorfismo



inheritance asp.net-mvc-3 (5)

Primero, perdón por el gran post (he tratado de investigar primero) 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 algunos pedidos de clientes.

En resumen: tengo algunos objetos heredados y abstractos de Order clase y necesito analizarlos cuando se realiza una solicitud POST a mi controlador. ¿Cómo puedo resolver el tipo correcto? ¿Debo reemplazar la clase DefaultModelBinder o hay alguna otra forma de hacer esto? ¿Puede alguien 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 dejarlo claro.

Por lo tanto, 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 */ }

Entity Framework genera todas estas clases, por lo que no las modificaré porque necesitaré actualizar el modelo (sé que puedo extenderlas). Además, habrá más órdenes, pero todas derivadas de la Order .

Tengo una vista genérica ( Create.aspx ) para crear una orden y esta vista llama a una vista parcial muy tipificada para cada una de las órdenes heredadas (en este caso, OrderBottling y OrderFinishing ). OrderController un método Create() para una solicitud GET y otro 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, el archivador predeterminado 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 envía la vista?

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

  • DefaultModelBinder ASP.NET MVC DefaultModelBinder y use la inyección directa para descubrir qué tipo es la Order ;
  • crear un método para cada orden (no hermoso y sería problemático mantener).

No he probado la segunda opción porque no creo que sea la forma correcta de resolver el problema. Para la primera opción, he intentado 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<Order>().To<OrderBottling>(); Bind<Order>().To<OrderFinishing>(); } }

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

También he intentado 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 produce 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 al tipo correcto.

Muchas gracias de antemano!

Edición: en primer lugar, ¡gracias Martin y Jason por sus respuestas y disculpe la demora! Probé ambos enfoques y ambos trabajaron! Marqué la respuesta de Martin como correcta porque es más flexible y satisface algunas de las necesidades de mi proyecto. Específicamente, los ID para cada solicitud se almacenan en una base de datos y al colocarlos en la clase se puede romper el software si cambio el 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 así, en un ensamblaje diferente). Estoy compartiendo esto porque parece ser más flexible que obtener solo el ensamblado en ejecución que no puede resolver tipos en ensamblados 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;)


Cambia 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 comprueba cada ensamblaje para el tipo. Estoy seguro de que hay formas más inteligentes de hacerlo, pero esto funciona lo suficientemente bien.


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

La opción con la que fui fue crear mi propio archivador de modelos (aunque se heredó del valor predeterminado, por lo que no es demasiado código). Buscó un valor de devolución con el nombre del tipo llamado xxxConcreteType donde xxx era otro tipo al que estaba enlazando. Esto significa que un campo debe devolverse con el valor del tipo que está intentando enlazar; en este caso, OrderConcreteType con un valor de OrderBottling o OrderFinishing.

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

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 verse así:

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

Necesitarías poner lo siguiente en tu vista:

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


Mi solución para ese problema es compatible con modelos complejos que pueden contener otra clase abstracta, 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, tiene que agregar un campo (de nombre Tipo ) que contenga información sobre qué clase concreta se debe crear heredando de la clase abstracta. Por ejemplo, las clases: clase abstracta Contenido , clase TextContent , el Contenido debe tener Tipo establecido en "TextContent". Recuerde cambiar el archivador de modelo predeterminado en global.asax:

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

Para más información y muestra del proyecto, ver el siguiente enlace


Puede crear un ModelBinder de custodia que funcione cuando su acción acepte un determinado tipo, y puede crear un objeto del tipo que desee devolver. El método CreateModel () toma un ControllerContext y un ModelBindingContext que le dan acceso a los parámetros pasados ​​por la ruta, la cadena de consulta url y la publicación que puede usar para completar su objeto con valores. La implementación predeterminada de la carpeta de modelos convierte los valores de las propiedades del mismo nombre para colocarlos 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 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 de Orden en sus acciones agregando esto a Application_Start () en Global.asax.cs:

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


También puede crear 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 en 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); } }