2012-02-23 19 views
44

Supongo que tengo un modelo de Producto, el modelo de Producto tiene una propiedad de ProductSubType (resumen) y tenemos dos implementaciones concretas Camisa y Pantalones.Modelo MVC 3 Cómo vincular un subtipo (clase o interfaz abstracta)

Aquí está la fuente:

public class Product 
{ 
    public int Id { get; set; } 

    [Required] 
    public string Name { get; set; } 

    [Required] 
    public decimal? Price { get; set; } 

    [Required] 
    public int? ProductType { get; set; } 

    public ProductTypeBase SubProduct { get; set; } 
} 

public abstract class ProductTypeBase { } 

public class Shirt : ProductTypeBase 
{ 
    [Required] 
    public string Color { get; set; } 
    public bool HasSleeves { get; set; } 
} 

public class Pants : ProductTypeBase 
{ 
    [Required] 
    public string Color { get; set; } 
    [Required] 
    public string Size { get; set; } 
} 

En mi interfaz de usuario, el usuario tiene una lista desplegable, se puede seleccionar el tipo de producto y los elementos de entrada se muestran según el tipo de producto adecuado. Tengo todo esto resuelto (usando un cambio de menú desplegable ajax get, devuelvo una plantilla parcial/editor y vuelvo a configurar la validación de jquery en consecuencia).

A continuación, creé un archivador de modelo personalizado para ProductTypeBase.

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
{ 

     ProductTypeBase subType = null; 

     var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int)); 

     if (productType == 1) 
     { 
      var shirt = new Shirt(); 

      shirt.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string)); 
      shirt.HasSleeves = (bool)bindingContext.ValueProvider.GetValue("SubProduct.HasSleeves").ConvertTo(typeof(bool)); 

      subType = shirt; 
     } 
     else if (productType == 2) 
     { 
      var pants = new Pants(); 

      pants.Size = (string)bindingContext.ValueProvider.GetValue("SubProduct.Size").ConvertTo(typeof(string)); 
      pants.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string)); 

      subType = pants; 
     } 

     return subType; 

    } 
} 

Esto une los valores correctamente y funciona en su mayor parte, excepto que pierdo la validación del lado del servidor. Así que en el presentimiento de que estoy haciendo esto de forma incorrecta He hecho un poco más la búsqueda y se encontró con esta respuesta de Darin Dimitrov:

ASP.NET MVC 2 - Binding To Abstract Model

así que cambié el modelo de aglutinante sólo anular CreateModel, pero ahora no lo hace enlazar los valores.

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) 
    { 
     ProductTypeBase subType = null; 

     var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int)); 

     if (productType == 1) 
     { 
      subType = new Shirt(); 
     } 
     else if (productType == 2) 
     { 
      subType = new Pants(); 
     } 

     return subType; 
    } 

Stepping aunque el src MVC 3, parece que en BindProperties, los GetFilteredModelProperties devuelve un resultado vacío, y creo que es porque el modelo BindingContext se establece en ProductTypeBase que no tiene ninguna propiedad.

¿Alguien puede detectar lo que estoy haciendo mal? Esto no parece ser así de difícil. Estoy seguro de que me falta algo simple ... Tengo otra alternativa en mente en lugar de tener una propiedad SubProduct en el modelo de Producto para tener propiedades separadas para Camisa y Pantalones. Estos son solo modelos de Vista/Forma, así que creo que funcionaría, pero me gustaría que el enfoque actual funcione para entender lo que está pasando ...

¡Gracias por cualquier ayuda!

Actualización:

que no quede claro, pero el modelo personalizado ligante añadí, hereda de la DefaultModelBinder

respuesta

ajuste de ModelMetadata y modelo fue la pieza que falta. Gracias Manas!

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) 
     { 
      if (modelType.Equals(typeof(ProductTypeBase))) { 
       Type instantiationType = null; 

       var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int)); 

       if (productType == 1) { 
        instantiationType = typeof(Shirt); 
       } 
       else if (productType == 2) { 
        instantiationType = typeof(Pants); 
       } 

       var obj = Activator.CreateInstance(instantiationType); 
       bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType); 
       bindingContext.ModelMetadata.Model = obj; 
       return obj; 
      } 

      return base.CreateModel(controllerContext, bindingContext, modelType); 

     } 

Respuesta

53

Esto se puede lograr anulando CreateModel (...). Lo demostraré con un ejemplo.

1. Permite crear un modelo y algunas clases base y secundarias.

public class MyModel 
{ 
    public MyBaseClass BaseClass { get; set; } 
} 

public abstract class MyBaseClass 
{ 
    public virtual string MyName 
    { 
     get 
     { 
      return "MyBaseClass"; 
     } 
    } 
} 

public class MyDerievedClass : MyBaseClass 
{ 

    public int MyProperty { get; set; } 
    public override string MyName 
    { 
     get 
     { 
      return "MyDerievedClass"; 
     } 
    } 
} 

2. Ahora cree un modelbinder y anulan CreateModel

public class MyModelBinder : DefaultModelBinder 
{ 
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) 
    { 
     /// MyBaseClass and MyDerievedClass are hardcoded. 
     /// We can use reflection to read the assembly and get concrete types of any base type 
     if (modelType.Equals(typeof(MyBaseClass))) 
     { 
      Type instantiationType = typeof(MyDerievedClass);     
      var obj=Activator.CreateInstance(instantiationType); 
      bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType); 
      bindingContext.ModelMetadata.Model = obj; 
      return obj; 
     } 
     return base.CreateModel(controllerContext, bindingContext, modelType); 
    } 

} 

