50 votes

Modèle MVC 3 liant un sous-type (classe ou interface abstraite)

Dire que j'ai un modèle de Produit, le modèle de Produit a une propriété de ProductSubType (abstrait) et nous avons deux des implémentations concrètes Chemise et un Pantalon.

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, l'utilisateur dispose d'une liste déroulante permet de sélectionner le type de produit et les éléments d'entrée sont affichés selon le droit type de produit. J'ai tout compris (à l'aide d'un ajax obtenir sur la liste déroulante changer, retour partiel/éditeur de modèle et de ré-installation de jquery validation en conséquence).

Ensuite, j'ai créé un modèle de liaison 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;

    }
}

Cette lie les valeurs correctement et fonctionne pour la plupart, sauf que je perds le côté serveur de validation. Donc, sur une intuition que j'ai fais une erreur j'ai fait quelques recherches et suis tombé sur cette réponse par Darin Dimitrov:

ASP.NET MVC 2 - Liaison De Modèle Abstrait

J'ai donc changé le modèle de classeur remplacer que CreateModel, mais maintenant il ne se lient 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;
    }

Stepping bien que le MVC 3 src, il semble que dans BindProperties, le GetFilteredModelProperties retourne un résultat vide, et je pense que parce que bindingcontext modèle est défini à ProductTypeBase qui n'ont pas de propriétés.

Quelqu'un peut-il repérer ce que je fais de mal? Cela ne semble pas comme il devrait être, cela est difficile. Je suis sûr que je suis absent quelque chose de simple...j'ai une autre alternative à l'esprit, au lieu d'avoir un SubProduct propriété dans le modèle de Produit pour seulement ont des propriétés distinctes pour la Chemise et le Pantalon. Ce sont juste View/les modèles à Forme donc je pense que cela fonctionnerait, mais souhaitez obtenir de l'approche actuelle de travail, voire rien, pour comprendre ce qui se passe...

Merci pour toute aide!

Mise à jour:

Je n'ai pas préciser, mais le modèle de classeur que j'ai ajouté, hérite de la DefaultModelBinder

Réponse

Réglage ModelMetadata et le Modèle était la pièce manquante. Merci De 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);

        }

58voto

Manas Points 1632

Ceci peut être réalisé par le biais de substitution CreateModel(...). Je vais vous montrer qu'avec un exemple.

1. Permet de créer un modèle de base et les et les classes enfant.

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 remplacer 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. Maintenant, dans le contrôleur de créer get et 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. Maintenant Défini MyModelBinder en tant que par Défaut ModelBinder mondiale.asax Ceci est fait afin de définir un modèle de classeur par défaut pour toutes les actions, pour une seule action, nous pouvons utiliser ModelBinder attribut dans les paramètres de l'action)

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

        ModelBinders.Binders.DefaultBinder = new MyModelBinder();

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

5. Maintenant, nous pouvons créer un affichage de type Monmodèle 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, il fonctionnera comme prévu, Contrôleur de recevoir un Objet de type "MyDerievedClass". Les Validations va se passer comme prévu.

enter image description here

4voto

Marcel de Castilho Points 3508

J'ai eu le même problème, j'ai fini par utiliser MvcContrib comme suggéré ici.

La documentation est obsolète, mais si vous regardez les échantillons, c'est assez facile.

Vous devez inscrire votre types dans le monde.asax:

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

Ajouter deux lignes à votre vue partielle sur:

@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 (à l'aide de 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>
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X