Comment rendre la création de modèles de vue à l'exécution moins pénible

17

Je m'excuse pour la longue question, elle se lit un peu comme une diatribe, mais je promets que ce n'est pas! J'ai résumé ma (mes) question (s) ci-dessous

Dans le monde MVC, les choses sont simples. Le modèle a un état, la vue montre le modèle et le contrôleur fait des choses avec / avec le modèle (en gros), un contrôleur n'a pas d'état. Pour faire des choses, le contrôleur a des dépendances sur les services Web, le référentiel, le lot. Lorsque vous instanciez un contrôleur, vous vous souciez de fournir ces dépendances, rien d'autre. Lorsque vous exécutez une action (méthode sur Controller), vous utilisez ces dépendances pour récupérer ou mettre à jour le modèle ou appeler un autre service de domaine. S'il y a un contexte, par exemple, comme un utilisateur souhaite voir les détails d'un élément particulier, vous passez l'ID de cet élément comme paramètre à l'action. Nulle part dans le contrôleur il n'y a de référence à un état. Jusqu'ici tout va bien.

Entrez MVVM. J'adore WPF, j'adore la liaison de données. J'adore les frameworks qui rendent la liaison de données à ViewModels encore plus facile (en utilisant Caliburn Micro atm). Je pense cependant que les choses sont moins simples dans ce monde. Faisons l'exercice à nouveau: le modèle a l' état, la vue montre la ViewModel et le ViewModel fait des choses à / avec le modèle (essentiellement), un ViewModel n'ont l' état! (à préciser, peut - être il délègue toutes les propriétés à un ou plusieurs modèles, mais cela signifie qu'il doit avoir une référence au modèle d' une façon ou d'une autre, ce qui est l' état en soi) Pour fairele ViewModel a des dépendances sur les services Web, le référentiel, le lot. Lorsque vous instanciez un ViewModel, vous vous souciez de fournir ces dépendances, mais aussi l'état. Et cela, mesdames et messieurs, m'agace sans fin.

