Gestion des boîtes de dialogue dans WPF avec MVVM

235

Dans le modèle MVVM pour WPF, la gestion des boîtes de dialogue est l'une des opérations les plus complexes. Comme votre modèle de vue ne sait rien de la vue, la communication par dialogue peut être intéressante. Je peux exposer ICommandque lorsque la vue l'invoque, une boîte de dialogue peut apparaître.

Quelqu'un connaît-il un bon moyen de gérer les résultats des dialogues? Je parle de boîtes de dialogue Windows telles que MessageBox.

L'une des façons dont nous l'avons fait était d'avoir un événement sur le modèle de vue auquel la vue s'abonnerait lorsqu'une boîte de dialogue était requise.

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

C'est correct, mais cela signifie que la vue nécessite du code, ce que je voudrais éviter.

Ray Booysen
la source
Pourquoi ne pas vous lier à un objet d'assistance dans la vue?
Paul Williams
1
Pas sûr de ce que vous voulez dire.
Ray Booysen
1
Si je comprends la question, vous ne voulez pas que les boîtes de dialogue de la machine virtuelle apparaissent et vous ne voulez pas de code-behind dans la vue. De plus, il semble que vous préfériez les commandes aux événements. Je suis d'accord avec tout cela, donc j'utilise une classe d'assistance dans la vue qui expose une commande pour gérer la boîte de dialogue. J'ai répondu à cette question sur un autre fil ici: stackoverflow.com/a/23303267/420400 . Cependant, la dernière phrase donne l'impression que vous ne voulez pas de code du tout, n'importe où dans la vue. Je comprends cette préoccupation, mais le code en question n'est qu'un conditionnel et il est peu probable qu'il change.
Paul Williams
4
Le modèle de vue devrait toujours être responsable de la logique derrière la création de la boîte de dialogue, c'est tout le motif de son existence en premier lieu. Cela dit, cela ne fait pas (et ne devrait pas) faire le soulèvement de la création de la vue elle-même. J'ai écrit un article à ce sujet sur codeproject.com/Articles/820324/… où je montre que le cycle de vie complet des boîtes de dialogue peut être géré via une liaison de données WPF régulière et sans rompre le modèle MVVM.
Mark Feldman

Réponses:

131

Je suggère de renoncer aux dialogues modaux des années 1990 et d'implémenter un contrôle en superposition (canevas + positionnement absolu) avec une visibilité liée à un retour booléen dans la machine virtuelle. Plus proche d'un contrôle de type ajax.

C'est très utile:

<BooleanToVisibilityConverter x:Key="booltoVis" />

un péché:

<my:ErrorControl Visibility="{Binding Path=ThereWasAnError, Mode=TwoWay, Converter={StaticResource booltoVis}, UpdateSourceTrigger=PropertyChanged}"/>

Voici comment j'en ai implémenté un en tant que contrôle utilisateur. Cliquer sur le «x» ferme le contrôle dans une ligne de code dans le code de l'utilisateur derrière. (Étant donné que j'ai mes vues dans un fichier .exe et ViewModels dans une DLL, je ne me sens pas mal à propos du code qui manipule l'interface utilisateur.)

Boîte de dialogue Wpf

Jeffrey Knight
la source
20
Ouais, j'aime aussi cette idée, mais j'aimerais voir un exemple de ce contrôle en termes de façon de l'afficher et d'en récupérer le résultat, etc. Particulièrement dans le scénario MVVM dans Silverlight.
Roboblob
16
Comment empêchez-vous l'utilisateur d'interagir avec les contrôles sous cette superposition de boîte de dialogue?
Andrew Garrison
17
Le problème avec cette approche est que vous ne pouvez pas ouvrir une deuxième boîte de dialogue modale à partir de la première, du moins pas sans de lourdes modifications au système de superposition ...
Thomas Levesque
6
Un autre problème avec cette approche est que la "boîte de dialogue" ne peut pas être déplacée. Dans nos applications, nous devons avoir une boîte de dialogue mobile pour que l'utilisateur puisse voir ce qu'il y a derrière.
JAB
13
Cette approche me semble terrible. Qu'est-ce que je rate? En quoi est-ce mieux qu'une vraie boîte de dialogue?
Jonathan Wood
51

Vous devez utiliser un médiateur pour cela. Mediator est un modèle de conception commun également connu sous le nom de Messenger dans certaines de ses implémentations. C'est un paradigme de type Register / Notify et permet à votre ViewModel et à vos vues de communiquer via un mécanisme de messagerie à faible couplage.

Vous devriez consulter le groupe Google WPF Disciples et rechercher simplement Mediator. Vous serez très satisfait des réponses ...

Vous pouvez cependant commencer par ceci:

http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/

Prendre plaisir !

Edit: vous pouvez voir la réponse à ce problème avec le MVVM Light Toolkit ici:

http://mvvmlight.codeplex.com/Thread/View.aspx?ThreadId=209338

Roubachof
la source
2
Marlon grech vient de publier une toute nouvelle implémentation du médiateur: marlongrech.wordpress.com/2009/04/16/…
Roubachof
21
Juste une remarque: le modèle Mediator n'a pas été introduit par les Disciples WPF, c'est un modèle GoF classique ... ( dofactory.com/Patterns/PatternMediator.aspx ). Belle réponse sinon;)
Thomas Levesque
10
S'il vous plaît mon dieu, n'utilisez pas de médiateur ou de messager sacré. Ce type de code avec des dizaines de messages qui circulent devient très difficile à déboguer à moins que vous ne vous souveniez en quelque sorte de tous les nombreux points de votre base de code entière qui s'abonnent et gèrent chaque événement. Cela devient un cauchemar pour les nouveaux développeurs. En fait, je considère que la bibliothèque MvvMLight entière est un anti-modèle massif pour son utilisation omniprésente et inutile de la messagerie asynchrone. La solution est simple: appelez un service de dialogue distinct (par exemple, IDialogService) de votre conception. L'interface possède des méthodes et des événements pour les rappels.
Chris Bordeman
34

Une bonne boîte de dialogue MVVM devrait:

  1. Être déclaré avec seulement XAML.
  2. Obtenez tout son comportement de la liaison de données.

Malheureusement, WPF ne fournit pas ces fonctionnalités. L'affichage d'une boîte de dialogue nécessite un appel codé à ShowDialog(). La classe Window, qui prend en charge les boîtes de dialogue, ne peut pas être déclarée en XAML, elle ne peut donc pas être facilement liée aux données du DataContext.

Pour résoudre ce problème, j'ai écrit un contrôle de stub XAML qui se trouve dans l'arborescence logique et relie la liaison de données à un Windowet gère l'affichage et le masquage de la boîte de dialogue. Vous pouvez le trouver ici: http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

C'est vraiment simple à utiliser et ne nécessite aucune modification étrange de votre ViewModel et ne nécessite pas d'événements ou de messages. L'appel de base ressemble à ceci:

<dialog:Dialog Content="{Binding Path=DialogViewModel}" Showing="True" />

Vous voudrez probablement ajouter un style qui définit Showing. Je l'explique dans mon article. J'espère que ceci vous aide.

user92541
la source
2
C'est une approche vraiment intéressante du problème de l'affichage des fenêtres de dialogue dans MVVM.
dthrasher
2
"Showing a dialog requires a code-behind"mmm vous pouvez appeler ça dans ViewModel
Brock Hensley
J'ajouterais le point 3 - vous êtes libre de vous lier à d'autres objets dans la vue. Laisser le code de la boîte de dialogue vide signifie qu'il n'y a pas de code C # n'importe où dans la vue, et la liaison de données n'implique pas la liaison à la machine virtuelle.
Paul Williams
25

J'utilise cette approche pour les dialogues avec MVVM.

Il ne me reste plus qu'à appeler ce qui suit à partir de mon modèle de vue.

var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);
aveugle
la source
de quelle bibliothèque vient uiDialogService?
aggietech
1
pas de bibliothèque. n'est qu'une petite interface et implémentation: stackoverflow.com/questions/3801681/… . pour être juste atm, il y a plus de surcharges pour mes besoins :) (hauteur, largeur, propriétés et ainsi de suite)
blindmeis
16

Ma solution actuelle résout la plupart des problèmes que vous avez mentionnés, mais elle est complètement abstraite des éléments spécifiques à la plate-forme et peut être réutilisée. De plus, je n'ai utilisé aucune liaison de code uniquement avec des délégués qui implémentent ICommand. La boîte de dialogue est essentiellement une vue - un contrôle distinct qui a son propre ViewModel et il est affiché à partir du ViewModel de l'écran principal mais déclenché à partir de l'interface utilisateur via la liaison DelagateCommand.

Voir la solution complète de Silverlight 4 ici Dialogues modaux avec MVVM et Silverlight 4

Roboblob
la source
Tout comme l'approche de @Elad Katz, votre réponse n'a pas le contenu lié - veuillez améliorer votre réponse en l'insérant car c'est ce qui est considéré comme une bonne réponse ici sur SO. Néanmoins, merci pour votre contribution! :)
Yoda
6

J'ai vraiment eu du mal avec ce concept pendant un certain temps lors de l'apprentissage (toujours en cours) de MVVM. Ce que j'ai décidé, et ce que je pense que d'autres ont déjà décidé, mais qui n'était pas clair pour moi, c'est ceci:

