Cette question a été posée avant dans les versions antérieures de MVC. Il y a aussi cette entrée de blog sur une façon de contourner le problème. Je me demande si MVC3 a introduit quelque chose qui pourrait aider, ou s'il existe d'autres options.
En un mot. Voici la situation. J'ai un modèle de base abstrait et 2 sous-classes concrètes. J'ai une vue fortement typée qui rend les modèles avec EditorForModel()
. Ensuite, j'ai des modèles personnalisés pour rendre chaque type de béton.
Le problème survient au moment de la publication. Si je fais en sorte que la méthode post-action prenne la classe de base comme paramètre, MVC ne peut pas en créer une version abstraite (ce que je ne voudrais pas de toute façon, je voudrais qu'elle crée le type concret réel). Si je crée plusieurs méthodes de post-action qui ne varient que par la signature des paramètres, alors MVC se plaint d'être ambigu.
Donc, pour autant que je sache, j'ai quelques choix sur la façon de résoudre ce problème. Je n'aime aucun d'entre eux pour diverses raisons, mais je vais les énumérer ici:
Je n'aime pas 1, car c'est essentiellement la configuration qui est cachée. Certains autres développeurs travaillant sur le code ne le savent peut-être pas et perdent beaucoup de temps à essayer de comprendre pourquoi les choses se cassent lorsque les choses changent.
Je n'aime pas 2, car cela semble un peu hacky. Mais je penche pour cette approche.
Je n'aime pas 3, car cela signifie violer DRY.
D'autres suggestions?
Éditer:
J'ai décidé de suivre la méthode de Darin, mais j'ai fait un léger changement. J'ai ajouté ceci à mon modèle abstrait:
[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}
Puis un caché est généré automatiquement dans ma DisplayForModel()
. La seule chose dont vous devez vous souvenir est que si vous n'utilisez pas DisplayForModel()
, vous devrez l'ajouter vous-même.
Puisque j'opte évidemment pour l'option 1 (:-)) permettez-moi d'essayer de la développer un peu plus afin qu'elle soit moins cassable et d'éviter le codage en dur des instances concrètes dans le classeur de modèles. L'idée est de passer le type de béton dans un champ caché et d'utiliser la réflexion pour instancier le type de béton.
Supposons que vous disposiez des modèles de vue suivants:
public abstract class BaseViewModel
{
public int Id { get; set; }
}
public class FooViewModel : BaseViewModel
{
public string Foo { get; set; }
}
le contrôleur suivant:
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 vue Index
correspondante:
@model BaseViewModel
@using (Html.BeginForm())
{
@Html.Hidden("ModelType", Model.GetType())
@Html.EditorForModel()
<input type="submit" value="OK" />
}
et le ~/Views/Home/EditorTemplates/FooViewModel.cshtml
modèle d'éditeur:
@model FooViewModel
@Html.EditorFor(x => x.Id)
@Html.EditorFor(x => x.Foo)
Maintenant, nous pourrions avoir le classeur de modèle personnalisé suivant:
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;
}
}
Le type réel est déduit de la valeur du champ caché ModelType
. Il n'est pas codé en dur, ce qui signifie que vous pouvez ajouter d'autres types d'enfants plus tard sans avoir à toucher ce classeur de modèle.
Cette même technique pourrait être facilement applicable aux collections de modèles de vue de base.
Je viens de penser à une solution intéressante à ce problème. Au lieu d'utiliser la liaison de modèle bsed de paramètre comme ceci:
[HttpPost]
public ActionResult Index(MyModel model) {...}
Je peux plutôt utiliser TryUpdateModel () pour me permettre de déterminer à quel type de modèle se lier dans le code. Par exemple, je fais quelque chose comme ça:
[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);
}
Cela fonctionne en fait beaucoup mieux de toute façon, car si je fais un traitement, je devrais de toute façon convertir le modèle en quoi que ce soit, ou utiliser is
pour trouver la bonne carte pour appeler avec AutoMapper.
Je suppose que ceux d'entre nous qui n'ont pas utilisé MVC depuis le premier jour oublient UpdateModel
et TryUpdateModel
, mais il a toujours ses utilisations.
Il m'a fallu une bonne journée pour trouver une réponse à un problème étroitement lié - bien que je ne sois pas sûr que ce soit exactement le même problème, je le poste ici au cas où d'autres chercheraient une solution au même problème exact.
Dans mon cas, j'ai un type de base abstrait pour un certain nombre de types de modèles de vue différents. Donc, dans le modèle de vue principal, j'ai une propriété d'un type de base abstrait:
class View
{
public AbstractBaseItemView ItemView { get; set; }
}
J'ai un certain nombre de sous-types de AbstractBaseItemView, dont beaucoup définissent leurs propres propriétés exclusives.
Mon problème est que le modèle-classeur ne regarde pas le type d'objet attaché à View.ItemView, mais regarde uniquement le type de propriété déclaré, qui est AbstractBaseItemView - et décide de lier uniquement les propriétés définies dans le type abstrait, en ignorant les propriétés spécifiques au type concret de AbstractBaseItemView qui se trouve être utilisé.
La solution pour cela n'est pas jolie:
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);
}
// ...
}
Bien que ce changement semble hacky et très "systémique", il semble fonctionner - et ne représente pas, autant que je sache, un risque de sécurité considérable, car il ne le fait pas lier à CreateModel () et donc ne pas vous permettre de publier quoi que ce soit et de tromper le classeur de modèles pour créer n'importe quel objet .
Il ne fonctionne également que lorsque le type de propriété déclaré est de type abstrait , par ex. une classe abstraite ou une interface.
Sur une note connexe, il me semble que d'autres implémentations que j'ai vues ici qui remplacent CreateModel () ne fonctionneront probablement que lorsque vous publierez entièrement nouveaux objets - et souffrira du même problème que j'ai rencontré, lorsque le type de propriété déclaré est de type abstrait. Donc, vous ne pourrez probablement pas modifier les propriétés spécifiques des types de béton sur existantes modélisez les objets, mais n'en créez que de nouveaux.
En d'autres termes, vous devrez probablement intégrer cette solution de contournement dans votre classeur pour pouvoir également modifier correctement les objets qui ont été ajoutés au modèle de vue avant la liaison ... Personnellement, je pense que c'est une approche plus sûre, car Je contrôle quel type concret est ajouté - de sorte que le contrôleur/l'action peut, indirectement, spécifier le type concret qui peut être lié, en remplissant simplement la propriété avec une instance vide.
J'espère que cela sera utile aux autres ...
En utilisant la méthode de Darin pour discriminer vos types de modèles via un champ caché dans votre vue, je vous recommande d'utiliser un RouteHandler
personnalisé pour distinguer vos types de modèles et de diriger chacun vers une action au nom unique sur votre contrôleur. Par exemple, si vous avez deux modèles concrets, Foo et Bar, pour votre action Create
dans votre contrôleur, effectuez une action CreateFoo(Foo model)
et une action CreateBar(Bar model)
. Ensuite, créez un RouteHandler personnalisé, comme suit:
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;
}
}
Ensuite, dans Global.asax.cs, modifiez RegisterRoutes()
comme suit:
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()));
}
Ensuite, lorsqu'une demande de création arrive, si un ModelType est défini dans le formulaire renvoyé, le RouteHandler ajoute le ModelType au nom de l'action, permettant ainsi de définir une action unique pour chaque modèle concret.