Chaque fois que vous avez besoin d'instancier un à ProductDetailsViewModelpartir du ProductSearchViewModel(dont vous avez appelé le ProductSearchWebServicequi à son tour est revenu IEnumerable<ProductDTO>, tout le monde est toujours avec moi?), Vous pouvez faire l'une des choses suivantes:

  • appel new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);, c'est mauvais, imaginez 3 autres dépendances, cela signifie que vous ProductSearchViewModeldevez également assumer ces dépendances. Changer le constructeur est également douloureux.
  • appel _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);, l'usine n'est qu'un Func, ils sont facilement générés par la plupart des frameworks IoC. Je pense que c'est mauvais parce que les méthodes Init sont une abstraction qui fuit. Vous ne pouvez pas non plus utiliser le mot clé readonly pour les champs définis dans la méthode Init. Je suis sûr qu'il y a quelques autres raisons.
  • appelez _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);donc ... c'est le modèle (fabrique abstraite) qui est généralement recommandé pour ce type de problème. Je pensais que c'était génial car il satisfait mon envie de frappe statique, jusqu'à ce que je commence à l'utiliser. Je pense que la quantité de code passe-partout est excessive (vous savez, à part les noms de variables ridicules que j'utilise). Pour chaque ViewModel qui nécessite des paramètres d'exécution, vous obtiendrez deux fichiers supplémentaires (interface d'usine et implémentation), et vous devrez taper les dépendances non liées à l'exécution comme 4 fois supplémentaires. Et chaque fois que les dépendances changent, vous pouvez également les changer en usine. J'ai l'impression que je n'utilise même plus de conteneur DI. (Je pense que Castle Windsor a une sorte de solution pour cela [avec ses propres inconvénients, corrigez-moi si je me trompe]).
  • faire quelque chose avec des types anonymes ou un dictionnaire. J'aime ma saisie statique.

Donc voilà. Mélanger l'état et le comportement de cette manière crée un problème qui n'existe pas du tout dans MVC. Et j'ai l'impression qu'il n'y a actuellement pas de solution vraiment adéquate à ce problème. J'aimerais maintenant observer certaines choses:

  • Les gens utilisent réellement MVVM. Donc, soit ils ne se soucient pas de tout ce qui précède, soit ils ont une autre brillante solution.
  • Je n'ai pas trouvé d'exemple détaillé de MVVM avec WPF. Par exemple, l'exemple de projet NDDD m'a énormément aidé à comprendre certains concepts DDD. J'aimerais vraiment que quelqu'un me pointe vers quelque chose de similaire pour MVVM / WPF.
  • Peut-être que je fais mal MVVM et que je devrais renverser ma conception. Peut-être que je ne devrais pas avoir ce problème du tout. Eh bien, je sais que d'autres personnes ont posé la même question, donc je pense que je ne suis pas le seul.

Résumer

  • Ai-je raison de conclure que le fait que le ViewModel soit un point d'intégration pour l'état et le comportement est la raison de certaines difficultés avec le modèle MVVM dans son ensemble?
  • L'utilisation du modèle d'usine abstrait est-elle la seule / meilleure façon d'instancier un ViewModel de manière statique?
  • Existe-t-il quelque chose comme une implémentation de référence approfondie disponible?
  • Est-ce que le fait d'avoir beaucoup de ViewModels avec un état / comportement est une odeur de conception?
dvdvorle
la source
10
C'est trop long à lire, pensez à réviser, il y a beaucoup de choses non pertinentes là-dedans. Vous pourriez manquer de bonnes réponses parce que les gens ne prendront pas la peine de lire tout cela.
yannis
Vous avez dit que vous aimiez Caliburn.Micro, mais vous ne savez pas comment ce cadre peut aider à instancier de nouveaux modèles de vue? Vérifiez quelques exemples.
Euphoric
@Euphoric Pourriez-vous être un peu plus précis, Google ne semble pas m'aider ici. Vous avez des mots clés à rechercher?
dvdvorle
3
Je pense que vous simplifiez un peu MVC. Bien sûr, la vue montre le modèle au début, mais pendant le fonctionnement, il change d'état. Cet état changeant est, à mon avis, un "modèle d'édition". Autrement dit, une version aplatie du modèle avec des restrictions de cohérence réduites. En fait, ce que j'appelle un modèle d'édition est le MVVM ViewModel. Il détient l'état pendant la transition, qui était auparavant détenu par la vue dans MVC, ou repoussé dans une version non validée du modèle, où je ne pense pas qu'il appartient. Vous aviez donc un état "en mouvement" auparavant. Maintenant, tout est dans le ViewModel.
Scott Whitlock
@ScottWhitlock Je simplifie effectivement MVC. Mais je ne dis pas qu'il est faux que l'état "en flux" soit dans le ViewModel, je dis que le fait de bourrer le comportement là-dedans rend également plus difficile l'initialisation du ViewModel dans un état utilisable, disons, un autre ViewModel. Votre "Edit Model" dans MVC ne sait pas comment se sauvegarder (il n'a pas de méthode Save). Mais le contrôleur le sait et possède toutes les dépendances nécessaires pour le faire.
dvdvorle

Réponses:

2

Le problème des dépendances lors du lancement d'un nouveau modèle de vue peut être traité avec IOC.

public class MyCustomViewModel{
  private readonly IShoppingCartWebService _cartService;

  private readonly ITimeService _timeService;

  public ProductDTO ProductDTO { get; set; }

  public ProductDetailsViewModel(IShoppingCartWebService cartService, ITimeService timeService){
    _cartService = cartService;
    _timeService = timeService;
  }
}

Lors de la mise en place du conteneur ...

Container.Register<IShoppingCartWebService,ShoppingCartWebSerivce>().As.Singleton();
Container.Register<ITimeService,TimeService>().As.Singleton();
Container.Register<ProductDetailsViewModel>();

Lorsque vous avez besoin de votre modèle de vue:

var viewmodel = Container.Resolve<ProductDetailsViewModel>();
viewmodel.ProductDTO = myProductDTO;

Lors de l'utilisation d'un cadre tel que caliburn micro, il existe souvent une certaine forme de conteneur IOC déjà présent.

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);
Mike
la source
1

Je travaille quotidiennement avec ASP.NET MVC et travaille sur un WPF depuis plus d'un an et voici comment je le vois:

MVC

Le contrôleur est censé orchestrer des actions (récupérer ceci, ajouter cela).

La vue est responsable de l'affichage du modèle.

Le modèle englobe généralement les données (ex. UserId, FirstName) ainsi que l'état (ex. Titles) et est généralement spécifique à la vue.

MVVM