Ma pensée originale était qu'un ViewModel ne devrait pas être autorisé à appeler une boîte de dialogue directement car il n'a pas à décider comment une boîte de dialogue devrait apparaître. À cause de cela, j'ai commencé à réfléchir à la façon dont je pouvais passer des messages comme je le ferais dans MVP (c'est-à-dire View.ShowSaveFileDialog ()). Cependant, je pense que c'est la mauvaise approche.

Il est normal qu'un ViewModel appelle directement une boîte de dialogue. Cependant, lorsque vous testez un ViewModel, cela signifie que la boîte de dialogue apparaîtra pendant votre test, ou échouera tous ensemble (jamais vraiment essayé cela).

Donc, ce qui doit se produire, c'est que pendant le test, vous utilisez une version "test" de votre boîte de dialogue. Cela signifie que pour chaque boîte de dialogue que vous avez, vous devez créer une interface et simuler la réponse de la boîte de dialogue ou créer une maquette de test qui aura un comportement par défaut.

Vous devez déjà utiliser une sorte de localisateur de service ou IoC que vous pouvez configurer pour vous fournir la version correcte en fonction du contexte.

En utilisant cette approche, votre ViewModel est toujours testable et selon la façon dont vous modifiez vos boîtes de dialogue, vous pouvez contrôler le comportement.

J'espère que cela t'aides.

Mike Rowley
la source
6

Il existe deux bonnes façons de procéder: 1) un service de dialogue (facile, propre) et 2) une vue assistée. L'aide assistée fournit quelques fonctionnalités intéressantes, mais cela n'en vaut généralement pas la peine.

SERVICE DE DIALOGUE

a) une interface de service de dialogue comme via un constructeur ou un conteneur de dépendance:

interface IDialogService { Task ShowDialogAsync(DialogViewModel dlgVm); }

b) Votre implémentation de IDialogService devrait ouvrir une fenêtre (ou injecter un certain contrôle dans la fenêtre active), créer une vue correspondant au nom du type dlgVm donné (utiliser l'enregistrement ou la convention de conteneur ou un ContentPresenter avec le type DataTemplates associé). ShowDialogAsync doit créer un TaskCompletionSource et renvoyer sa propriété .Task. La classe DialogViewModel elle-même a besoin d'un événement que vous pouvez invoquer dans la classe dérivée lorsque vous souhaitez fermer et regarder dans la vue de dialogue pour fermer / masquer réellement la boîte de dialogue et terminer la TaskCompletionSource.

b) Pour l'utiliser, appelez simplement wait this.DialogService.ShowDialog (myDlgVm) sur votre instance d'une classe dérivée de DialogViewModel. Après avoir attendu les retours, regardez les propriétés que vous avez ajoutées sur votre machine virtuelle de dialogue pour déterminer ce qui s'est passé; vous n'avez même pas besoin d'un rappel.

VOIR ASSISTÉ

Cela vous permet d'écouter un événement sur le modèle d'affichage. Tout cela pourrait être enveloppé dans un comportement Blend pour éviter le code et l'utilisation des ressources si vous êtes si enclin (FMI, sous-classe la classe "Behavior" pour voir une sorte de propriété attachée Blendable sur les stéroïdes). Pour l'instant, nous allons le faire manuellement sur chaque vue:

a) Créez un OpenXXXXXDialogEvent avec une charge utile personnalisée (une classe dérivée de DialogViewModel).

b) Demandez à la vue de s'abonner à l'événement dans son événement OnDataContextChanged. Assurez-vous de masquer et de vous désabonner si l'ancienne valeur! = Null et dans l'événement Unloaded de la fenêtre.

