Legame modello polimorfico

Questa domanda è stata già chiesta nelle versioni precedenti di MVC. C’è anche questo post sul blog su come aggirare il problema. Mi chiedo se MVC3 abbia introdotto qualcosa che potrebbe aiutare o se ci sono altre opzioni.

In poche parole. Ecco la situazione. Ho un modello base astratto e 2 sottoclassi concrete. Ho una vista fortemente tipizzata che rende i modelli con EditorForModel() . Quindi ho modelli personalizzati per rendere ogni tipo concreto.

Il problema arriva al momento post. Se creo il metodo post action, prendo la class base come parametro, quindi MVC non può creare una versione astratta di esso (che non vorrei comunque, vorrei creare il concreto tipo concreto). Se creo più metodi di post azione che variano solo dalla firma dei parametri, MVC si lamenta che è ambiguo.

Quindi, per quanto posso dire, ho alcune scelte su come risolvere questo problema. Non mi piace nessuno di loro per vari motivi, ma li elencherò qui:

  1. Crea un raccoglitore di modelli personalizzato come suggerisce Darin nel primo post a cui mi sono collegato.
  2. Crea un attributo discriminatore come suggerisce il secondo post che ho collegato.
  3. Pubblica su diversi metodi di azione in base al tipo
  4. ???

Non mi piace 1, perché è fondamentalmente una configurazione che è nascosta. Qualche altro sviluppatore che lavora sul codice potrebbe non saperlo e perdere un sacco di tempo cercando di capire perché le cose si rompono quando cambiano le cose.

Non mi piace il 2, perché sembra un po ‘hacky. Ma, mi sto appoggiando a questo approccio.

Non mi piace il 3, perché questo significa violare ASCIUTTO.

Qualche altro suggerimento?

Modificare:

Ho deciso di andare con il metodo di Darin, ma ho fatto un piccolo cambiamento. Ho aggiunto questo al mio modello astratto:

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

Quindi un nascosto viene generato automaticamente nel mio DisplayForModel() . L’unica cosa che devi ricordare è che se non stai usando DisplayForModel() , dovrai aggiungerlo tu stesso.

Dal momento che ovviamente opto per l’opzione 1 (:-)) lasciatemi tentare di elaborarlo un po ‘di più in modo che sia meno fragile ed evitare le istanze concrete hardcoding nel raccoglitore del modello. L’idea è di passare il tipo concreto in un campo nascosto e utilizzare la riflessione per istanziare il tipo concreto.

Supponiamo di avere i seguenti modelli di vista:

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

il seguente controller:

 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 Index corrispondente:

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

e il modello dell’editor ~/Views/Home/EditorTemplates/FooViewModel.cshtml :

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

Ora potremmo avere il seguente modello di raccoglitore personalizzato:

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

Il tipo effettivo viene dedotto dal valore del campo nascosto ModelType . Non è hardcoded, il che significa che è ansible aggiungere altri tipi di bambini in un secondo momento senza dover mai toccare questo modello di raccoglitore.

Questa stessa tecnica potrebbe essere facilmente applicata alle raccolte di modelli di visualizzazione di base.

Ho appena pensato a una soluzione interessante a questo problema. Invece di utilizzare l’associazione del modello bsed Parameter come questo:

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

Posso invece usare TryUpdateModel () per permettermi di determinare il tipo di modello da associare al codice. Ad esempio faccio qualcosa del genere:

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

Questo in realtà funziona molto meglio in ogni caso, perché se sto facendo qualche elaborazione, allora dovrei lanciare il modello a qualsiasi cosa sia effettivamente, o usare is per capire la Mappa corretta da chiamare con AutoMapper.

Immagino che quelli di noi che non utilizzano MVC dal primo giorno dimentichino UpdateModel e TryUpdateModel , ma ha ancora i suoi usi.

Mi ci è voluta una buona giornata per trovare una risposta a un problema strettamente correlato – anche se non sono sicuro che sia esattamente lo stesso problema, lo sto postando nel caso in cui altri stiano cercando una soluzione allo stesso identico problema.

Nel mio caso, ho un tipo base astratto per diversi tipi di modelli di viste. Quindi nel modello di vista principale, ho una proprietà di un tipo di base astratto:

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

Ho un numero di sottotipi di AbstractBaseItemView, molti dei quali definiscono le loro proprietà esclusive.

Il mio problema è che il model-binder non guarda il tipo di object collegato a View.ItemView, ma invece guarda solo al tipo di proprietà dichiarato, che è AbstractBaseItemView – e decide di associare solo le proprietà definite nel tipo astratto, ignorando le proprietà specifiche del tipo concreto di AbstractBaseItemView che è in uso.

La soluzione per questo non è carina:

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

Anche se questo cambiamento si sente hacky ed è molto “sistemico”, sembra funzionare – e non, per quanto posso immaginare, pone un notevole rischio per la sicurezza, dal momento che non si collega a CreateModel () e quindi non ti consente per postare qualsiasi cosa e ingannare il modello-legatore per creare qualsiasi object.

Funziona anche quando il tipo di proprietà dichiarato è un tipo astratto , ad esempio una class astratta o un’interfaccia.

In una nota correlata, mi viene in mente che altre implementazioni che ho visto qui che sovrascrivono CreateModel () probabilmente funzioneranno solo quando pubblicherete oggetti completamente nuovi – e soffriranno dello stesso problema a cui mi sono imbattuto, quando la proprietà dichiarata -tipo è di un tipo astratto. Quindi molto probabilmente non sarai in grado di modificare proprietà specifiche dei tipi di calcestruzzo su oggetti modello esistenti , ma solo di crearne di nuovi.

Quindi, in altre parole, probabilmente avrai bisogno di integrare questo work-around nel tuo raccoglitore per essere anche in grado di modificare correttamente gli oggetti che sono stati aggiunti al modello di vista prima del binding … Personalmente, ritengo che sia un approccio più sicuro, dal momento che Controllo il tipo di concreto che viene aggiunto – quindi il controllore / azione può, indirettamente, specificare il tipo concreto che può essere associato, semplicemente compilando la proprietà con un’istanza vuota.

Spero che questo sia utile per gli altri …

Utilizzando il metodo Darin per discriminare i tipi di modelli tramite un campo nascosto nella visualizzazione, ti consigliamo di utilizzare un RouteHandler personalizzato per distinguere i tipi di modello e indirizzare ciascuno di essi a un’azione dal nome univoco sul controller. Ad esempio, se hai due modelli in cemento, Foo e Bar, per la tua azione Create nel tuo controller, crea un’azione CreateFoo(Foo model) e un’azione CreateBar(Bar model) . Quindi, crea un RouteHandler personalizzato, come segue:

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

Quindi, in Global.asax.cs, cambia RegisterRoutes() come segue:

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

Quindi, quando arriva una richiesta di creazione, se nel modulo restituito viene definito un modelloTipo, RouteHandler aggiungerà ModelType al nome dell’azione, consentendo di definire un’azione unica per ogni modello concreto.