Qui doit contrôler la navigation dans une application MVVM?

33

Exemple # 1: J'ai une vue affichée dans mon application MVVM (utilisons Silverlight pour les besoins de la discussion) et je clique sur un bouton qui devrait m'amener à une nouvelle page.

Exemple # 2: Cette même vue possède un autre bouton qui, lorsque vous cliquez dessus, devrait ouvrir une vue détaillée dans une fenêtre enfant (boîte de dialogue).

Nous savons qu'il y aura des objets Command exposés par notre ViewModel lié aux boutons avec des méthodes qui répondent au clic de l'utilisateur. Mais alors quoi? Comment terminons-nous l'action? Même si nous utilisons un soi-disant NavigationService, que disons-nous?

Pour être plus précis, dans un modèle View-first traditionnel (comme les schémas de navigation basés sur des URL comme sur le Web ou le cadre de navigation intégré SL), les objets Command devraient savoir quelle vue afficher ensuite. Cela semble franchir la ligne quand il s'agit de la séparation des préoccupations promues par le modèle.

D'un autre côté, si le bouton n'était pas connecté à un objet Command et se comportait comme un lien hypertexte, les règles de navigation pouvaient être définies dans le balisage. Mais voulons-nous que les vues contrôlent le flux des applications et la navigation n'est-elle pas simplement un autre type de logique métier? (Je peux dire oui dans certains cas et non dans d'autres.)

Pour moi, l'implémentation utopique du modèle MVVM (et j'en ai entendu d'autres professer cela) consisterait à câbler le ViewModel de manière à ce que l'application puisse fonctionner sans tête (c'est-à-dire sans vues). Cela fournit la plus grande surface pour les tests basés sur le code et fait des vues un véritable habillage sur l'application. Et mon ViewModel ne devrait pas se soucier s'il s'affiche dans la fenêtre principale, un panneau flottant ou une fenêtre enfant, n'est-ce pas?

Selon cette approbation, il appartient à un autre mécanisme au moment de l'exécution de «lier» quelle vue doit être affichée pour chaque ViewModel. Mais que se passe-t-il si nous voulons partager une vue avec plusieurs ViewModels ou vice versa?

Donc, étant donné la nécessité de gérer la relation View-ViewModel afin que nous sachions quoi afficher quand ainsi que la nécessité de naviguer entre les vues, y compris l'affichage des fenêtres / boîtes de dialogue enfants, comment pouvons-nous vraiment accomplir cela dans le modèle MVVM?

SonOfPirate
la source

Réponses:

21

La navigation doit toujours être gérée dans le ViewModel.

Vous êtes sur la bonne voie en pensant que la mise en œuvre parfaite du modèle de conception MVVM signifierait que vous pourriez exécuter votre application entièrement sans vues, et vous ne pouvez pas le faire si vos vues contrôlent votre navigation.

J'ai généralement un ApplicationViewModelou ShellViewModelqui gère l'état général de ma demande. Cela inclut le CurrentPage(qui est un ViewModel) et le code de manipulation ChangePageEvents. (Il est également souvent utilisé pour d'autres objets à l'échelle de l'application tels que CurrentUser ou ErrorMessages)

Donc, si un ViewModel, n'importe où, diffuse un ChangePageEvent(new SomePageViewModel), le ShellViewModelrécupérera ce message et basculera sur la CurrentPagepage spécifiée dans le message.

J'ai en fait écrit un article de blog sur la navigation avec MVVM si cela vous intéresse

Rachel
la source
2
Approche intéressante. Quatre commentaires: 1) Silverlight ne prend pas en charge la propriété DataType sur DataTemplate, donc le mappage du DataTemplate au ViewModel n'est pas possible dans SL. 2) Cela n'aborde pas les possibilités de plusieurs à plusieurs entre les vues et les ViewModels. 3) Il ne gère pas les fenêtres enfants (ou du moins je ne vois pas comment). 4) Il nécessite un couplage étroit entre votre ViewModel Application / Shell et ses enfants (petits-enfants, etc.) Si j'ai 40 pages dans mon application, ce ViewModel serait un moyen trop lourd à gérer.
SonOfPirate
Cela dit, c'est certainement quelque chose à considérer.
SonOfPirate
@SonOfPirate 1) Silverlight ne prend pas (encore) en charge le mappage implicite de DataTemplate, mais il prend en charge a DataTemplateSelector, ce que j'utilise habituellement pour les applications Silverlight. 2) J'ai déjà utilisé cela dans de nombreuses situations. Par exemple, un ViewModel peut avoir plusieurs vues, ou une vue peut être associée à plusieurs ViewModels. Pour un exemple de la première, voir rachel53461.wordpress.com/2011/05/28/… . Pour les versions ultérieures, il vous suffit de spécifier que plusieurs ViewModels mappent vers la même vue dans votre DataTemplateSelector.
Rachel
3) Ce n'est qu'un exemple de base. Si vous saviez que votre application allait être à plusieurs fenêtres, vous modifieriez évidemment le ShellViewModel pour en gérer plusieurs CurrentPages4) Encore une fois, ce n'était qu'un exemple de base. En réalité, mes PageViewModels sont tous basés sur une classe de base, donc mon ShellViewModel ne fonctionne qu'avec la classe ou l'interface de base, comme IPageViewModel. Vraiment le plus gros morceau de mappage désordonné est le DataTemplateSelector qui devrait mapper 40 vues à 40 ViewModels.
Rachel
@SonOfPirate J'espère que cela a répondu à certaines de vos questions. N'hésitez pas à me rechercher dans le chat si vous en avez d'autres :)
Rachel
6

