web-dev-qa-db-fra.com

Comment séparer le ViewModel du modèle, tout en les associant les uns aux autres en fonction du modèle?

Dans une application wpf, un objet Model est créé à partir de zéro ou désérialisé à partir d'un fichier .xml. Pour créer une connexion au ViewModel, la méthode suivante (sur le Model!) Est utilisée par l'application:

class Model
{
    public override UserControl View
    {
        get
        {
            if (_view == null)
            {
                ViewModel viewModel = new ViewModel(this);
                _view = new View();
                _view.DataContext = viewModel;
            }
            return _view;
        }
    }
}

Jusqu'à présent, donc pasmvvm . Le Model ne doit pas connaître (= avoir une référence directe à) le ViewModel, sans parler du View.

mvvm diagram

Briser le modèle de cette façon a servi de raccourci pratique, mais commence à faire des ravages. Je voudrais me débarrasser de cette méthode pour séparer correctement les préoccupations comme prévu par le modèle.

Mais comment ferais-je cela? La fonctionnalité de la méthode en elle-même semble correcte en mmvm. Si je le retirais de Model et le plaçais dans l'application, cela ressemble au bon code mvvm-ish qui connecte les composants:

ViewModel viewModel = new ViewModel(new Model);
_view = new View();
_view.DataContext = viewModel;

Mais je ne peux pas faire cela, car je ne sais pas à quelle classe Model je fais face, lorsque j'examine le problème du point de vue de l'application, car l'une des plusieurs classes Model peut obtenir instancié ou désérialisé. Dans un certain sens, le Model sait le mieux ce que ViewModel à utiliser (d'où l'implémentation actuelle), mais c'est exactement la dépendance que j'essaie d'éviter.

Comment construire les parties du modèle mvvm "à contre-courant" des dépendances du modèle? Autrement dit, passer de Model à View(Model) et pas les autres inverse.

  • Une solution simple pourrait être de définir un structure de données dans l'application (par exemple un Dictionary) à associer un ViewModel spécifique qui devrait être utilisé avec un spécifique Model, mais cela signifie une maintenance lors de l'ajout de classes, des erreurs, etc.
  • On pourrait aussi construire cette association avec la réflexion introduire un schéma de nommage strict comme chaque FooModel ira de pair avec un FooModelView, ce qui serait une amélioration, mais est toujours sujet à casser.
  • Ou utilisez un thingy injector de dépendance (par exemple mef ) pour automatiser le processus de recherche des pièces qui vont ensemble. Mais encore une fois, les dépendances semblent aller dans le mauvais sens: le ViewModel pourrait [Import] Un IModel que le Model pourrait alors [export], mais le Model (= la dépendance) est créé en premier, donc je chercherais des importations qui seraient satisfaites d'une exportation donnée et non l'inverse. Bien que ces outils aideraient à diviser les préoccupations entre différents assemblages (ce qui est bon et un objectif aussi), ils ne résolvent pas le problème sous-jacent que je ne sais pas comment commencer cette séparation en premier lieu, ce qui n'est pas 't leur but de toute façon. Si je permute l'importation et l'exportation, je suppose que je finirais avec ce que j'ai commencé.

Existe-t-il de meilleures solutions que de créer une association entre les deux (soit en codant en dur soit par réflexion) dans l'application?


pouvez-vous expliquer comment et pourquoi chaque ViewModel ne connaît pas déjà la classe Model appropriée à instancier et les méthodes Model appropriées à appeler? N'est-ce pas l'une des principales raisons d'avoir des classes ViewModel en premier lieu?

