2011-08-28 19 views
56

Esta pregunta ha sido asked before en versiones anteriores de MVC. También hay this blog entry sobre una forma de evitar el problema. Me pregunto si MVC3 ha introducido algo que podría ayudar, o si hay otras opciones.Enlace de modelo polimórfico

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

El problema viene en el tiempo del mensaje. 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 diversas razones, pero voy a enumerarlos aquí:

  1. Crear una carpeta de modelo personalizado como Darin sugiere en el primer post he vinculado.
  2. Cree un atributo discriminador como sugiere la segunda publicación a la que me he vinculado.
  3. Publicar en diferentes métodos de acción según el tipo
  4. ???

No me gusta 1, porque es básicamente 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 gusta 2, porque parece un poco hacky. Pero, me estoy inclinando por este enfoque.

No me gusta 3, porque eso significa violar DRY.

¿Alguna otra sugerencia?

Editar:

decidí ir con el método de Darin, pero hice un pequeño cambio. He añadido esto a mi modelo abstracto:

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

A continuación, una oculta automáticamente se genera en mi DisplayForModel(). Lo único que debe recordar es que si no está usando DisplayForModel(), tendrá que agregarlo usted mismo.

Respuesta

59

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

Suponga 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 correspondiente Index vista:

@model BaseViewModel 
@using (Html.BeginForm()) 
{ 
    @Html.Hidden("ModelType", Model.GetType())  
    @Html.EditorForModel() 
    <input type="submit" value="OK" /> 
} 

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

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

ahora podríamos tener el siguiente modelo personalizado de aglutinante:

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 infiere a partir 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 ser easily be applied para colecciones de modelos de vista base.

+0

Hmm .. Eso definitivamente mejora la capacidad de mantenimiento. Y funcionaría donde sus modelos concretos no difieran por las firmas de la propiedad (una falla en el método Atributo). Sin embargo, no estoy seguro si me gusta contaminar mi punto de vista con este tipo de discriminadores. –

+0

Elegí tu solución, pero hice un pequeño cambio. Ver mi edición –

+0

Descubrí una solución diferente también, (ver mi respuesta) que resulta mucho más simple y un poco más segura ya que no implica exponer el tipo de datos al cliente, y espero que no haya sido enredado con un hacker. –

4

Utilizando 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 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, hacer un encargo RouteHandler, 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() de la 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())); 
} 

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

+0

Todas estas soluciones dependen de tener un campo oculto. Pero esto tiene dos problemas. 1) que el desarrollador tiene que recordar agregar el campo oculto, y 2) es posible que alguien pueda crear un parámetro de modelo con el mismo nombre y tropezar accidentalmente con un problema de enrutamiento extraño. Al hacer esto global, es agradable y conveniente, pero es fácil de romper y/o tropezar. El enfoque de atributos tiene la ventaja de afectar solo a ese método de acción. –

+0

Enfoque interesante – BlackTigerX

14

Acabo de pensar en una solución interesante para este problema. En lugar de utilizar el enlace de parámetros como este modelo bsed:

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

puedo utilizar en su lugar TryUpdateModel() que me permita determinar qué tipo de modelo que se unen en el código. Por ejemplo, yo 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); 
} 

En realidad, esto funciona mucho mejor de todos modos, porque si yo estoy haciendo ningún tipo de procesamiento, entonces tendría que emitir el modelo de lo que en realidad es de todos modos, o utilizar is para averiguar 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 aún tiene sus aplicaciones.

+1

Tal vez hice algo mal, pero si acabo de hacer 'TryUpdateModel (modelo)' solo actualizará las propiedades, etc. de 'MyModel', si lo quiero para actualizar completamente el modelo, tengo que hacer' TryUpdateModel ((MyDerivedModel) model) ', pero de cualquier forma, este es un gran truco. – Brook

7

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

En mi caso, tengo un tipo base abstracto para varios tipos de modelos de vista diferentes. Así, en la vista principal modelo, tengo una propiedad de un tipo base abstracta:

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

Tengo un número de sub-tipos de AbstractBaseItemView, muchos de los cuales definen sus propias propiedades exclusivas.

Mi problema es que el modelo de ligante no se fija en el tipo de objeto unido a View.ItemView, sino que sólo se fija en el tipo de bienes declarados, que es AbstractBaseItemView - y decide unirse solamente las propiedades definido en el tipo abstracto, ignorando las propiedades específicas del tipo concreto de AbstractBaseItemView que está en uso.

El trabajo en torno a esto no es bastante:

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 hacky y es muy "sistémica", parece que funciona - y no, por lo que se puede imaginar, plantear un riesgo de seguridad considerable, ya que no empatar en CreateModel() y así lo hace no le permiten 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 tipo abstracto, p. Ej. una clase abstracta o una interfaz.

En una nota relacionada, se me ocurre que otras implementaciones que he visto aquí que CreateModel anulación() probablemente lo hará solamente trabajo cuando usted está enviando completamente nuevos objetos - y van a sufrir el mismo problema me encontré en, cuando el tipo de propiedad declarado es de un tipo abstracto. Por lo tanto, lo más probable es que no pueda editar propiedades específicas de tipos de hormigón en objetos de modelo existentes, sino solo crear objetos nuevos.

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, siento que es un enfoque más seguro, ya que controlo qué tipo de concreto 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 los demás ...

+0

Esto funcionó muy bien para mí. Trabajé durante días para encontrar una solución. Pude crear los nuevos objetos tal como lo describe, pero las propiedades del tipo concreto nunca se poblaron con la carpeta. Ahora lo son. – davcar

+0

Me alegro de que esto haya funcionado para alguien, parece que la mayoría de las personas no comprende estos problemas lo suficiente como para preocuparse ;-) –

Cuestiones relacionadas