c) Lorsque l'événement se déclenche, demandez à la vue d'ouvrir votre vue, qui peut se trouver dans une ressource sur votre page, ou vous pouvez la localiser par convention ailleurs (comme dans l'approche du service de dialogue).

Cette approche est plus flexible, mais nécessite plus de travail à utiliser. Je ne l'utilise pas beaucoup. Le seul avantage est la possibilité de placer la vue physiquement à l'intérieur d'un onglet, par exemple. J'ai utilisé un algorithme pour le placer dans les limites du contrôle utilisateur actuel, ou s'il n'est pas assez grand, parcourir l'arborescence visuelle jusqu'à ce qu'un conteneur assez grand soit trouvé.

Cela permet aux boîtes de dialogue d'être proches de l'endroit où elles sont réellement utilisées, atténuer uniquement la partie de l'application liée à l'activité en cours, et permettre à l'utilisateur de se déplacer dans l'application sans avoir à repousser manuellement les boîtes de dialogue, même en ayant plusieurs quasi- les boîtes de dialogue modales s'ouvrent sur différents onglets ou sous-vues.

Chris Bordeman
la source
Un service de dialogue est beaucoup plus facile, certainement, et ce que je fais habituellement. Il permet également de fermer facilement la boîte de dialogue de la vue à partir du modèle de vue parent, ce qui est nécessaire lorsque le modèle de vue parent se ferme ou s'annule.
Chris Bordeman
4

Utilisez une commande figable

<Grid>
        <Grid.DataContext>
            <WpfApplication1:ViewModel />
        </Grid.DataContext>


        <Button Content="Text">
            <Button.Command>
                <WpfApplication1:MessageBoxCommand YesCommand="{Binding MyViewModelCommand}" />
            </Button.Command>
        </Button>

</Grid>
public class MessageBoxCommand : Freezable, ICommand
{
    public static readonly DependencyProperty YesCommandProperty = DependencyProperty.Register(
        "YesCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty OKCommandProperty = DependencyProperty.Register(
        "OKCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty CancelCommandProperty = DependencyProperty.Register(
        "CancelCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty NoCommandProperty = DependencyProperty.Register(
        "NoCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
        "Message",
        typeof (string),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata("")
        );

    public static readonly DependencyProperty MessageBoxButtonsProperty = DependencyProperty.Register(
        "MessageBoxButtons",
        typeof(MessageBoxButton),
        typeof(MessageBoxCommand),
        new FrameworkPropertyMetadata(MessageBoxButton.OKCancel)
        );

    public ICommand YesCommand
    {
        get { return (ICommand) GetValue(YesCommandProperty); }
        set { SetValue(YesCommandProperty, value); }
    }

    public ICommand OKCommand
    {
        get { return (ICommand) GetValue(OKCommandProperty); }
        set { SetValue(OKCommandProperty, value); }
    }

    public ICommand CancelCommand
    {
        get { return (ICommand) GetValue(CancelCommandProperty); }
        set { SetValue(CancelCommandProperty, value); }
    }

    public ICommand NoCommand
    {
        get { return (ICommand) GetValue(NoCommandProperty); }
        set { SetValue(NoCommandProperty, value); }
    }

    public MessageBoxButton MessageBoxButtons
    {
        get { return (MessageBoxButton)GetValue(MessageBoxButtonsProperty); }
        set { SetValue(MessageBoxButtonsProperty, value); }
    }

    public string Message
    {
        get { return (string) GetValue(MessageProperty); }
        set { SetValue(MessageProperty, value); }
    }

    public void Execute(object parameter)
    {
        var messageBoxResult = MessageBox.Show(Message);
        switch (messageBoxResult)
        {
            case MessageBoxResult.OK:
                OKCommand.Execute(null);
                break;
            case MessageBoxResult.Yes:
                YesCommand.Execute(null);
                break;
            case MessageBoxResult.No:
                NoCommand.Execute(null);
                break;
            case MessageBoxResult.Cancel:
                if (CancelCommand != null) CancelCommand.Execute(null); //Cancel usually means do nothing ,so can be null
                break;

        }
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;


    protected override Freezable CreateInstanceCore()
    {
        throw new NotImplementedException();
    }
}
Maxm007
la source
Ce code a besoin d'un peu de travail, mais c'est de loin la meilleure idée, en particulier pour les boîtes de dialogue système telles que les boîtes de dialogue de fichier ou d'imprimante. Les boîtes de dialogue appartiennent à View si quelque chose le fait. Pour les boîtes de dialogue de fichiers, le résultat (nom de fichier sélectionné) peut être transmis à la commande interne comme paramètre.
Anton Tykhyy
3

Je pense que la gestion d'une boîte de dialogue devrait être la responsabilité de la vue, et la vue doit avoir du code pour le supporter.

Si vous modifiez l'interaction ViewModel - View pour gérer les boîtes de dialogue, le ViewModel dépend de cette implémentation. Le moyen le plus simple de résoudre ce problème consiste à confier à la vue la responsabilité d'effectuer la tâche. Si cela signifie afficher une boîte de dialogue, c'est bien, mais cela peut également être un message d'état dans la barre d'état, etc.

Mon point est que tout le point du modèle MVVM est de séparer la logique métier de l'interface graphique, donc vous ne devriez pas mélanger la logique GUI (pour afficher une boîte de dialogue) dans la couche métier (le ViewModel).

Cameron MacFarland
la source
2
La machine virtuelle ne gérerait jamais la boîte de dialogue, dans mon exemple, elle aurait simplement un événement qui nécessiterait que la boîte de dialogue se déclenche et renvoie des informations sous une forme d'EventArgs. Si la vue est responsable, comment transmet-elle les informations à la machine virtuelle?
Ray Booysen
Supposons que la machine virtuelle doit supprimer quelque chose. La machine virtuelle appelle une méthode sur la vue Supprimer qui renvoie un booléen. La vue peut alors soit supprimer directement l'élément et renvoyer vrai, soit afficher une boîte de dialogue de confirmation et retourner vrai / faux selon la réponse des utilisateurs.
Cameron MacFarland
La machine virtuelle ne sait rien de la boîte de dialogue, mais a uniquement demandé à la vue de supprimer quelque chose, ce que la vue a confirmé ou refusé.
Cameron MacFarland
J'ai toujours pensé que le point de MVVM était le modèle: logique métier, ViewModel: logique GUI et View: pas de logique. Ce qui est en quelque sorte contredit par votre dernier paragraphe. S'il vous plaît, expliquez!
David Schmitt
2
Il faut d'abord déterminer si la demande de confirmation de pré-suppression est une logique métier ou une logique d'affichage. S'il s'agit d'une logique métier, la méthode DeleteFile du modèle ne doit pas le faire, mais renvoie plutôt l'objet de question de confirmation. Cela comprendra une référence au délégué qui effectue la suppression réelle. S'il ne s'agit pas d'une logique métier, la machine virtuelle doit créer une machine virtuelle de la question dans DeleteFileCommand, avec deux membres ICommand. Un pour oui et un pour non. Il y a probablement des arguments pour les deux vues, et dans RL la plupart des utilisations rencontreront probablement les deux.
Guge
3

Une alternative intéressante consiste à utiliser des contrôleurs chargés d'afficher les vues (boîtes de dialogue).

Comment cela fonctionne est montré par le cadre d'application WPF (WAF) .

jbe
la source
3

Pourquoi ne pas simplement déclencher un événement dans la machine virtuelle et vous abonner à l'événement dans la vue? Cela garderait la logique d'application et la vue séparées et vous permettrait toujours d'utiliser une fenêtre enfant pour les boîtes de dialogue.

Eric Grover
la source
3

J'ai implémenté un comportement qui écoute un message du ViewModel. Il est basé sur la solution Laurent Bugnion, mais comme il n'utilise pas de code derrière et est plus réutilisable, je pense que c'est plus élégant.

Comment faire en sorte que WPF se comporte comme si MVVM était pris en charge immédiatement

Elad Katz
la source
1
Vous devez inclure le code complet ici car c'est ce que SO exige pour de bonnes réponses. Néanmoins, l'approche liée est assez soignée, alors merci! :)
Yoda
2
@yoda le code complet est assez long, et c'est pourquoi je préfère le lier. J'ai modifié ma réponse pour refléter les changements et pointer vers un lien qui n'est pas rompu
Elad Katz
Merci pour l'amélioration. Néanmoins, il est préférable de fournir des parchemins pleine page de code 3 ici sur SO plutôt qu'un lien qui pourrait être hors ligne un jour. Les bons articles pour des sujets complexes sont toujours assez longs - et je ne vois aucun avantage à ouvrir un nouvel onglet, à y basculer et à y faire défiler en faisant défiler la même page / l'onglet que je consultais auparavant. ;)
Yoda
@EladKatz J'ai vu que vous avez partagé une partie de votre implémentation WPF dans le lien que vous avez fourni. Avez-vous une solution pour ouvrir une nouvelle fenêtre à partir de ViewModel? Fondamentalement, j'ai deux formulaires et chacun a un ViewModel. Un utilisateur clique sur un bouton, un autre formulaire apparaît et viewmodel1 envoie son objet à viewmodel2. Dans le formulaire 2, l'utilisateur peut modifier l'objet et lorsqu'il ferme la fenêtre, l'objet mis à jour sera renvoyé au premier ViewModel. Avez-vous une solution à cela?
Ehsan
2

Je pense que la vue pourrait avoir du code pour gérer l'événement à partir du modèle de vue.

Selon l'événement / scénario, il peut également avoir un déclencheur d'événement qui s'abonne pour afficher les événements du modèle et une ou plusieurs actions à invoquer en réponse.

Nikhil Kothari
la source
1

Karl Shifflett a créé un exemple d'application pour afficher les boîtes de dialogue en utilisant l'approche service et l'approche Prism InteractionRequest.

J'aime l'approche de service - Elle est moins flexible, donc les utilisateurs sont moins susceptibles de casser quelque chose :) Elle est également cohérente avec la partie WinForms de mon application (MessageBox.Show) Mais si vous prévoyez d'afficher beaucoup de dialogues différents, alors InteractionRequest est un meilleure façon d'aller.