Dans un souci de clôture, j'ai pensé publier la direction que j'ai finalement choisie pour résoudre ce problème.

La première décision a été de tirer parti du cadre de navigation de page Silverlight fourni dès le départ. Cette décision était basée sur plusieurs facteurs, notamment la connaissance du fait que ce type de navigation est repris par Microsoft dans les applications Windows 8 Metro et est compatible avec la navigation dans les applications Phone 7.

Pour le faire fonctionner, j'ai ensuite examiné le travail effectué par ASP.NET MVC avec la navigation basée sur les conventions. Le contrôle Frame utilise des URI pour localiser la «page» à afficher. La similitude a permis d'utiliser une approche similaire basée sur des conventions dans l'application Silverlight. L'astuce consistait à faire fonctionner tout cela de manière MVVM.

La solution est le NavigationService. Ce service expose plusieurs méthodes, telles que NavigateTo et Back, que ViewModels peut utiliser pour lancer un changement de page. Lorsqu'une nouvelle page est demandée, le NavigationService envoie un CurrentPageChangedMessage à l'aide de la fonction MVVMLight Messenger.

La vue qui contient le contrôle Frame a son propre ViewModel défini comme DataContext qui écoute ce message. Une fois reçu, le nom de la nouvelle vue est placé dans une fonction de mappage qui applique nos règles de convention et défini sur la propriété CurrentPage. La propriété Source du contrôle Frame est liée à la propriété CurrentPage. Par conséquent, la définition de la propriété met à jour la source et déclenche la navigation.

Revenons au NavigationService. La méthode NavigateTo accepte le nom de la page cible. Pour vous assurer que les ViewModels n'ont aucun problème d'interface utilisateur, le nom utilisé est le nom du ViewModel à afficher. J'ai en fait créé une énumération qui a un champ pour chaque ViewModel navigable comme aide et pour éliminer les chaînes magiques partout dans l'application. La fonction de mappage que j'ai mentionnée ci-dessus supprimera le suffixe "ViewModel" du nom, ajoutera "Page" au nom et définira le nom complet sur "Vues {Nom} Page.xaml".

Ainsi, par exemple, pour accéder à la vue des détails du client, je peux appeler:

NavigationService.NavigateTo(ViewModels.CustomerDetails);

La valeur de CustomerDetails est "CustomerDetailsViewModel" qui est mappée sur "Views \ CustomerDetailsPage.xaml".

La beauté de cette approche est que l'interface utilisateur est complètement découplée des ViewModels mais que nous avons une prise en charge complète de la navigation. Cependant, je peux maintenant refaire ma demande et à chaque fois que je le juge bon sans aucun changement de code.

J'espère que l'explication aide.

SonOfPirate
la source
2

Semblable à ce que Rachel a dit, mon application principalement MVVM a Presenterpour gérer les commutateurs entre les fenêtres ou les pages. Rachel appelle cela un ApplicationViewModel, mais d'après mon expérience, cela doit généralement faire plus que simplement être une cible contraignante (comme recevoir des messages, créer Windows, etc.) donc c'est techniquement plus comme un traditionnel Presenterou Controller.

Dans ma candidature, mon Presentercommence par un CurrentViewModel. Le Presenterintercepte toutes les communications entre le Viewet le ViewModel. L'une des choses que vous ViewModelpouvez faire pendant une interaction est de renvoyer une nouvelle ViewModel, ce qui signifie qu'une nouvelle page ou une nouvelle Windowdoit être affichée. Le Presenterprend soin de créer ou d'écraser le Viewpour le nouveau ViewModelet de le paramétrer DataContext.

Le résultat d'une action peut également être que a ViewModelest "terminé", auquel cas le Presenterdétecte et ferme la fenêtre, ou la fait ViewModeldisparaître de la pile VM et revient à l'affichage de la page précédente.

Scott Whitlock
la source
Comment le présentateur sait-il quelle vue afficher?
SonOfPirate
1
@SonOfPirate - Cela se fait normalement via les mécanismes WPF. Le Presentercolle simplement le retourné ViewModeldans l'arborescence visuelle, et WPF saisit le approprié View, raccorde le DataContextet le place dans l'arborescence visuelle à la place. Vous pouvez le faire en utilisant des DataTemplates qui déclarent quel ViewModeltype ils rendent, ou vous pouvez créer un sélecteur de modèle de données personnalisé. Cela fait toujours partie des fonctionnalités de WPF.
Scott Whitlock