Le modèle ne contient généralement que des données (ex. UserId, FirstName) et est généralement transmis

Le modèle de vue englobe le comportement de la vue (méthodes), de ses données (modèle) et des interactions (commandes) - similaire au modèle MVP actif où le présentateur connaît le modèle. Le modèle de vue est spécifique à la vue (1 vue = 1 modèle de vue).

La vue est responsable de l'affichage des données et de la liaison de données au modèle de vue. Lorsqu'une vue est créée, son modèle de vue associé est généralement créé avec elle.


Ce que vous devez retenir, c'est que le modèle de présentation MVVM est spécifique à WPF / Silverlight en raison de leur nature de liaison de données.

La vue sait généralement à quel modèle de vue elle est associée (ou une abstraction de celle-ci).

Je vous conseille de traiter le modèle de vue comme un singleton, même s'il est instancié par vue. En d'autres termes, vous devriez pouvoir le créer via DI via un conteneur IOC et appeler des méthodes appropriées pour dire; charger son modèle en fonction des paramètres. Quelque chose comme ça:

public partial class EditUserView
{
    public EditUserView(IContainer container, int userId) : this() {
        var viewModel = container.Resolve<EditUserViewModel>();
        viewModel.LoadModel(userId);
        DataContext = viewModel;
    }
}

Par exemple, dans ce cas, vous ne créeriez pas de modèle de vue spécifique à l'utilisateur mis à jour - à la place, le modèle contiendrait des données spécifiques à l'utilisateur qui seraient chargées via un appel sur le modèle de vue.