http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/

surfen
la source
1

Je sais que c'est une vieille question, mais quand j'ai fait cette recherche, je trouve beaucoup de questions connexes, mais je n'ai pas trouvé de réponse vraiment claire. Je fais donc ma propre implémentation d'une boîte de dialogue / boîte de message / popin, et je la partage!
Je pense que c'est "MVVM proof", et j'essaie de le rendre simple et approprié, mais je suis nouveau sur WPF, alors n'hésitez pas à commenter, ou même à faire une pull request.

https://github.com/Plasma-Paris/Plasma.WpfUtils

Vous pouvez l'utiliser comme ceci:

public RelayCommand YesNoMessageBoxCommand { get; private set; }
async void YesNoMessageBox()
{
    var result = await _Service.ShowMessage("This is the content of the message box", "This is the title", System.Windows.MessageBoxButton.YesNo);
    if (result == System.Windows.MessageBoxResult.Yes)
        // [...]
}

Ou comme ça si vous voulez un popin plus sophistiqué:

var result = await _Service.ShowCustomMessageBox(new MyMessageBoxViewModel { /* What you want */ });

Et ça montre des choses comme ça:

2

Xav987
la source
1

L'approche standard

Après avoir passé des années à résoudre ce problème dans WPF, j'ai finalement trouvé la manière standard d'implémenter des dialogues dans WPF. Voici les avantages de cette approche:

  1. NETTOYER
  2. Ne viole pas le modèle de conception MVVM
  3. ViewModal ne fait jamais référence à aucune des bibliothèques d'interface utilisateur (WindowBase, PresentationFramework, etc.)
  4. Parfait pour les tests automatisés
  5. Les dialogues peuvent être remplacés facilement.

