59 votes

Liaison de modèle polymorphe

Cette question a été posée avant dans les versions antérieures de la MVC. Il y a aussi cette entrée de blog sur un moyen de contourner le problème. Je me demandais si MVC3 a introduit quelque chose qui pourrait aider, ou si il y a d'autres options.

Dans une coquille de noix. Voici la situation. J'ai un résumé modèle de base, et 2 sous-classes concrètes. J'ai fortement typé vue qui rend les modèles avec EditorForModel(). Puis-je avoir des modèles personnalisés pour le rendu de chaque type de béton.

Le problème vient à la fois de la poste. Si je fais le post de la méthode d'action de prendre la classe de base comme le paramètre, puis MVC ne peut pas créer une version abstraite (ce qui je ne voudrais pas, de toute façon, j'avais envie de créer le véritable type de béton). Si je créer plusieurs post méthodes d'action qui ne varient que par le paramètre signature, puis MVC se plaint que c'est ambigu.

Donc autant que je peux dire, j'ai un peu de choix sur la façon de résoudre ce problème. Je n'aime pas l'un d'eux, pour diverses raisons, mais je vais en faire la liste ici:

  1. Créer un modèle personnalisé classeur en tant que Darin suggère dans le premier post, je lien.
  2. Créer un discriminateur attribut comme le deuxième post je l'ai lié à l'indique.
  3. Post de différentes méthodes d'action basé sur le type
  4. ???

Je n'aime pas le 1, parce que c'est fondamentalement la configuration de ce qui est caché. Certains autres développeurs à travailler sur le code ne peut pas savoir à ce sujet et de déchets beaucoup de temps à essayer de comprendre pourquoi les choses casser quand change les choses.

Je n'aime pas les 2, car il semble genre de hacky. Mais, je me suis penché vers cette approche.

Je n'aime pas le 3, parce que cela signifie que la violation SÈCHE.

Toutes les autres suggestions?

Edit:

J'ai décidé d'aller avec Darin méthode, mais fait un léger changement. J'ai ajouté à mon modèle abstrait:

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

Alors un cachée est automatiquement généré dans mon DisplayForModel(). La seule chose que vous avez à retenir est que si vous n'utilisez pas d' DisplayForModel(), vous devez les ajouter vous-même.

62voto

Darin Dimitrov Points 528142

Depuis que j'ai évidemment opter pour l'option 1 (:-)) permettez-moi de tenter d'élaborer un peu plus, de sorte qu'il est moins cassante et d'éviter de coder en dur des cas concrets dans le modèle de classeur. L'idée est de passer le type de béton dans un champ caché et utiliser la réflexion pour instancier le type de béton.

Supposons que vous ayez le point de vue suivant les modèles:

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 correspondante Index vue:

@model BaseViewModel
@using (Html.BeginForm())
{
    @Html.Hidden("ModelType", Model.GetType())    
    @Html.EditorForModel()
    <input type="submit" value="OK" />
}

et l' ~/Views/Home/EditorTemplates/FooViewModel.cshtml éditeur de template:

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

Maintenant, nous pourrions avoir l'personnalisé suivant le modèle de classeur:

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 de l' ModelType champ caché. Il n'est pas codé en dur, ce qui signifie que vous pouvez ajouter d'autres types enfants plus tard sans avoir à toucher ce modèle de classeur.

Cette même technique peut être facilement être appliquée à des collections de vue de la base de modèles.

15voto

Erik Funkenbusch Points 53436

J'ai juste pensé à un intéressant solution à ce problème. Au lieu d'utiliser le Paramètre bsed de la liaison de modèle comme ceci:

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

Je peux utiliser plutôt tryupdatemodel pour mettre() pour me permettre de déterminer quel type de modèle de la liaison dans le code. Par exemple, je fais quelque chose comme ceci:

[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 beaucoup mieux de toute façon, parce que si je fais tout traitement, alors que j'aurais à lancer le modèle de ce qu'il est réellement de toute façon, ou utiliser l' is à la figure exacte de la Carte d'appel avec AutoMapper.

Je suppose que ceux d'entre nous qui n'ont pas été en utilisant MVC depuis le jour 1 oublier UpdateModel et TryUpdateModel, mais il a encore ses utilisations.

7voto

mindplay.dk Points 2555

Il m'a fallu une bonne journée pour trouver une réponse à un problème étroitement lié - bien que je ne suis pas sûr que c'est précisément la même question, je poste ici au cas où d'autres sont à la recherche d'une solution au même problème.

Dans mon cas, j'ai une base abstraite de type pour un certain nombre de vue différents-types de modèle. Ainsi, dans la vue principale de modèle, j'ai une propriété d'un résumé de base de type:

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

J'ai un certain nombre de sous-types de AbstractBaseItemView, beaucoup de qui définissent leurs propres propriétés exclusives.

Mon problème, c'est le modèle de classeur ne regarde pas le type de l'objet attaché à Vue.ItemView, mais au lieu de cela ne regarde que la propriété déclarée de type, qui est AbstractBaseItemView - et décide de se lier uniquement les propriétés définies dans le type abstrait, en ignorant les propriétés spécifiques pour le type concret de AbstractBaseItemView qui se trouve être en cours d'utilisation.

Le travail autour de ce n'est pas assez:

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 se sent hacky et est très "systémique", il semble que cela fonctionne et ne fonctionne pas, autant que je peux comprendre, poser un considérable sécurité-risque, car il n'a pas de cravate en CreateModel() et ne fait donc pas vous permettre de publier quoi que ce soit et tromper le modèle de classeur dans la création de n'importe quel objet.

Il travaille également que lorsque la propriété déclarée de type est un résumé de type, par exemple une classe abstraite ou une interface.

Sur une note connexe, il me semble que d'autres implémentations que j'ai vu ici que remplacer CreateModel() sera probablement seulement le travail lorsque vous êtes à poster des objets entièrement nouveaux - et souffrent du même problème que j'ai rencontré, lors de la déclaration de la propriété type d'un type abstrait. Donc, vous avez très probablement ne sera pas en mesure de modifier les propriétés spécifiques de types de béton sur existant objets de modèle, mais seulement d'en créer de nouveaux.

Donc, en d'autres termes, vous aurez probablement besoin d'intégrer ce travail autour dans votre classeur d'être également en mesure de modifier les objets qui ont été ajoutés à la vue-modèle avant de liaison... Personnellement, je pense que c'est une approche plus sûre, depuis que j'ai le contrôle de ce type de béton ajoutée - de sorte que le contrôleur/action peut, indirectement, spécifiez le type de béton qui peut être lié, par un simple remplissage de la propriété avec un vide de l'instance.

J'espère que cela sera utile à d'autres...

4voto

counsellorben Points 7865

À l'aide de Darin la méthode de discrimination à vos types de modèles par l'intermédiaire d'un champ caché de votre point de vue, je vous recommande d'utiliser une coutume RouteHandler de distinguer vos types de modèle, et de diriger chacun à un nom unique, l'action sur votre contrôleur. Par exemple, si vous avez deux des modèles concrets, Foo et Bar, pour votre Create d'action dans votre contrôleur, faire un CreateFoo(Foo model) d'action et un CreateBar(Bar model) action. Ensuite, faire une personnalisée RouteHandler, 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; 
    }
}

Puis, dans Global.asax.cs, changement 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())); 
} 

Alors, quand un Créer de la demande, si un ModelType est défini dans le formulaire retourné, le RouteHandler ajouter la ModelType pour le nom de l'action, permettant une action unique être définies pour chaque modèle concret.

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