2012-02-23 15 views
44

Dire che ho un modello di prodotto, il modello di prodotto ha una proprietà di ProductSubType (abstract) e abbiamo due implementazioni concrete Shirt and Pants.MVC 3 Modello che associa un sottotipo (Classe astratta o interfaccia)

Qui è la fonte:

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; } 
} 

Nel mio UI, l'utente ha un menu a discesa, è possibile selezionare il tipo di prodotto e gli elementi di ingresso vengono visualizzati in base al tipo di prodotto giusto. Ho capito tutto questo (usando un ajax get on dropdown change, restituisci un partial/editor template e re-setup la validazione jquery di conseguenza).

Successivamente ho creato un raccoglitore modello personalizzato per 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; 

    } 
} 

Questa lega i valori correttamente e funziona per la maggior parte, tranne che perdo la validazione lato server. Così su una sensazione che sto facendo questo in modo non corretto ho fatto un po 'di ricerca e sono imbattuto in questa risposta da Darin Dimitrov:

ASP.NET MVC 2 - Binding To Abstract Model

Così ho acceso il modello legante per ignorare solo CreateModel, ma ora non lo fa legare i valori.

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; 
    } 

passo se il MVC 3 src, sembra che in BindProperties, i GetFilteredModelProperties restituisce un risultato vuoto, e penso che è perché il modello BindingContext è impostato su ProductTypeBase che non ha alcuna proprietà.

Qualcuno può individuare ciò che sto facendo male? Non sembra che dovrebbe essere così difficile. Sono sicuro che mi manca qualcosa di semplice ... Ho un'altra alternativa in mente invece di avere una proprietà SubProduct nel modello Product per avere solo proprietà separate per Shirt and Pants. Questi sono solo i modelli View/Form quindi penso che funzionerebbe, ma vorrei che l'approccio attuale funzioni se non altro per capire cosa sta succedendo ...

Grazie per qualsiasi aiuto!

Aggiornamento:

Non ho fatto in chiaro, ma il modello personalizzato legante ho aggiunto, eredita dalla DefaultModelBinder

risposta

impostazione ModelMetadata e modello era il pezzo mancante. Grazie 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); 

     } 

risposta

53

Questo può essere ottenuto tramite Overriding di CreateModel (...). Lo dimostrerò con un esempio.

1. Consente di creare un modello e alcune classi di base e figlio.

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. Ora creare un modelbinder e sovrascrivere 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. Ora nel controller creano ottenere e l'azione posta.

[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. ora impostato come predefinito MyModelBinder ModelBinder in global.asax Questo viene fatto per impostare un legante modello predefinito per tutte le azioni, per una singola azione che possiamo usare l'attributo ModelBinder nei parametri di azione)

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

     ModelBinders.Binders.DefaultBinder = new MyModelBinder(); 

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

5. Ora possiamo creare vista di tipo MyModel e una vista parziale di 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> 

Ora che funzionerà come previsto, controller riceverà un oggetto di tipo "MyDerievedClass". Le convalide avverranno come previsto.

enter image description here

+2

Perfetto, l'impostazione di ModelMetaData e Model in create era il pezzo mancante, grazie! –

+0

Ho avuto un problema molto simile con un tipo ereditato e derivato e il suddetto modello di codifica del modello ha fatto il trucco. Saluti! –

+0

Grazie! Trascorro quasi tre giorni provando cose diverse per ricorrere infine a questa soluzione, anche se il mio problema era un po 'diverso. –

4

Ho avuto lo stesso problema, ho finito per usare MvcContrib come sugested here.

Il documentation non è aggiornato ma se si guardano gli esempi è piuttosto semplice.

Dovrete registrare i tipi nel Global.asax:

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

aggiungere due righe al vostro punto di vista parziali:

@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> 

Infine, nella vista principale (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

ben Ho avuto lo stesso problema e ho risolto in un modo più generale, penso. Nel mio caso sto inviando oggetto attraverso JSON dal backend per client e dal client al backend:

prima di tutto nella classe astratta Ho campo che ho impostato nel costruttore:

ClassDescriptor = this.GetType().AssemblyQualifiedName; 

Così in JSON I have campo ClassDescriptor

La prossima cosa avrebbe scritto su misura legante:

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); 
     }  
} 

E ora tutto quello che devo fare è quello di decorare classe con a omaggio. Per esempio:

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

Questo è tutto.

Problemi correlati