Alors quelle est la clé. C'est DI + IoC .

Voici comment cela fonctionne. J'utilise MVVM Light, mais cette approche peut également être étendue à d'autres cadres:

  1. Ajoutez un projet d'application WPF à votre solution. Appelez-le App .
  2. Ajoutez une bibliothèque de classes ViewModal. Appelez-le VM .
  3. L'application fait référence au projet VM. Le projet VM ne sait rien de l'application.
  4. Ajoutez la référence NuGet à MVVM Light aux deux projets . J'utilise MVVM Light Standard ces jours-ci, mais vous êtes également d'accord avec la version complète du Framework.
  5. Ajoutez une interface IDialogService au projet VM:

    public interface IDialogService
    {
      void ShowMessage(string msg, bool isError);
      bool AskBooleanQuestion(string msg);
      string AskStringQuestion(string msg, string default_value);
    
      string ShowOpen(string filter, string initDir = "", string title = "");
      string ShowSave(string filter, string initDir = "", string title = "", string fileName = "");
      string ShowFolder(string initDir = "");
    
      bool ShowSettings();
    }
  6. Exposez une propriété statique publique de IDialogServicetype dans votre ViewModelLocator, mais laissez la partie d'enregistrement pour que la couche View s'exécute. C'est la clé .:

    public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>();
  7. Ajoutez une implémentation de cette interface dans le projet App.

    public class DialogPresenter : IDialogService
    {
        private static OpenFileDialog dlgOpen = new OpenFileDialog();
        private static SaveFileDialog dlgSave = new SaveFileDialog();
        private static FolderBrowserDialog dlgFolder = new FolderBrowserDialog();
    
        /// <summary>
        /// Displays a simple Information or Error message to the user.
        /// </summary>
        /// <param name="msg">String text that is to be displayed in the MessageBox</param>
        /// <param name="isError">If true, Error icon is displayed. If false, Information icon is displayed.</param>
        public void ShowMessage(string msg, bool isError)
        {
                if(isError)
                        System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Error);
                else
                        System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Information);
        }
    
        /// <summary>
        /// Displays a Yes/No MessageBox.Returns true if user clicks Yes, otherwise false.
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public bool AskBooleanQuestion(string msg)
        {
                var Result = System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
                return Result;
        }
    
        /// <summary>
        /// Displays Save dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
        /// user clicks Save button. Returns null if user clicks Cancel button.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="initDir"></param>
        /// <param name="title"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public string ShowSave(string filter, string initDir = "", string title = "", string fileName = "")
        {
                if (!string.IsNullOrEmpty(title))
                        dlgSave.Title = title;
                else
                        dlgSave.Title = "Save";
    
                if (!string.IsNullOrEmpty(fileName))
                        dlgSave.FileName = fileName;
                else
                        dlgSave.FileName = "";
    
                dlgSave.Filter = filter;
                if (!string.IsNullOrEmpty(initDir))
                        dlgSave.InitialDirectory = initDir;
    
                if (dlgSave.ShowDialog() == DialogResult.OK)
                        return dlgSave.FileName;
                else
                        return null;
        }
    
    
        public string ShowFolder(string initDir = "")
        {
                if (!string.IsNullOrEmpty(initDir))
                        dlgFolder.SelectedPath = initDir;
    
                if (dlgFolder.ShowDialog() == DialogResult.OK)
                        return dlgFolder.SelectedPath;
                else
                        return null;
        }
    
    
        /// <summary>
        /// Displays Open dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
        /// user clicks Open button. Returns null if user clicks Cancel button.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="initDir"></param>
        /// <param name="title"></param>
        /// <returns></returns>
        public string ShowOpen(string filter, string initDir = "", string title = "")
        {
                if (!string.IsNullOrEmpty(title))
                        dlgOpen.Title = title;
                else
                        dlgOpen.Title = "Open";
    
                dlgOpen.Multiselect = false;
                dlgOpen.Filter = filter;
                if (!string.IsNullOrEmpty(initDir))
                        dlgOpen.InitialDirectory = initDir;
    
                if (dlgOpen.ShowDialog() == DialogResult.OK)
                        return dlgOpen.FileName;
                else
                        return null;
        }
    
        /// <summary>
        /// Shows Settings dialog.
        /// </summary>
        /// <returns>true if User clicks OK button, otherwise false.</returns>
        public bool ShowSettings()
        {
                var w = new SettingsWindow();
                MakeChild(w); //Show this dialog as child of Microsoft Word window.
                var Result = w.ShowDialog().Value;
                return Result;
        }
    
        /// <summary>
        /// Prompts user for a single value input. First parameter specifies the message to be displayed in the dialog 
        /// and the second string specifies the default value to be displayed in the input box.
        /// </summary>
        /// <param name="m"></param>
        public string AskStringQuestion(string msg, string default_value)
        {
                string Result = null;
    
                InputBox w = new InputBox();
                MakeChild(w);
                if (w.ShowDialog(msg, default_value).Value)
                        Result = w.Value;
    
                return Result;
        }
    
        /// <summary>
        /// Sets Word window as parent of the specified window.
        /// </summary>
        /// <param name="w"></param>
        private static void MakeChild(System.Windows.Window w)
        {
                IntPtr HWND = Process.GetCurrentProcess().MainWindowHandle;
                var helper = new WindowInteropHelper(w) { Owner = HWND };
        }
    }
  8. Alors que certaines de ces fonctions sont génériques ( ShowMessage, AskBooleanQuestionetc.), d'autres sont spécifiques à ce projet et utilisent des Windows personnalisés . Vous pouvez ajouter d'autres fenêtres personnalisées de la même manière. La clé est de conserver les éléments spécifiques à l'interface utilisateur dans la couche View et d'exposer simplement les données renvoyées à l'aide de POCO dans la couche VM .
  9. Effectuez l'enregistrement IoC de votre interface dans la couche View à l'aide de cette classe. Vous pouvez le faire dans le constructeur de votre vue principale (après InitializeComponent()appel):

    SimpleIoc.Default.Register<IDialogService, DialogPresenter>();
  10. Voilà. Vous avez désormais accès à toutes vos fonctionnalités de dialogue sur les couches VM et View. Votre couche VM peut appeler ces fonctions comme ceci:

    var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", "");
  11. Si propre que vous voyez. La couche VM ne sait pas comment une question Oui / Non sera présentée à l'utilisateur par la couche UI et peut toujours fonctionner avec succès avec le résultat renvoyé par la boîte de dialogue.