Shelakel
la source
Si mon prénom est "Peter" et mes titres sont {"Rev", "Dr"} *, pourquoi considérez-vous les données FirstName et l'état du titre? Ou pouvez-vous clarifier votre exemple? * pas vraiment
Pete Kirkham
@PeteKirkham - l'exemple des «titres» auquel je faisais référence dans le contexte, disons, d'une zone de liste déroulante. Généralement, lorsque vous envoyez des informations à conserver, vous n'enverrez pas l'État (par exemple, une liste des États / provinces / titres) qui a été utilisé pour effectuer des sélections. Tout état utile à transférer avec les données (ex. Est le nom d'utilisateur utilisé) doit être vérifié au moment du traitement car l'état peut être devenu obsolète (si vous utilisiez un modèle asynchrone tel que la mise en file d'attente des messages).
Shelakel
Bien que deux ans se soient écoulés depuis ce post, je dois faire un commentaire en faveur des futurs téléspectateurs: deux choses m'ont dérangé avec votre réponse. Une vue peut correspondre à un ViewModel, mais un ViewModel peut être représenté par plusieurs vues. Deuxièmement, ce que vous décrivez est l'anti-modèle Service Locator. À mon humble avis, vous ne devez pas résoudre directement les modèles d'affichage partout. C'est à ça que sert la DI. Faites vos résolutions à moins de points que vous le pouvez. Laissez Caliburn faire ce travail pour vous, par exemple.
Jony Adamit
1

Réponse courte à vos questions:

  1. Oui State + Behavior conduit à ces problèmes, mais cela est vrai pour tous les OO. Le vrai coupable est le couplage de ViewModels qui est une sorte de violation de SRP.
  2. Saisie statique, probablement. Mais vous devez réduire / éliminer votre besoin d'instancier des ViewModels à partir d'autres ViewModels.
  3. Pas que je sache.
  4. Non, mais ayant des ViewModels avec un état et un comportement non liés (comme certaines références de modèle et certaines références de ViewModel)

La version longue:

Nous sommes confrontés au même problème et avons trouvé certaines choses qui pourraient vous aider. Bien que je ne connaisse pas la solution "magique", ces choses soulagent un peu la douleur.

  1. Implémentez des modèles pouvant être liés à partir de DTO pour le suivi et la validation des modifications. Ces "Data" -ViewModels ne doivent pas dépendre des services et ne proviennent pas du conteneur. Ils peuvent être simplement "nouveaux" édités, transmis et peuvent même dériver du DTO. L'essentiel est d'implémenter un modèle spécifique à votre application (comme MVC).

  2. Découplez vos ViewModels. Caliburn facilite le couplage des ViewModels. Il le suggère même à travers son modèle Screen / Conductor. Mais ce couplage rend les ViewModels difficiles à tester, crée beaucoup de dépendances et le plus important: impose la charge de la gestion du cycle de vie de ViewModel à vos ViewModels. Une façon de les découpler est d'utiliser quelque chose comme un service de navigation ou un contrôleur ViewModel. Par exemple

    interface publique IShowViewModels {void Show (object inlineArgumentsAsAnonymousType, string regionId); }

Mieux encore, cela se fait par une forme de messagerie. Mais l'important n'est pas de gérer le cycle de vie de ViewModel à partir d'autres ViewModels. Dans MVC, les contrôleurs ne dépendent pas les uns des autres, et dans MVVM ViewModels ne doivent pas dépendre les uns des autres. Intégrez-les par d'autres moyens.

  1. Utilisez vos conteneurs avec des fonctionnalités dynamiques / de type "stringly". Bien qu'il puisse être possible de créer quelque chose comme INeedData<T1,T2,...>et d'appliquer des paramètres de création de type sécurisé, cela n'en vaut pas la peine. La création d'usines pour chaque type de ViewModel n'en vaut pas la peine. La plupart des conteneurs IoC fournissent des solutions à cela. Vous obtiendrez des erreurs lors de l'exécution, mais le découplage et la testabilité de l'unité en valent la peine. Vous faites toujours une sorte de test d'intégration et ces erreurs sont facilement repérées.
sanosdole
la source
0

La façon dont je le fais habituellement (en utilisant PRISM), c'est que chaque assemblage contient un module d'initialisation de conteneur, où toutes les interfaces, les instances sont enregistrées au démarrage.

private void RegisterResources()
{
    Container.RegisterType<IDataService, DataService>();
    Container.RegisterType<IProductSearchViewModel, ProductSearchViewModel>();
    Container.RegisterType<IProductDetailsViewModel, ProductDetailsViewModel>();
}

Et étant donné vos exemples de classes, serait implémenté comme ceci, avec le conteneur passé tout le long. De cette façon, toutes les nouvelles dépendances peuvent être ajoutées facilement car vous avez déjà accès au conteneur.

/// <summary>
/// IDataService Interface
/// </summary>
public interface IDataService
{
    DataTable GetSomeData();
}

public class DataService : IDataService
{
    public DataTable GetSomeData()
    {
        MessageBox.Show("This is a call to the GetSomeData() method.");

        var someData = new DataTable("SomeData");
        return someData;
    }
}

public interface IProductSearchViewModel
{
}

public class ProductSearchViewModel : IProductSearchViewModel
{
    private readonly IUnityContainer _container;

    /// <summary>
    /// This will get resolved if it's been added to the container.
    /// Or alternately you could use constructor resolution. 
    /// </summary>
    [Dependency]
    public IDataService DataService { get; set; }

    public ProductSearchViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void SearchAndDisplay()
    {
        DataTable results = DataService.GetSomeData();

        var detailsViewModel = _container.Resolve<IProductDetailsViewModel>();
        detailsViewModel.DisplaySomeDataInView(results);

        // Create the view, usually resolve using region manager etc.
        var detailsView = new DetailsView() { DataContext = detailsViewModel };
    }
}

public interface IProductDetailsViewModel
{
    void DisplaySomeDataInView(DataTable dataTable);
}

public class ProductDetailsViewModel : IProductDetailsViewModel
{
    private readonly IUnityContainer _container;

    public ProductDetailsViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void DisplaySomeDataInView(DataTable dataTable)
    {
    }
}

Il est assez courant d'avoir une classe ViewModelBase, dont tous vos modèles de vue sont dérivés, qui contient une référence au conteneur. Tant que vous prenez l'habitude de résoudre tous les modèles de vue au lieu d' new()'ingeux, cela devrait rendre la résolution des dépendances beaucoup plus simple.

Martin Cooper
la source
0

Parfois, il est bon d'aller à la définition la plus simple plutôt qu'à un exemple complet: http://en.wikipedia.org/wiki/Model_View_ViewModel peut-être que la lecture de l'exemple Java ZK est plus éclairante que celle de C #.

D'autres fois, écoutez votre instinct ...

Est-ce que le fait d'avoir beaucoup de ViewModels avec un état / comportement est une odeur de conception?

Vos modèles sont-ils des mappages objet par table? Peut-être qu'un ORM aiderait à mapper aux objets de domaine tout en gérant l'entreprise ou en mettant à jour plusieurs tables.

Gerry King
la source