Disons que j'ai un modèle de produit, le modèle de produit a une propriété de ProductSubType (abstrait) et nous avons deux implémentations concrètes Shirt et Pants.
Voici la source:
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; }
}
Dans mon interface utilisateur, l'utilisateur dispose d'une liste déroulante, il peut sélectionner le type de produit et les éléments d'entrée sont affichés en fonction du bon type de produit. J'ai tout compris (en utilisant un changement de liste déroulante get ajax, retournez un modèle partiel/éditeur et réinstallez la validation jquery en conséquence).
Ensuite, j'ai créé un classeur de modèle personnalisé pour 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;
}
}
Cela lie les valeurs correctement et fonctionne pour la plupart, sauf que je perds la validation côté serveur. Donc, sur une intuition que je fais mal, j'ai fait plus de recherches et suis tombé sur cette réponse de Darin Dimitrov:
ASP.NET MVC 2 - Liaison au modèle abstrait
J'ai donc changé le classeur de modèle pour ne remplacer que CreateModel, mais maintenant il ne lie pas les valeurs.
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;
}
En passant par le MVC 3 src, il semble que dans BindProperties, le GetFilteredModelProperties renvoie un résultat vide, et je pense que c'est parce que le modèle bindingcontext est défini sur ProductTypeBase qui n'a aucune propriété.
Quelqu'un peut-il repérer ce que je fais mal? Cela ne semble pas être difficile. Je suis sûr que je manque quelque chose de simple ... J'ai une autre alternative en tête au lieu d'avoir une propriété SubProduct dans le modèle de produit pour avoir juste des propriétés distinctes pour la chemise et le pantalon. Ce ne sont que des modèles View/Form, donc je pense que cela fonctionnerait, mais j'aimerais que l'approche actuelle fonctionne si quelque chose comprenne ce qui se passe ...
Merci pour toute aide!
Je n'ai pas été clair, mais le classeur de modèle personnalisé que j'ai ajouté hérite de DefaultModelBinder
La définition de ModelMetadata et Model était la pièce manquante. Merci 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);
}
Ceci peut être réalisé en remplaçant CreateModel (...). Je vais le démontrer avec un exemple.
1. Permet de créer un modèle et des classes de base et enfants.
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. Maintenant, créez un modelbinder et remplacez 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);
}
}
. Maintenant, dans le contrôleur, créez l'action get et post.
[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. Maintenant, définissez MyModelBinder comme ModelBinder par défaut dans global.asax Ceci est fait pour définir un classeur de modèle par défaut pour toutes les actions, pour une seule action, nous pouvons utiliser l'attribut ModelBinder dans les paramètres d'action)
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.DefaultBinder = new MyModelBinder();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
5. Maintenant, nous pouvons créer une vue de type MyModel et une vue partielle de type 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>
Maintenant, cela fonctionnera comme prévu, le contrôleur recevra un objet de type "MyDerievedClass". Les validations se feront comme prévu.
J'ai eu le même problème, j'ai fini par utiliser MvcContrib comme suggéré ici .
documentation est obsolète mais si vous regardez les exemples, c'est assez simple.
Vous devrez enregistrer vos types dans Global.asax:
protected void Application_Start(object sender, EventArgs e) {
// (...)
DerivedTypeModelBinderCache.RegisterDerivedTypes(typeof(ProductTypeBase), new[] { typeof(Shirt), typeof(Pants) });
}
Ajoutez deux lignes à vos vues partielles:
@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>
Enfin, dans la vue principale (en utilisant 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>
}
eh bien j'ai eu ce même problème et je l'ai résolu de manière plus générale je pense. Dans mon cas, j'envoie un objet via Json du backend au client et du client au backend:
Tout d'abord, dans la classe abstraite, j'ai un champ que j'ai défini dans le constructeur:
ClassDescriptor = this.GetType().AssemblyQualifiedName;
Donc, dans Json, j'ai le champ ClassDescriptor
La prochaine chose était d'écrire un classeur personnalisé:
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);
}
}
Et maintenant, tout ce que j'ai à faire est de décorer la classe avec un attribut. Par exemple:
[ModelBinder (typeof (SmartClassBinder))] classe publique ConfigurationItemDescription
C'est ça.