3. Ahora en el controlador de crear conseguir y acción posterior.

[HttpGet] 
public ActionResult Index() 
    { 
     ViewBag.Message = "Welcome to ASP.NET MVC!"; 

     MyModel model = new MyModel(); 
     model.BaseClass = new MyDerievedClass(); 

     return View(model); 
    } 

    [HttpPost] 
    public ActionResult Index(MyModel model) 
    { 

     return View(model); 
    } 

4. Ahora Establecer como predeterminado MyModelBinder ModelBinder en global.asax Esto se hace para establecer un ligante modelo por defecto para todas las acciones, por una sola acción podemos utilizar el atributo ModelBinder en parámetros de acción)

protected void Application_Start() 
    { 
     AreaRegistration.RegisterAllAreas(); 

     ModelBinders.Binders.DefaultBinder = new MyModelBinder(); 

     RegisterGlobalFilters(GlobalFilters.Filters); 
     RegisterRoutes(RouteTable.Routes); 
    } 

5. Ahora podemos crear vista de tipo MyModel y una vista parcial del tipo MyDerievedClass

Index.cshtml

@model MvcApplication2.Models.MyModel 

@{ 
ViewBag.Title = "Index"; 
Layout = "~/Views/Shared/_Layout.cshtml"; 
} 

<h2>Index</h2> 

@using (Html.BeginForm()) { 
@Html.ValidationSummary(true) 
<fieldset> 
    <legend>MyModel</legend> 
    @Html.EditorFor(m=>m.BaseClass,"DerievedView") 
    <p> 
     <input type="submit" value="Create" /> 
    </p> 
</fieldset> 
} 

DerievedView.cshtml

@model MvcApplication2.Models.MyDerievedClass 

@Html.ValidationSummary(true) 
<fieldset> 
    <legend>MyDerievedClass</legend> 

    <div class="editor-label"> 
     @Html.LabelFor(model => model.MyProperty) 
    </div> 
    <div class="editor-field"> 
     @Html.EditorFor(model => model.MyProperty) 
     @Html.ValidationMessageFor(model => model.MyProperty) 
    </div> 

</fieldset> 

Ahora que va a funcionar como se esperaba, controlador recibirá un objeto de tipo "MyDerievedClass". Las validaciones se realizarán como se espera.

enter image description here

+2

Perfecto, configurar el ModelMetaData y el modelo en crear fue la pieza que falta, ¡gracias! –

+0

Tuve un problema muy similar con un tipo heredado y derivado, y el código de encuadernador anterior funcionó. ¡Aclamaciones! –

+0

Gracias! Pasé casi tres días intentando algo diferente para finalmente recurrir a esta solución, aunque mi problema era algo diferente. –

4

que tenían el mismo problema, que terminó usando MvcContrib como sugested here.

El documentation está desactualizado, pero si nos fijamos en las muestras, es bastante fácil.

Tendrás que dar de alta sus tipos en el Global.asax:

protected void Application_Start(object sender, EventArgs e) { 
    // (...) 
    DerivedTypeModelBinderCache.RegisterDerivedTypes(typeof(ProductTypeBase), new[] { typeof(Shirt), typeof(Pants) }); 
} 

añadir dos líneas a las vistas parciales:

@model MvcApplication.Models.Shirt 
@using MvcContrib.UI.DerivedTypeModelBinder 
@Html.TypeStamp() 
<div> 
    @Html.LabelFor(m => m.Color) 
</div> 
<div> 
    @Html.EditorFor(m => m.Color) 
    @Html.ValidationMessageFor(m => m.Color) 
</div> 

Por último, en la vista principal (usando EditorTemplates):

@model MvcApplication.Models.Product 
@{ 
    ViewBag.Title = "Products"; 
} 
<h2> 
    @ViewBag.Title</h2> 

@using (Html.BeginForm()) { 
    <div> 
     @Html.LabelFor(m => m.Name) 
    </div> 
    <div> 
     @Html.EditorFor(m => m.Name) 
     @Html.ValidationMessageFor(m => m.Name) 
    </div> 
    <div> 
     @Html.EditorFor(m => m.SubProduct) 
    </div> 
    <p> 
     <input type="submit" value="create" /> 
    </p> 
} 
1

así Tuve el mismo problema y lo he resuelto de una manera más general, creo. en mi caso estoy enviando objeto JSON a través de back-end para el cliente y desde el cliente al backend:

En primer lugar clase abstracta que tiene campo que me puse en el constructor:

ClassDescriptor = this.GetType().AssemblyQualifiedName; 

Así JSON tengo campo ClassDescriptor

lo siguiente fue escribir carpeta de encargo:

public class SmartClassBinder : DefaultModelBinder 
{ 
     protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) 
     { 

      string field = String.Join(".", new String[]{bindingContext.ModelName , "ClassDescriptor"}); 
       var values = (ValueProviderCollection) bindingContext.ValueProvider; 
       var classDescription = (string) values.GetValue(field).ConvertTo(typeof (string)); 
       modelType = Type.GetType(classDescription); 

      return base.CreateModel(controllerContext, bindingContext, modelType); 
     }  
} 

Y ahora todo lo que tengo que hacer es decorar la clase con al tributo. Por ejemplo:

[ModelBinder (typeof (SmartClassBinder))] public class ConfigurationItemDescription

Eso es todo.

Cuestiones relacionadas