Autres avantages gratuits

  1. Pour écrire un test unitaire, vous pouvez fournir une implémentation personnalisée de IDialogServicedans votre projet Test et enregistrer cette classe dans IoC dans le constructeur de votre classe de test.
  2. Vous devrez importer des espaces de noms tels que Microsoft.Win32pour accéder aux boîtes de dialogue Ouvrir et Enregistrer. Je les ai laissés de côté, car il existe également une version WinForms de ces boîtes de dialogue, plus quelqu'un peut vouloir créer sa propre version. Notez également que certains des identifiants utilisés dans DialogPresentersont des noms de mes propres fenêtres (par exemple SettingsWindow). Vous devrez soit les supprimer de l'interface et de l'implémentation, soit fournir vos propres fenêtres.
  3. Si votre machine virtuelle exécute plusieurs threads, appelez MVVM Light au DispatcherHelper.Initialize()début du cycle de vie de votre application.
  4. À l'exception de ceux DialogPresenterqui sont injectés dans la couche View, les autres ViewModals doivent être enregistrés dans ViewModelLocator, puis une propriété statique publique de ce type doit être exposée pour que la couche View puisse consommer. Quelque chose comme ça:

    public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>();
  5. Pour la plupart, vos boîtes de dialogue ne devraient pas avoir de code pour des choses comme la liaison ou la définition de DataContext etc. Vous ne devriez même pas passer des choses en tant que paramètres de constructeur. XAML peut faire tout cela pour vous, comme ceci:

    <Window x:Class="YourViewNamespace.SettingsWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:local="clr-namespace:YourViewProject"
      xmlns:vm="clr-namespace:YourVMProject;assembly=YourVMProject"
      DataContext="{x:Static vm:ViewModelLocator.Settings}"
      d:DataContext="{d:DesignInstance Type=vm:SettingsVM}" />
  6. DataContextCette configuration vous offre toutes sortes d'avantages au moment de la conception tels que Intellisense et l'auto-complétion.

J'espère que cela aide tout le monde.

