Encuadernación de modelo polimórfico

Esta pregunta se ha formulado anteriormente en versiones anteriores de MVC. También hay esta entrada de blog sobre una forma de evitar el problema. Me pregunto si MVC3 ha introducido algo que podría ayudar, o si hay otras opciones.

En una palabra. Aquí está la situación. Tengo un modelo base abstracto y 2 subclases concretas. Tengo una vista fuertemente EditorForModel() que representa los modelos con EditorForModel() . Luego tengo plantillas personalizadas para representar cada tipo concreto.

El problema viene en el tiempo del poste. Si hago que el método de acción posterior tome la clase base como parámetro, entonces MVC no puede crear una versión abstracta de la misma (que de todos modos no desearía, me gustaría que cree el tipo concreto actual). Si creo varios métodos de acción posterior que varían solo por la firma del parámetro, entonces MVC se queja de que es ambiguo.

Por lo que puedo decir, tengo algunas opciones sobre cómo resolver este problema. No me gusta ninguno de ellos por varias razones, pero las voy a enumerar aquí:

  1. Cree un archivador de modelo personalizado como sugiere Darin en la primera publicación a la que me he vinculado.
  2. Crea un atributo discriminador como sugiere la segunda publicación a la que me he vinculado.
  3. Publicar en diferentes métodos de acción basados ​​en el tipo
  4. ???

No me gusta 1, porque básicamente es la configuración que está oculta. Algún otro desarrollador que trabaje en el código puede no saberlo y perder mucho tiempo tratando de descubrir por qué las cosas se rompen cuando cambia las cosas.

No me gustan los 2, porque parece un poco hacky. Pero, me estoy inclinando por este enfoque.

No me gustan 3, porque eso significa violar DRY.

¿Cualquier otra sugerencia?

Editar:

Decidí seguir el método de Darin, pero hice un pequeño cambio. Agregué esto a mi modelo abstracto:

 [HiddenInput(DisplayValue = false)] public string ConcreteModelType { get { return this.GetType().ToString(); }} 

Entonces se genera automáticamente un oculto en mi DisplayForModel() . Lo único que debe recordar es que si no está utilizando DisplayForModel() , tendrá que agregarlo usted mismo.

Como obviamente opto por la opción 1 (:-)), permítanme tratar de elaborarla un poco más para que sea menos frágil y evite codificar instancias concretas en la carpeta del modelo. La idea es pasar el tipo concreto a un campo oculto y usar el reflection para crear una instancia del tipo concreto.

Supongamos que tiene los siguientes modelos de vista:

 public abstract class BaseViewModel { public int Id { get; set; } } public class FooViewModel : BaseViewModel { public string Foo { get; set; } } 

el siguiente controlador:

 public class HomeController : Controller { public ActionResult Index() { var model = new FooViewModel { Id = 1, Foo = "foo" }; return View(model); } [HttpPost] public ActionResult Index(BaseViewModel model) { return View(model); } } 

la vista de Index correspondiente:

 @model BaseViewModel @using (Html.BeginForm()) { @Html.Hidden("ModelType", Model.GetType()) @Html.EditorForModel()  } 

y la plantilla del editor ~/Views/Home/EditorTemplates/FooViewModel.cshtml :

 @model FooViewModel @Html.EditorFor(x => x.Id) @Html.EditorFor(x => x.Foo) 

Ahora podríamos tener la siguiente carpeta de modelo personalizado:

 public class BaseViewModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { var typeValue = bindingContext.ValueProvider.GetValue("ModelType"); var type = Type.GetType( (string)typeValue.ConvertTo(typeof(string)), true ); if (!typeof(BaseViewModel).IsAssignableFrom(type)) { throw new InvalidOperationException("Bad Type"); } var model = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type); return model; } } 

El tipo real se deduce del valor del campo oculto ModelType . No está codificado de forma rígida, lo que significa que podría agregar otros tipos secundarios más adelante sin tener que tocar nunca esta carpeta modelo.

Esta misma técnica podría aplicarse fácilmente a colecciones de modelos de vista base.

Acabo de pensar en una solución interesante para este problema. En lugar de usar el enlace del modelo Bsed modelo de esta manera:

 [HttpPost] public ActionResult Index(MyModel model) {...} 

En su lugar, puedo usar TryUpdateModel () para permitirme determinar a qué tipo de modelo vincularme en el código. Por ejemplo, hago algo como esto:

 [HttpPost] public ActionResult Index() {...} { MyModel model; if (ViewData.SomeData == Something) { model = new MyDerivedModel(); } else { model = new MyOtherDerivedModel(); } TryUpdateModel(model); if (Model.IsValid) {...} return View(model); } 

