MVC 3 Model Binding a Sub Type (Clase abstracta o interfaz)

Supongamos que tengo un modelo de Producto, el modelo de Producto tiene una propiedad de ProductSubType (resumen) y tenemos dos implementaciones concretas Camisa y Pantalones.

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 IU, el usuario tiene un menú desplegable, puede seleccionar el tipo de producto y los elementos de entrada se muestran de acuerdo con el tipo de producto correcto. 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).

Luego, 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, por la corazonada de que estoy haciendo esto incorrectamente, investigué un poco más y encontré esta respuesta de Darin Dimitrov:

ASP.NET MVC 2 – Enlace al modelo abstracto

Así que cambié la carpeta del modelo para que solo anule CreateModel, pero ahora no vincula 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; } 

Al pisar el MVC 3 src, parece que en BindProperties, GetFilteredModelProperties arroja un resultado vacío, y creo que se debe a que el modelo de 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!

Actualizar:

No lo dejé claro, pero el archivador de modelo personalizado que agregué, hereda de DefaultModelBinder

Responder

Establecer ModelMetadata y Model era la pieza que faltaba. 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); } 

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

    1. Permite crear un modelo y algunas clases base e infantil .

     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 crea un encuadernador y anula 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 crea get y post action.

     [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 configure MyModelBinder como Default ModelBinder en global.asax. Esto se hace para establecer una carpeta de modelo predeterminada para todas las acciones, para una sola acción podemos usar el atributo de ModelBinder en los 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 de tipo MyDerievedClass

    Index.cshtml

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

    Index

    @using (Html.BeginForm()) { @Html.ValidationSummary(true)
    MyModel @Html.EditorFor(m=>m.BaseClass,"DerievedView")

    }

    DerievedView.cshtml

     @model MvcApplication2.Models.MyDerievedClass @Html.ValidationSummary(true) 
    MyDerievedClass
    @Html.LabelFor(model => model.MyProperty)
    @Html.EditorFor(model => model.MyProperty) @Html.ValidationMessageFor(model => model.MyProperty)

    Ahora funcionará como se esperaba, el Controlador recibirá un Objeto del tipo “MyDerievedClass”. Las validaciones ocurrirán como se espera.

    enter image description here

    Tuve el mismo problema, terminé usando MvcContrib como se sugiere aquí .

    La documentación no está actualizada, pero si observa las muestras, es bastante fácil.

    Deberá registrar sus tipos en Global.asax:

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

    Agregue dos líneas a sus vistas parciales:

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

    Finalmente, en la vista principal (usando EditorTemplates ):

     @model MvcApplication.Models.Product @{ ViewBag.Title = "Products"; } 

    @ViewBag.Title

    @using (Html.BeginForm()) {
    @Html.LabelFor(m => m.Name)
    @Html.EditorFor(m => m.Name) @Html.ValidationMessageFor(m => m.Name)
    @Html.EditorFor(m => m.SubProduct)

    }

    bueno, tuve el mismo problema y lo he resuelto de una manera más general, creo. En mi caso, estoy enviando objetos a través de Json desde el backend al cliente y desde el cliente al back-end:

    Antes que nada En la clase abstracta tengo un campo que configuro en constructor:

     ClassDescriptor = this.GetType().AssemblyQualifiedName; 

    Entonces en el campo Json I Have ClassDescriptor

    Lo siguiente era escribir una carpeta personalizada:

     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 clase con atributo. Por ejemplo:

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

    Eso es.