point net
la source
0

Je réfléchissais à un problème similaire en demandant à quoi devrait ressembler le modèle de vue pour une tâche ou une boîte de dialogue .

Ma solution actuelle ressemble à ceci:

public class SelectionTaskModel<TChoosable> : ViewModel
    where TChoosable : ViewModel
{
    public SelectionTaskModel(ICollection<TChoosable> choices);
    public ReadOnlyCollection<TChoosable> Choices { get; }
    public void Choose(TChoosable choosen);
    public void Abort();
}

Lorsque le modèle de vue décide qu'une entrée utilisateur est requise, il extrait une instance de SelectionTaskModelavec les choix possibles pour l'utilisateur. L'infrastructure s'occupe de faire apparaître la vue correspondante, qui en temps voulu appellera la Choose()fonction au choix de l'utilisateur.

David Schmitt
la source
0

J'ai lutté avec le même problème. J'ai trouvé un moyen d'intercommuniquer entre le View et le ViewModel. Vous pouvez lancer l'envoi d'un message du ViewModel à la vue pour lui dire d'afficher une boîte de message et il fera rapport avec le résultat. Ensuite, le ViewModel peut répondre au résultat renvoyé par la vue.

Je le démontre dans mon blog :

Dan joue avec Firelight
la source
0

J'ai écrit un article assez complet sur ce sujet et j'ai également développé une bibliothèque de pop-in pour MVVM Dialogs. L'adhésion stricte à MVVM est non seulement possible mais très propre lorsqu'elle est correctement mise en œuvre, et elle peut être facilement étendue à des bibliothèques tierces qui n'y adhèrent pas elles-mêmes:

https://www.codeproject.com/Articles/820324/Implementing-Dialog-Boxes-in-MVVM

Mark Feldman
la source
0

Désolé, mais je dois intervenir. J'ai parcouru plusieurs des solutions suggérées avant de trouver l'espace de noms Prism.Wpf.Interactivity dans le projet Prism. Vous pouvez utiliser les demandes d'interaction et l'action de fenêtre contextuelle pour ouvrir une fenêtre personnalisée ou pour des besoins plus simples, des fenêtres contextuelles de notification et de confirmation sont intégrées. Celles-ci créent de véritables fenêtres et sont gérées comme telles. vous pouvez passer un objet de contexte avec toutes les dépendances dont vous avez besoin dans la boîte de dialogue. Nous utilisons cette solution dans mon travail depuis que je l'ai trouvée. Nous avons de nombreux développeurs seniors ici et personne n'a rien trouvé de mieux. Notre solution précédente était le service de dialogue en superposition et l'utilisation d'une classe de présentateur pour y arriver, mais vous deviez avoir des usines pour tous les modèles de vue de dialogue, etc.

Ce n'est pas anodin mais ce n'est pas non plus super compliqué. Et il est intégré à Prism et est donc la meilleure (ou meilleure) pratique IMHO.

Mes 2 cents!

jogi
la source
-1

EDIT: oui, je suis d'accord que ce n'est pas une approche MVVM correcte et j'utilise maintenant quelque chose de similaire à ce qui est suggéré par blindmeis.

L'une des façons dont vous pourriez y parvenir est

Dans votre modèle de vue principale (où vous ouvrez le modal):

void OpenModal()
{
    ModalWindowViewModel mwvm = new ModalWindowViewModel();
    Window mw = new Window();
    mw.content = mwvm;
    mw.ShowDialog()
    if(mw.DialogResult == true)
    { 
        // Your Code, you can access property in mwvm if you need.
    }
}

Et dans votre fenêtre modale View / ViewModel:

XAML:

<Button Name="okButton" Command="{Binding OkCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">OK</Button>
<Button Margin="2" VerticalAlignment="Center" Name="cancelButton" IsCancel="True">Cancel</Button>

ViewModel:

public ICommand OkCommand
{
    get
    {
        if (_okCommand == null)
        {
            _okCommand = new ActionCommand<Window>(DoOk, CanDoOk);
        }
        return _okCommand ;
    }
}

void DoOk(Window win)
{
    <!--Your Code-->
    win.DialogResult = true;
    win.Close();
}

bool CanDoOk(Window win) { return true; }

ou similaire à ce qui est affiché ici WPF MVVM: Comment fermer une fenêtre

Simone
la source
2
Je n'étais pas le downvote, mais je soupçonne que c'est parce que le modèle de vue a une référence directe à la vue.
Brian Gideon
@BrianGideon, merci pour votre commentaire. Je suis d'accord que ce n'est pas une solution découplée. En fait, je n'utilise pas quelque chose de similaire au whar suggéré par blindmeis. Merci encore.
Simone
C'est une mauvaise forme d'atteindre la vue quand c'est si facile de ne pas le faire.
Chris Bordeman