Chaque ViewModel connaît la classe Model appropriée. Je suis désolé si cela ne ressort pas clairement du texte. Dans l'exemple de code, le Model est passé au constructeur du ViewModel. ViewModel ne pas instancie l'objet Model car cet objet pourrait être construit ailleurs (par exemple, il pourrait provenir d'un processus de désérialisation). C'est en effet la principale raison de son existence.

Le problème est de décider quel ViewModel créer, de sorte que le Model (d'un type qui est décidé au moment de l'exécution) puisse lui être transmis.

En supposant que vous avez l'intention d'utiliser une forme d'IoC pour résoudre ce problème, ne pourriez-vous pas simplement utiliser la configuration du conteneur DI pour identifier le modèle requis, ou utiliser un localisateur de service?

Pouvez-vous également clarifier davantage la phrase "Briser le schéma de cette manière a servi de raccourci pratique, mais commence à faire des ravages"?

Étant donné que le Model lui-même garantissait que le bon ViewModel (et View) serait instancié, la valeur renvoyée pourrait être négligemment ajoutée à l'écran. Le câblage correct des composants s'est produit dans la méthode vue dans le premier exemple de code. Voilà pour la commodité de pouvoir "afficher" un Model. Le prix à payer est que Model utiliserait et donc référencerait View et ViewModel, qui sont des classes qui dépendent des bibliothèques GUI, que le Model devrait pas dépendre.

ou, pour être franc: pourquoi cette application console a-t-elle besoin de 5 DLL pour les composants de l'interface utilisateur graphique?

ne pourriez-vous pas simplement utiliser la configuration du conteneur DI pour identifier le modèle requis, ou utiliser un localisateur de service?

Je suppose que la structure de données mentionnée qui associe ViewModel à Model est un localisateur de services (pour les pauvres). Je suis nouveau sur ce modèle, mais à première vue, cela semble être le cas. Utiliser le type attendu de ViewModels pour le construire est quelque chose auquel je n'ai pas pensé. (Je pense et pas ou concernant votre question) En ce qui concerne cette approche, je prendrais votre suggestion comme réponse. J'espérais cependant une solution un peu moins DIY-ish. En voyant comment la liaison fait beaucoup de choses automatiquement, je suis quelque peu surpris de constater que le fait d'aller dans la direction opposée (c'est-à-dire d'avoir un Model pour commencer) nécessite une telle gestion manuelle des dépendances. Mais alors c'est peut-être ce qu'il faut.

4
surface

Voici mes suggestions:

Suggestion 1

Vous pouvez essayer d'utiliser des méthodes d'extension afin de séparer les préoccupations et, à la fin, effectuer exactement ce dont vous avez besoin (modèles qui connaissent leurs modèles de vue respectifs):

using System;
using System.Collections.Generic;

namespace Yay
{
    public class BaseModel
    {
        public bool CommonStuff { get; set; }
    }

    public class FromScratchModel : BaseModel
    {
        public string A { get; set; }
        public string B { get; set; }

    }

    public class DeserializedModel : BaseModel
    {
        public int C { get; set; }
        public bool D { get; set; }

    }

    public class BaseViewModel
    {
        public override string ToString()
        {
            return "Base View Model!!!!";
        }
    }

    public class ViewModel1 : BaseViewModel
    {
        public override string ToString()
        {
            return "View Model - From Scratch!!!!";
        }
    }

    public class ViewModel2 : BaseViewModel
    {
        public override string ToString()
        {
            return "View Model - Deserialized!!!!";
        }
    }

    static class Extensions
    {

        public static BaseViewModel GetViewModel(this BaseModel model)
        {
            return new BaseViewModel();
        }

        public static BaseViewModel GetViewModel(this FromScratchModel model)
        {
            return new ViewModel1();
        }

        public static BaseViewModel GetViewModel(this DeserializedModel model)
        {
            return new ViewModel2();
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            IList<BaseModel> models = 
                new List<BaseModel> { new BaseModel(), 
                                    new FromScratchModel(), 
                                    new DeserializedModel() };
            foreach (var model in models)
            {
                //Calling model.GetViewModel() will not work here; already tried (it runs, but it will return the BaseViewModel always).
                //But it's possible to call the right method resolved at runtime
                //using dynamic keyword
                BaseViewModel viewModel = Extensions.GetViewModel(model as dynamic);
                Console.WriteLine(viewModel.ToString());
            }

            Console.ReadLine();
        }
    }
}

Suggestion 2

Vous avez différents modèles qui peuvent arriver, qui sont résolus au moment de l'exécution. Dans cette option, je suggère:

  • Sur votre projet principal (pas le projet d'interface utilisateur), pour chaque type de votre modèle, créez une classe "chargeur de modèle", qui informera les clients des nouveaux modèles (par exemple: DeserializedModelLoader; FromScratchModelLoader)
  • Sur votre interface utilisateur, créez un écouteur pour chaque type de modèle existant (par exemple: DeserializedModelListener, FromScratchModelListener);
  • chaque écouteur peut renvoyer un ViewModel prêt à être rendu, qui sera du type approprié et contient déjà le modèle reçu;
  • tenir une liste de ces auditeurs; pour chaque nouveau modèle arrivé, vous pouvez récupérer le ViewModel approprié de l'auditeur qui a reçu quelque chose.
1
Emerson Cardoso

Ce qui suit est le résultat de mes délibérations au cours des deux derniers jours. La solution qui en résulte est une idée, qui contient également certains problèmes que j'ai déjà découverts. J'espère avoir une discussion pour savoir si la structure et la conception de la solution sont bonnes et quels (anti) schémas elle peut réaliser. Puisque mes idées proviennent de ce post, je vais le poster comme une réponse ici, mais j'ai aussi pensé à poser une nouvelle question (lien vers celle-ci).


Eh bien, nous voulons y parvenir modèles sont complètement indépendants de voir les modèles (encore moins de vues) et inférer encore à un, si nous en avons besoin. Imaginons que nos modèles soient POCO s:

class Base { /* some properties */ }
class Special : Base { /* more properties */ }
class ExtraSpecial : Special { /* even more properties */ }

Maintenant, nous utilisons une approche générique simple comme classe de base pour nos voir les modèles:

abstract class ViewModel<ModelType>
{
    protected ModelType Model;

    public ViewModel(ModelType model)
    {
        this.Model = model;
    }
}

class BaseViewModel : ViewModel<Base> { /* constructor and view model stuff */ }

class SpecialViewModel : ViewModel<Special> { /* constructor and view model stuff */ }

Maintenant, nos voir les modèles connaissent le type de modèle, qu'ils doivent présenter à la vue respective. Le view sait seulement quoi montrer pour quel view-model.


Revenons au problème dans la question. Nous obtenons un nouveau modèle (de n'importe où, par exemple désérialisation) et nous savons qu'il s'agit d'un Base, mais nous ne savons pas lequel Base. Nous pourrions simplement utiliser un BaseViewModel, mais s'il s'agit d'un Special, nous conserverons tout au plus des fonctionnalités lâches. Nous voulons vérifier quel type de Base est vraiment notre modèle. Pour un cas simple, nous pourrions utiliser quelques branches if-, mais cela deviendra très laid bientôt. En utilisant la réflexion, nous pourrions trouver un nom voir le modèle via le nom modèle, mais les règles de nommage (et non les conventions) sont merdiques neuf fois sur dix. Nous pourrions établir une relation entre modèles et voir les modèles ... peut-être utiliser un dictionnaire ... ou les enregistrer dans une sorte de localisateur de service ... mais attendez! Nous avons déjà construit une relation via des génériques, de nos voir les modèles aux modèles. Ne pourrions-nous pas simplement vérifier quel voir le modèle est capable de gérer notre type modèle?

Tout d'abord, nous collectons tous les voir les modèles ...

HashSet<Type> types = new [] { typeof(BaseViewModel), typeof(SpecialViewModel) };
// or we collect them from some Assembly or namespace

Ensuite, nous extrayons le type modèle pour chacun d'eux:

for (; type != null; type = type.BaseType)
{
    if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(ViewModel<>)))
    {
        return type.GetGenericArguments().Single();
    }
}
return null;

Et enfin, nous pouvons vérifier si cela correspond à notre cible modèle:

type.IsAssignableFrom(modelTypeTarget)

Premier problème: Si nous ne vérifions que l'assignation (au lieu de l'égalité), nous trouverons plusieurs possibles voir les modèles pour a modèle, nous devons décider lequel utiliser.