Esto realmente funciona mucho mejor de todos modos, porque si estoy haciendo cualquier procesamiento, entonces tendría que convertir el modelo a lo que sea que sea de todos modos, o usarlo para calcular el mapa correcto para llamar con AutoMapper.

Supongo que aquellos de nosotros que no hemos usado MVC desde el día 1 nos olvidamos de UpdateModel y TryUpdateModel , pero todavía tiene sus aplicaciones.

Me tomó un buen día encontrar una respuesta a un problema estrechamente relacionado, aunque no estoy seguro de que sea exactamente el mismo problema, lo estoy publicando aquí en caso de que otros estén buscando una solución para el mismo problema.

En mi caso, tengo un tipo base abstracto para varios tipos diferentes de modelos de vista. Entonces, en el modelo de vista principal, tengo una propiedad de tipo base abstracto:

 class View { public AbstractBaseItemView ItemView { get; set; } } 

Tengo varios subtipos de AbstractBaseItemView, muchos de los cuales definen sus propias propiedades exclusivas.

Mi problema es que el modelo-enlazador no mira el tipo de objeto adjunto a View.ItemView, sino que solo mira el tipo de propiedad declarado, que es AbstractBaseItemView, y decide vincular solo las propiedades definidas en el tipo abstracto. ignorando las propiedades específicas del tipo concreto de AbstractBaseItemView que está en uso.

La solución para esto no es bonita:

 using System.ComponentModel; using System.ComponentModel.DataAnnotations; // ... public class ModelBinder : DefaultModelBinder { // ... override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null) { var concreteType = bindingContext.Model.GetType(); if (Nullable.GetUnderlyingType(concreteType) == null) { return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType); } } return base.GetTypeDescriptor(controllerContext, bindingContext); } // ... } 

Aunque este cambio se siente raro y es muy “sistémico”, parece funcionar, y no representa, por lo que puedo imaginar, un considerable riesgo de seguridad, ya que no se relaciona con CreateModel () y por lo tanto no te permite publicar cualquier cosa y engañar a la carpeta de modelos para crear cualquier objeto.

También funciona solo cuando el tipo de propiedad declarado es un tipo abstracto , por ejemplo, una clase abstracta o una interfaz.

En una nota relacionada, se me ocurre que otras implementaciones que he visto aquí que anulan CreateModel () probablemente solo funcionarán cuando publiques objetos completamente nuevos, y sufrirán el mismo problema con el que me encontré, cuando la propiedad declarada -type es de un tipo abstracto. Por lo tanto, es muy probable que no pueda editar propiedades específicas de tipos concretos en objetos de modelos existentes , sino que solo cree propiedades nuevas.

En otras palabras, es probable que necesite integrar este trabajo en su carpeta para poder editar correctamente los objetos que se agregaron al modelo de vista antes de enlazar … Personalmente, creo que es un enfoque más seguro, ya que Controlo qué tipo de hormigón se agrega, por lo que el controlador / acción puede, indirectamente, especificar el tipo concreto que puede estar vinculado, simplemente rellenando la propiedad con una instancia vacía.

Espero que esto sea útil para otros …

Usando el método de Darin para discriminar los tipos de modelo a través de un campo oculto en su vista, le recomendaría que use un RouteHandler personalizado para distinguir los tipos de modelo y dirigir cada uno a una acción con un nombre único en su controlador. Por ejemplo, si tiene dos modelos concretos, Foo y Bar, para su acción Create en su controlador, realice una acción CreateFoo(Foo model) y una acción CreateBar(Bar model) . A continuación, cree un RouteHandler personalizado, de la siguiente manera:

 public class MyRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { var httpContext = requestContext.HttpContext; var modelType = httpContext.Request.Form["ModelType"]; var routeData = requestContext.RouteData; if (!String.IsNullOrEmpty(modelType)) { var action = routeData.Values["action"]; routeData.Values["action"] = action + modelType; } var handler = new MvcHandler(requestContext); return handler; } } 

Luego, en Global.asax.cs, cambie RegisterRoutes() siguiente manera:

 public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); AreaRegistration.RegisterAllAreas(); routes.Add("Default", new Route("{controller}/{action}/{id}", new RouteValueDictionary( new { controller = "Home", action = "Index", id = UrlParameter.Optional }), new MyRouteHandler())); } 

Luego, cuando aparece una solicitud Create, si se define un ModelType en el formulario devuelto, el RouteHandler agregará el ModelType al nombre de la acción, lo que permite definir una acción única para cada modelo concreto.