Solution: Nous calculons une distance d'héritage (et la minimisons) pour déterminer laquelle voir le modèle correspond à a modèle le meilleur:

private static uint GetInheritanceDistance(Type type, Type parent)
{
    uint i = 0;
    for (; type != null; type = type.BaseType)
    {
        if (type.Equals(parent))
        {
            return i;
        }
        i++;
    }
    return uint.MaxValue;
}

De cette façon, un ExtraSpecial obtiendra un SpecialViewModel assigné, pas un BaseViewModel.


Deuxième problème : Que se passe-t-il si nous avons besoin (ou voulons) plusieurs voir les modèles pour un modèle, par exemple un pour afficher une entrée et l'autre pour l'édition?

Solution : Nous introduisons des contextes. Ces contextes peuvent être attribués aux types voir le modèle via des attributs:

[ViewModelContext(Context.View)]
class BaseViewModel : ViewModel<Base> { /* ... */ }
[ViewModelContext(Context.Edit)]
class EditBaseViewModel : ViewModel<Base> { /* ... */ }

Maintenant, nous ne considérons que les modèles de vue qui correspondent au contexte dont nous avons besoin dans notre cas spécifique.


Juste pour votre information, j'ai implémenté la fonctionnalité que j'ai décrite ci-dessus, donc je peux ajouter un lien de référentiel plus tard, mais pour l'instant je veux savoir si cela peut résoudre le problème décrit dans la question et s'il serait considéré comme un bon solution.

1
Lukas Körfer