Comment le ViewModel doit-il fermer le formulaire?

247

J'essaie d'apprendre WPF et le problème MVVM, mais j'ai rencontré un problème. Cette question est similaire mais pas tout à fait la même que celle-ci (manipulation-dialogues-en-wpf-avec-mvvm) ...

J'ai un formulaire de «connexion» écrit en utilisant le modèle MVVM.

Ce formulaire a un ViewModel qui contient le nom d'utilisateur et le mot de passe, qui sont liés à la vue dans le XAML à l'aide de liaisons de données normales. Il a également une commande "Login" qui est liée au bouton "Login" sur le formulaire, agan utilisant une liaison de données normale.

Lorsque la commande "Login" se déclenche, elle appelle une fonction dans le ViewModel qui s'éteint et envoie des données sur le réseau pour se connecter. Lorsque cette fonction se termine, il y a 2 actions:

  1. La connexion n'était pas valide - nous montrons juste un MessageBox et tout va bien

  2. La connexion était valide, nous devons fermer le formulaire de connexion et le faire renvoyer vrai comme son DialogResult...

Le problème est que le ViewModel ne sait rien de la vue réelle, alors comment peut-il fermer la vue et lui dire de renvoyer un DialogResult particulier ?? Je pourrais coller du code dans le CodeBehind, et / ou passer la vue au ViewModel, mais il semble que cela vaincrait entièrement le point de MVVM ...


Mettre à jour

En fin de compte, je viens de violer la «pureté» du modèle MVVM et j'ai demandé à View de publier un Closedévénement et d'exposer une Closeméthode. Le ViewModel appelle alors simplement view.Close. La vue n'est connue que via une interface et câblée via un conteneur IOC, donc aucune testabilité ou maintenabilité n'est perdue.

Il semble plutôt idiot que la réponse acceptée soit à -5 voix! Bien que je sois bien conscient des bons sentiments que l'on ressent en résolvant un problème tout en étant "pur", je ne suis sûrement pas le seul à penser que 200 lignes d'événements, de commandes et de comportements juste pour éviter une méthode d'une ligne dans le nom de "motifs" et de "pureté" est un peu ridicule ....

Orion Edwards
la source
2
Je n'ai pas déprécié la réponse acceptée, mais je suppose que la raison des rétrogradations est qu'elle n'est pas utile en général, même si cela peut fonctionner dans un cas. Vous l'avez dit vous-même dans un autre commentaire: "Bien que le formulaire de connexion soit une boîte de dialogue" à deux champs ", j'en ai beaucoup d'autres qui sont beaucoup plus complexes (et donc garantissent MVVM), mais doivent encore être fermés ..."
Joe Blanc
1
Je vois votre point, mais je pense personnellement que même pour le cas général, une Closeméthode simple est toujours la meilleure solution. Tout le reste sur les autres dialogues plus complexes est MVVM et databound, mais il semblait tout simplement idiot d'implémenter les énormes "solutions" ici au lieu d'une simple méthode ...
Orion Edwards
2
Vous pouvez vérifier le lien suivant pour le résultat de la boîte de dialogue asimsajjad.blogspot.com/2010/10/… , qui renverra la boîte de dialogue et fermera la vue à partir du viewModel
Asim Sajjad
3
Veuillez modifier la réponse acceptée à cette question. Il existe de nombreuses bonnes solutions bien meilleures que quiconque remet en question l'utilisation de MVVM pour cette fonctionnalité. Ce n'est pas une réponse, c'est de l'évitement.
ScottCher
2
@OrionEdwards Je pense que vous aviez raison de casser le modèle ici. L'un des principaux objectifs d'un modèle de conception est d'accélérer les cycles de développement, d'augmenter la maintenabilité et de simplifier votre code en obligeant toute l'équipe à suivre les mêmes règles. Cela n'est pas possible en ajoutant des dépendances à des bibliothèques externes et en implémentant des centaines de lignes de code pour accomplir une tâche, ignorant totalement qu'il existe une solution beaucoup plus simple, simplement parce qu'il est obstiné de sacrifier la "pureté" du modèle. Assurez-vous simplement de documenter ce que vous avez fait et de KISS votre code ( k eep i t s hort and s imple).
M463

Réponses:

324

J'ai été inspiré par la réponse de Thejuan pour écrire une propriété attachée plus simple. Pas de styles, pas de déclencheurs; à la place, vous pouvez simplement faire ceci:

<Window ...
        xmlns:xc="clr-namespace:ExCastle.Wpf"
        xc:DialogCloser.DialogResult="{Binding DialogResult}">

C'est presque aussi propre que si l'équipe WPF avait bien compris et avait fait de DialogResult une propriété de dépendance en premier lieu. Il suffit de mettre une bool? DialogResultpropriété sur votre ViewModel et d'implémenter INotifyPropertyChanged, et voilà, votre ViewModel peut fermer la fenêtre (et définir son DialogResult) simplement en définissant une propriété. MVVM comme il se doit.

Voici le code pour DialogCloser:

using System.Windows;

namespace ExCastle.Wpf
{
    public static class DialogCloser
    {
        public static readonly DependencyProperty DialogResultProperty =
            DependencyProperty.RegisterAttached(
                "DialogResult",
                typeof(bool?),
                typeof(DialogCloser),
                new PropertyMetadata(DialogResultChanged));

        private static void DialogResultChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var window = d as Window;
            if (window != null)
                window.DialogResult = e.NewValue as bool?;
        }
        public static void SetDialogResult(Window target, bool? value)
        {
            target.SetValue(DialogResultProperty, value);
        }
    }
}

J'ai également posté cela sur mon blog .

Joe White
la source
3
Celui-ci est la réponse que j'aime le plus! Bon travail d'écriture qui a attaché la propriété.
Jorge Vargas
2
Belle option, mais il y a un bug subtil dans cette solution. Si le modèle d'affichage de la boîte de dialogue est un singleton, la valeur DialogResult est reportée à la prochaine utilisation de la boîte de dialogue. Cela signifie qu'il annulera ou acceptera instantanément avant de s'afficher, de sorte que la boîte de dialogue ne s'affichera pas une deuxième fois.
Fin du codage
13
@HiTech Magic, on dirait que le bug consiste à utiliser un ViewModel singleton en premier lieu. (sourire) Sérieusement, pourquoi voudriez-vous un ViewModel singleton? C'est une mauvaise idée de conserver l'état mutable dans les variables globales. Rend le test un cauchemar, et le test est l'une des raisons pour lesquelles vous utiliseriez MVVM en premier lieu.
Joe White
3
Le but de MVVM n'est-il pas de coupler étroitement votre logique à une interface utilisateur spécifique? Dans ce cas, bool? n'est certainement pas utilisable par une autre interface utilisateur telle qu'un WinForm, et DialogCloser est spécifique à WPF. Alors, comment cela s'intègre-t-il bien comme solution? Aussi, pourquoi écrire du code 2x-10x juste pour fermer une fenêtre via une liaison?
David Anderson
2
@DavidAnderson, je n'essaierais en aucun cas MVVM avec WinForms; son support de liaison de données est trop faible et MVVM s'appuie sur un système de liaison bien pensé. Et c'est loin du code 2x-10x. Vous écrivez ce code une fois , pas une fois pour chaque fenêtre. Après cela, c'est une liaison sur une ligne plus une propriété de notification, en utilisant le même mécanisme que vous utilisez déjà pour tout le reste de votre vue (donc, par exemple, vous n'avez pas besoin d'injecter une interface de vue supplémentaire juste pour gérer la fermeture de la la fenêtre). Vous êtes invités à faire d'autres compromis, mais cela me semble généralement une bonne affaire.
Joe White
64

De mon point de vue, la question est plutôt bonne car la même approche serait utilisée non seulement pour la fenêtre "Login", mais pour tout type de fenêtre. J'ai passé en revue de nombreuses suggestions et aucune ne me convient. Veuillez consulter ma suggestion tirée de l' article sur le modèle de conception MVVM .

Chaque classe ViewModel doit hériter de WorkspaceViewModelcelle qui a l' RequestCloseévénement et la CloseCommandpropriété du ICommandtype. L'implémentation par défaut de la CloseCommandpropriété déclenchera l' RequestCloseévénement.

Afin de fermer la fenêtre, la OnLoadedméthode de votre fenêtre doit être remplacée:

void CustomerWindow_Loaded(object sender, RoutedEventArgs e)
{
    CustomerViewModel customer = CustomerViewModel.GetYourCustomer();
    DataContext = customer;
    customer.RequestClose += () => { Close(); };
}

ou OnStartupméthode de votre application:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        MainWindow window = new MainWindow();
        var viewModel = new MainWindowViewModel();
        viewModel.RequestClose += window.Close;
        window.DataContext = viewModel;

        window.Show();
    }

Je suppose que la mise en œuvre des RequestCloseévénements et des CloseCommandpropriétés dans le WorkspaceViewModelest assez claire, mais je vais les montrer cohérentes:

public abstract class WorkspaceViewModel : ViewModelBase
// There's nothing interesting in ViewModelBase as it only implements the INotifyPropertyChanged interface
{
    RelayCommand _closeCommand;
    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
            {
                _closeCommand = new RelayCommand(
                   param => Close(),
                   param => CanClose()
                   );
            }
            return _closeCommand;
        }
    }

    public event Action RequestClose;

    public virtual void Close()
    {
        if ( RequestClose != null )
        {
            RequestClose();
        }
    }

    public virtual bool CanClose()
    {
        return true;
    }
}

Et le code source de RelayCommand:

public class RelayCommand : ICommand
{
    #region Constructors

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members

    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields
}

PS Ne me traitez pas mal pour ces sources! Si je les avais hier, cela m'aurait fait gagner quelques heures ...

PPS Tous commentaires ou suggestions sont les bienvenus.

Budda
la source
2
Umm, le fait que vous vous soyez accroché au gestionnaire d'événements customer.RequestClosedans le code derrière votre fichier XAML ne viole-t-il pas le modèle MVVM? Vous auriez pu tout aussi bien vous lier au Clickgestionnaire d'événements sur votre bouton de fermeture en voyant que vous avez touché le code derrière de toute façon et que vous avez fait un this.Close()! Droite?
GONeale
1
Je n'ai pas trop de problèmes avec l'approche événementielle mais je n'aime pas le mot RequestClose car pour moi cela implique encore beaucoup de connaissances sur l'implémentation de View. Je préfère exposer des propriétés telles que IsCancelled qui ont tendance à être plus significatives compte tenu du contexte et à impliquer moins sur ce que la vue est censée faire en réponse.
jpierson
18

J'ai utilisé des comportements attachés pour fermer la fenêtre. Liez une propriété "signal" sur votre ViewModel au comportement attaché (j'utilise en fait un déclencheur) Lorsqu'il est défini sur true, le comportement ferme la fenêtre.

http://adammills.wordpress.com/2009/07/01/window-close-from-xaml/

Adam Mills
la source
C'est la seule réponse jusqu'à présent qui ne nécessite aucun code dans la fenêtre (et ferme en fait une fenêtre modale, au lieu de suggérer une autre approche). Dommage que cela nécessite tellement de complexité, avec le style et le déclencheur et tout ce boue - il semble que cela devrait vraiment être faisable avec un comportement attaché à une ligne.
Joe White
4
Maintenant, c'est faisable avec un comportement attaché à une ligne. Voir ma réponse: stackoverflow.com/questions/501886/…
Joe White
15

Il y a beaucoup de commentaires faisant valoir les avantages et les inconvénients de MVVM ici. Pour moi, je suis d'accord avec Nir; il s'agit d'utiliser le modèle de manière appropriée et MVVM ne convient pas toujours. Les gens semblent être prêts à sacrifier JUST tous les principes les plus importants de la conception de logiciels pour l'adapter à MVVM.

Cela dit, je pense que votre cas pourrait être un bon ajustement avec un peu de refactoring.

Dans la plupart des cas que j'ai rencontrés, WPF vous permet de vous en tirer SANS plusieurs Window s. Vous pourriez peut-être essayer d'utiliser Frames et Pages au lieu de Windows avec DialogResults.

Dans votre cas, ma suggestion serait de LoginFormViewModelgérer le LoginCommandet si la connexion n'est pas valide, définissez une propriété sur LoginFormViewModelune valeur appropriée ( falseou une valeur d'énumération comme UserAuthenticationStates.FailedAuthentication). Vous feriez de même pour une connexion réussie ( trueou une autre valeur énumérée). Vous utiliseriez alors un DataTriggerqui répond aux différents états d'authentification des utilisateurs et pourriez utiliser un simple Setterpour changer la Sourcepropriété du Frame.

Ayant votre fenêtre de connexion renvoyer un DialogResultje pense que c'est là que vous êtes confus; c'est DialogResultvraiment une propriété de votre ViewModel. Dans mon expérience, certes limitée, avec WPF, quand quelque chose ne se sent pas bien, généralement parce que je pense à la façon dont j'aurais fait la même chose dans WinForms.

J'espère que cela pourra aider.

EightyOne Unite
la source
10

En supposant que votre boîte de dialogue de connexion est la première fenêtre créée, essayez ceci dans votre classe LoginViewModel:

    void OnLoginResponse(bool loginSucceded)
    {
        if (loginSucceded)
        {
            Window1 window = new Window1() { DataContext = new MainWindowViewModel() };
            window.Show();

            App.Current.MainWindow.Close();
            App.Current.MainWindow = window;
        }
        else
        {
            LoginError = true;
        }
    }
Jim Wallace
la source
Les hommes c'est simple et fonctionne très bien. Actuellement, j'utilise cette approche.
Erre Efe
Cela ne fonctionne que pour la fenêtre MAIN. Ne l'utilisez donc pas pour d'autres fenêtres.
Oleksii
7

Il s'agit d'une solution simple et propre - vous ajoutez un événement au ViewModel et demandez à la fenêtre de se fermer lorsque cet événement est déclenché.

Pour plus de détails, voir mon article de blog, Fermer la fenêtre de ViewModel .

XAML:

<Window
  x:Name="this"
  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
  <i:Interaction.Triggers>
    <i:EventTrigger SourceObject="{Binding}" EventName="Closed">
      <ei:CallMethodAction
        TargetObject="{Binding ElementName=this}"
        MethodName="Close"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
<Window>

ViewModel:

private ICommand _SaveAndCloseCommand;
public ICommand SaveAndCloseCommand
{
  get
  {
    return _SaveAndCloseCommand ??
      (_SaveAndCloseCommand = new DelegateCommand(SaveAndClose));
  }
}
private void SaveAndClose()
{
  Save();
  Close();
}

public event EventHandler Closed;
private void Close()
{
  if (Closed != null) Closed(this, EventArgs.Empty);
}

Remarque: L'exemple utilise Prism's DelegateCommand(voir Prism: Commanding ), mais n'importe quelle ICommandimplémentation peut être utilisée d'ailleurs.

Vous pouvez utiliser les comportements de ce package officiel.

Shimmy Weitzhandler
la source
2
+1 mais vous devez fournir plus de détails dans la réponse elle-même, par exemple que cette solution nécessite une référence à l'assemblage d'interactivité Expression Blend.
surfen
6

La façon dont je le gérerais est d'ajouter un gestionnaire d'événements dans mon ViewModel. Une fois l'utilisateur connecté, je déclencherais l'événement. À mon avis, je m'attacherais à cet événement et lorsqu'il se déclencherait, je fermerais la fenêtre.


la source
2
C'est aussi ce que je fais d'habitude. Même si cela semble un peu sale compte tenu de toutes ces nouveautés de commande wpf.
Botz3000
4

Voici ce que j'ai fait au départ, qui fonctionne, mais cela semble plutôt long et laid (tout ce qui est statique global n'est jamais bon)

1: App.xaml.cs

public partial class App : Application
{
    // create a new global custom WPF Command
    public static readonly RoutedUICommand LoggedIn = new RoutedUICommand();
}

2: LoginForm.xaml

// bind the global command to a local eventhandler
<CommandBinding Command="client:App.LoggedIn" Executed="OnLoggedIn" />

3: LoginForm.xaml.cs

// implement the local eventhandler in codebehind
private void OnLoggedIn( object sender, ExecutedRoutedEventArgs e )
{
    DialogResult = true;
    Close();
}

4: LoginFormViewModel.cs

// fire the global command from the viewmodel
private void OnRemoteServerReturnedSuccess()
{
    App.LoggedIn.Execute(this, null);
}

Plus tard, j'ai ensuite supprimé tout ce code et j'ai simplement LoginFormViewModelappelé la méthode Close sur sa vue. Cela a fini par être beaucoup plus agréable et plus facile à suivre. À mon humble avis, le motif est de donner aux gens un moyen plus facile de comprendre ce que fait votre application, et dans ce cas, MVVM le rendait beaucoup plus difficile à comprendre que si je ne l'avais pas utilisé, et était maintenant un anti- modèle.

Orion Edwards
la source
3

Pour info, je suis tombé sur ce même problème et je pense avoir trouvé un moyen de contourner le problème qui ne nécessite pas de variables globales ou statiques, bien que ce ne soit pas la meilleure réponse. Je vous laisse décider vous-même.

Dans mon cas, le ViewModel qui instancie la fenêtre à afficher (appelons-le ViewModelMain) connaît également le LoginFormViewModel (en utilisant la situation ci-dessus comme exemple).

J'ai donc créé une propriété sur le LoginFormViewModel de type ICommand (appelons-la CloseWindowCommand). Ensuite, avant d'appeler .ShowDialog () sur la fenêtre, j'ai défini la propriété CloseWindowCommand sur LoginFormViewModel sur la méthode window.Close () de la fenêtre que j'ai instanciée. Ensuite, à l'intérieur de LoginFormViewModel, tout ce que j'ai à faire est d'appeler CloseWindowCommand.Execute () pour fermer la fenêtre.

C'est un peu une solution de contournement / hack, je suppose, mais cela fonctionne bien sans vraiment casser le modèle MVVM.

N'hésitez pas à critiquer ce processus autant que vous le souhaitez, je peux le prendre! :)


la source
Je ne suis pas sûr de l'avoir complètement compris, mais cela ne signifie-t-il pas que votre MainWindow doit être instanciée avant votre LoginWindow? C'est quelque chose que j'aimerais éviter si possible
Orion Edwards
3

C'est probablement très tard, mais j'ai rencontré le même problème et j'ai trouvé une solution qui fonctionne pour moi.

Je ne peux pas comprendre comment créer une application sans dialogues (c'est peut-être juste un blocage mental). J'étais donc dans une impasse avec MVVM et je montrais une boîte de dialogue. Je suis donc tombé sur cet article CodeProject:

http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

Qui est un UserControl qui permet essentiellement à une fenêtre d'être dans l'arborescence visuelle d'une autre fenêtre (non autorisé dans xaml). Il expose également une propriété de dépendance booléenne appelée IsShowing.

Vous pouvez définir un style comme, généralement dans une ressource, qui affiche essentiellement la boîte de dialogue chaque fois que la propriété Content du contrôle! = Null via des déclencheurs:

<Style TargetType="{x:Type d:Dialog}">
    <Style.Triggers>
        <Trigger Property="HasContent"  Value="True">
            <Setter Property="Showing" Value="True" />
        </Trigger>
    </Style.Triggers>
</Style>

Dans la vue où vous souhaitez afficher la boîte de dialogue, il vous suffit de présenter ceci:

<d:Dialog Content="{Binding Path=DialogViewModel}"/>

Et dans votre ViewModel, tout ce que vous avez à faire est de définir la propriété sur une valeur (Remarque: la classe ViewModel doit prendre en charge INotifyPropertyChanged pour que la vue sache que quelque chose s'est produit).

ainsi:

DialogViewModel = new DisplayViewModel();

Pour faire correspondre le ViewModel avec la vue, vous devriez avoir quelque chose comme ça dans un spécialiste des ressources:

<DataTemplate DataType="{x:Type vm:DisplayViewModel}">
    <vw:DisplayView/>
</DataTemplate>

Avec tout cela, vous obtenez un code à une ligne pour afficher la boîte de dialogue. Le problème que vous obtenez est que vous ne pouvez pas vraiment fermer la boîte de dialogue avec juste le code ci-dessus. C'est pourquoi vous devez mettre un événement dans une classe de base ViewModel dont DisplayViewModel hérite et au lieu du code ci-dessus, écrivez ceci

        var vm = new DisplayViewModel();
        vm.RequestClose += new RequestCloseHandler(DisplayViewModel_RequestClose);
        DialogViewModel = vm;

Ensuite, vous pouvez gérer le résultat de la boîte de dialogue via le rappel.

Cela peut sembler un peu complexe, mais une fois que les bases sont posées, c'est assez simple. Encore une fois, c'est ma mise en œuvre, je suis sûr qu'il y en a d'autres :)

J'espère que cela aide, cela m'a sauvé.

Jose
la source
3

Ok, donc cette question a presque 6 ans et je ne trouve toujours pas ici ce que je pense que c'est la bonne réponse, alors permettez-moi de partager mes "2 cents" ...

J'ai en fait 2 façons de le faire, la première est la plus simple ... la seconde à droite, donc si vous cherchez la bonne, sautez simplement le n ° 1 et passez au n ° 2 :

1. Rapide et facile (mais pas complet)

Si je n'ai qu'un petit projet, je crée parfois simplement une action CloseWindow dans le ViewModel:

        public Action CloseWindow { get; set; } // In MyViewModel.cs

Et celui qui crée la vue, ou dans le code de la vue derrière, je viens de définir la méthode que l'action appellera:

(rappelez-vous que MVVM concerne la séparation de la vue et du ViewModel ... le code de la vue est toujours la vue et tant qu'il y a une séparation appropriée, vous ne violez pas le modèle)

Si certains ViewModel créent une nouvelle fenêtre:

private void CreateNewView()
{
    MyView window = new MyView();
    window.DataContext = new MyViewModel
                             {
                                 CloseWindow = window.Close,
                             }; 
    window.ShowDialog();
}

Ou si vous le souhaitez dans votre fenêtre principale, placez-le simplement sous le constructeur de votre vue:

public MyView()
{
    InitializeComponent();           
    this.DataContext = new MainViewModel
                           {
                                CloseWindow = this.Close
                           };
}

lorsque vous souhaitez fermer la fenêtre, il vous suffit d'appeler l'action sur votre ViewModel.


2. La bonne façon

Maintenant, la bonne façon de le faire est d'utiliser Prism (IMHO), et tout à ce sujet peut être trouvé ici .

Vous pouvez faire une demande d'interaction , la remplir avec toutes les données dont vous aurez besoin dans votre nouvelle fenêtre, la déjeuner, la fermer et même recevoir des données . Tout cela encapsulé et approuvé MVVM. Vous obtenez même un état de la façon dont la fenêtre a été fermée , comme si l'utilisateur Canceledou Accepted(bouton OK) la fenêtre et les données si vous en avez besoin . C'est un peu plus compliqué et réponse n ° 1, mais c'est beaucoup plus complet, et un modèle recommandé par Microsoft.

Le lien que j'ai donné contient tous les extraits de code et exemples, donc je ne vais pas prendre la peine de placer du code ici, il suffit de lire l'article de téléchargement du Prism Quick Start et de l'exécuter, c'est vraiment simple à comprendre juste un peu plus verbeux le faire fonctionner, mais les avantages sont plus importants que la simple fermeture d'une fenêtre.

mFeinstein
la source
Bien, mais la résolution et l'attribution des ViewModels ne peuvent pas toujours être aussi simples. Que se passe-t-il si le même modèle de vue est DataContext de nombreux Windows?
Kylo Ren
Ensuite, je suppose que vous devez fermer toutes les fenêtres en même temps, rappelez-vous qu'une action peut déclencher de nombreux délégués à la fois, utilisez simplement +=pour ajouter un délégué, et appelez l'action, elle se déclenchera toutes ... Ou vous devez faire une logique spéciale sur votre machine virtuelle afin qu'elle sache quelle fenêtre fermer (peut-être avoir une collection d'actions de fermeture) .... Mais je pense qu'avoir plusieurs vues liées à une machine virtuelle n'est pas la meilleure pratique, c'est mieux gérer pour avoir une vue et une instance de machine virtuelle, liées l'une à l'autre et peut-être une machine virtuelle parent qui gère toutes les machines virtuelles enfants liées à toutes les vues.
mFeinstein
3
public partial class MyWindow: Window
{
    public ApplicationSelection()
    {
      InitializeComponent();

      MyViewModel viewModel = new MyViewModel();

      DataContext = viewModel;

      viewModel.RequestClose += () => { Close(); };

    }
}

public class MyViewModel
{

  //...Your code...

  public event Action RequestClose;

  public virtual void Close()
  {
    if (RequestClose != null)
    {
      RequestClose();
    }
  }

  public void SomeFunction()
  {
     //...Do something...
     Close();
  }
}
Amir Touitou
la source
2

Vous pouvez demander au ViewModel d'exposer un événement auquel la vue s'enregistre. Ensuite, lorsque le ViewModel décide de l'heure de fermeture de la vue, il déclenche cet événement qui provoque la fermeture de la vue. Si vous voulez qu'une valeur de résultat spécifique soit renvoyée, vous auriez une propriété dans le ViewModel pour cela.

Abdulla Al-Qawasmeh
la source
Je suis d'accord avec cela - la simplicité est précieuse. Je dois penser à ce qui se passera lorsque le prochain développeur junior sera embauché pour reprendre ce projet. Je suppose qu'il aura de bien meilleures chances de bien faire les choses comme vous le décrivez. Sauf si vous pensez que vous allez maintenir ce code pour toujours vous-même? +1
Dean
2

Juste pour ajouter au nombre énorme de réponses, je veux ajouter ce qui suit. En supposant que vous avez une ICommand sur votre ViewModel et que vous souhaitez que cette commande ferme sa fenêtre (ou toute autre action d'ailleurs), vous pouvez utiliser quelque chose comme ce qui suit.

var windows = Application.Current.Windows;
for (var i=0;i< windows.Count;i++ )
    if (windows[i].DataContext == this)
        windows[i].Close();

Ce n'est pas parfait, et pourrait être difficile à tester (car il est difficile de se moquer / écraser un statique) mais il est plus propre (à mon humble avis) que les autres solutions.

Erick

Erick T
la source
Je suis devenu très heureux quand j'ai vu votre réponse simple! mais ça ne marche pas non plus! Je dois ouvrir et fermer avec Visual Basic. Connaissez-vous l'équivalence de (windows [i] .DataContext == this) en VB?
Ehsan
Je l'ai enfin compris! :) Merci. Si Windows (i) .DataContext est moi
Ehsan
Connaissez-vous la même manière simple d'ouvrir une fenêtre? Je dois également envoyer et recevoir des données dans le modèle de vue enfant et vice versa.
Ehsan
1

J'ai implémenté la solution de Joe White, mais j'ai rencontré des problèmes avec " DialogResult occasionnel ne peut être défini qu'après la création de la fenêtre et l'afficher sous forme de boîte de dialogue ».

Je gardais le ViewModel après la fermeture de la vue et, parfois, j'ai ouvert une nouvelle vue en utilisant la même machine virtuelle. Il semble que la fermeture de la nouvelle vue avant que l'ancienne vue n'ait été récupérée a conduit DialogResultChanged à essayer de définir DialogResult propriété sur la fenêtre fermée, provoquant ainsi l'erreur.

Ma solution était de changer DialogResultChanged pour vérifier la fenêtre propriété IsLoaded de :

private static void DialogResultChanged(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;
    if (window != null && window.IsLoaded)
        window.DialogResult = e.NewValue as bool?;
}

Après avoir effectué cette modification, toutes les pièces jointes aux boîtes de dialogue fermées sont ignorées.

Jim Hansen
la source
Merci Monsieur. J'ai eu le même problème
DJ Burb
1

J'ai fini par mélanger la réponse de Joe White et du code de la réponse d' Adam Mills , car je devais montrer un contrôle utilisateur dans une fenêtre créée par programme. Donc, le DialogCloser n'a pas besoin d'être sur la fenêtre, il peut être sur le contrôle utilisateur lui-même

<UserControl ...
    xmlns:xw="clr-namespace:Wpf"
    xw:DialogCloser.DialogResult="{Binding DialogResult}">

Et le DialogCloser trouvera la fenêtre du contrôle utilisateur si elle n'était pas attachée à la fenêtre elle-même.

namespace Wpf
{
  public static class DialogCloser
  {
    public static readonly DependencyProperty DialogResultProperty =
        DependencyProperty.RegisterAttached(
            "DialogResult",
            typeof(bool?),
            typeof(DialogCloser),
            new PropertyMetadata(DialogResultChanged));

    private static void DialogResultChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
      var window = d.GetWindow();
      if (window != null)
        window.DialogResult = e.NewValue as bool?;
    }

    public static void SetDialogResult(DependencyObject target, bool? value)
    {
      target.SetValue(DialogResultProperty, value);
    }
  }

  public static class Extensions
  {
    public static Window GetWindow(this DependencyObject sender_)
    {
      Window window = sender_ as Window;        
      return window ?? Window.GetWindow( sender_ );
    }
  }
}
Anuroopa Shenoy
la source
1

Le comportement est le moyen le plus pratique ici.

  • D'une part, il peut être lié au modèle de vue donné (qui peut signaler "fermez le formulaire!")

  • D'un autre côté, il a accès au formulaire lui-même et peut donc s'abonner aux événements spécifiques au formulaire nécessaires, afficher une boîte de dialogue de confirmation ou autre.

Écrire le comportement nécessaire peut être ennuyeux dès la première fois. Cependant, à partir de maintenant, vous pouvez le réutiliser sur chaque formulaire dont vous avez besoin par un extrait XAML à une ligne exacte. Et si nécessaire, vous pouvez l'extraire en tant qu'assemblage séparé afin qu'il puisse être inclus dans le prochain projet que vous souhaitez.

Yury Schkatula
la source
0

Pourquoi ne pas simplement passer la fenêtre en paramètre de commande?

C #:

 private void Cancel( Window window )
  {
     window.Close();
  }

  private ICommand _cancelCommand;
  public ICommand CancelCommand
  {
     get
     {
        return _cancelCommand ?? ( _cancelCommand = new Command.RelayCommand<Window>(
                                                      ( window ) => Cancel( window ),
                                                      ( window ) => ( true ) ) );
     }
  }

XAML:

<Window x:Class="WPFRunApp.MainWindow"
        x:Name="_runWindow"
...
   <Button Content="Cancel"
           Command="{Binding Path=CancelCommand}"
           CommandParameter="{Binding ElementName=_runWindow}" />
chrislarson
la source
Je ne pense pas que ce soit une bonne idée de restreindre la machine virtuelle à un type de fenêtre.
Shimmy Weitzhandler
2
Je ne pense pas que ce soit une bonne idée de restreindre la VM à un Windowtype qui n'est pas du tout "MVVM" pur. Voir cette réponse, où la machine virtuelle n'est pas limitée à un Windowobjet.
Shimmy Weitzhandler
de cette façon, la dépendance est mise sur un bouton qui ne peut pas toujours être la situation. Passer également le type d'interface utilisateur à ViewModel est une mauvaise pratique.
Kylo Ren
0

Une autre solution consiste à créer une propriété avec INotifyPropertyChanged dans View Model comme DialogResult, puis dans Code Behind, écrivez ceci:

public class SomeWindow: ChildWindow
{
    private SomeViewModel _someViewModel;

    public SomeWindow()
    {
        InitializeComponent();

        this.Loaded += SomeWindow_Loaded;
        this.Closed += SomeWindow_Closed;
    }

    void SomeWindow_Loaded(object sender, RoutedEventArgs e)
    {
        _someViewModel = this.DataContext as SomeViewModel;
        _someViewModel.PropertyChanged += _someViewModel_PropertyChanged;
    }

    void SomeWindow_Closed(object sender, System.EventArgs e)
    {
        _someViewModel.PropertyChanged -= _someViewModel_PropertyChanged;
        this.Loaded -= SomeWindow_Loaded;
        this.Closed -= SomeWindow_Closed;
    }

    void _someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == SomeViewModel.DialogResultPropertyName)
        {
            this.DialogResult = _someViewModel.DialogResult;
        }
    }
}

Le fragment le plus important est _someViewModel_PropertyChanged. DialogResultPropertyNamepeut être une chaîne de const publique SomeViewModel.

J'utilise ce genre d'astuce pour apporter des modifications dans les contrôles de vue au cas où cela serait difficile à faire dans ViewModel. OnPropertyChanged dans ViewModel, vous pouvez faire tout ce que vous voulez dans View. ViewModel est toujours «testable à l'unité» et quelques petites lignes de code dans le code ne font aucune différence.

sliwinski.lukas
la source
0

J'irais comme ça:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;    
using GalaSoft.MvvmLight.Messaging; 

// View

public partial class TestCloseWindow : Window
{
    public TestCloseWindow() {
        InitializeComponent();
        Messenger.Default.Register<CloseWindowMsg>(this, (msg) => Close());
    }
}

// View Model

public class MainViewModel: ViewModelBase
{
    ICommand _closeChildWindowCommand;

    public ICommand CloseChildWindowCommand {
        get {
            return _closeChildWindowCommand?? (_closeChildWindowCommand = new RelayCommand(() => {
                Messenger.Default.Send(new CloseWindowMsg());
        }));
        }
    }
}

public class CloseWindowMsg
{
}
romanoza
la source
0

J'ai lu toutes les réponses mais je dois dire que la plupart d'entre elles ne sont tout simplement pas assez bonnes ou même pires.

Vous pouvez gérer cela magnifiquement avec la classe DialogService dont la responsabilité est d'afficher la fenêtre de dialogue et de renvoyer le résultat de la boîte de dialogue. J'ai créé un exemple de projet démontrant sa mise en œuvre et son utilisation.

voici les parties les plus importantes:

//we will call this interface in our viewmodels
public interface IDialogService
{
    bool? ShowDialog(object dialogViewModel, string caption);
}

//we need to display logindialog from mainwindow
public class MainWindowViewModel : ViewModelBase
{
    public string Message {get; set;}
    public void ShowLoginCommandExecute()
    {
        var loginViewModel = new LoginViewModel();
        var dialogResult = this.DialogService.ShowDialog(loginViewModel, "Please, log in");

        //after dialog is closed, do someting
        if (dialogResult == true && loginViewModel.IsLoginSuccessful)
        {
            this.Message = string.Format("Hello, {0}!", loginViewModel.Username);
        }
    }
}


public class DialogService : IDialogService
{
    public bool? ShowDialog(object dialogViewModel, string caption)
    {
        var contentView = ViewLocator.GetView(dialogViewModel);
        var dlg = new DialogWindow
        {
            Title = caption
        };
        dlg.PART_ContentControl.Content = contentView;

        return dlg.ShowDialog();
    }
}

N'est-ce pas simplement plus simple? plus simple, plus lisible et enfin mais non moins facile à déboguer que EventAggregator ou d'autres solutions similaires?

comme vous pouvez le voir, dans mes modèles de vue, j'ai utilisé la première approche de ViewModel décrite dans mon article ici: Meilleure pratique pour appeler View depuis ViewModel dans WPF

Bien sûr, dans le monde réel, les DialogService.ShowDialogdoivent avoir plus d'options pour configurer la boîte de dialogue, par exemple les boutons et les commandes à exécuter. Il y a différentes façons de le faire, mais c'est hors de portée :)

Liero
la source
0

Bien que cela ne réponde pas à la question de savoir comment le faire via le viewmodel, cela montre comment le faire en utilisant uniquement XAML + le SDK de mélange.

J'ai choisi de télécharger et d'utiliser deux fichiers à partir du SDK Blend, que vous pouvez tous deux sous forme de package de Microsoft via NuGet. Les fichiers sont:

System.Windows.Interactivity.dll et Microsoft.Expression.Interactions.dll

Microsoft.Expression.Interactions.dll vous offre de belles fonctionnalités telles que la possibilité de définir des propriétés ou d'appeler une méthode sur votre modèle de vue ou autre cible et contient également d'autres widgets.

Certains XAML:

<Window x:Class="Blah.Blah.MyWindow"
    ...
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
  ...>
 <StackPanel>
    <Button x:Name="OKButton" Content="OK">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="True"
                      IsEnabled="{Binding SomeBoolOnTheVM}" />                                
          </i:EventTrigger>
    </Button>
    <Button x:Name="CancelButton" Content="Cancel">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="False" />                                
          </i:EventTrigger>
    </Button>

    <Button x:Name="CloseButton" Content="Close">
       <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <!-- method being invoked should be void w/ no args -->
                    <ei:CallMethodAction
                        TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                        MethodName="Close" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
    </Button>
 <StackPanel>
</Window>

Notez que si vous optez simplement pour un comportement OK / Annuler simple, vous pouvez vous en sortir en utilisant les propriétés IsDefault et IsCancel tant que la fenêtre est affichée avec Window.ShowDialog ().
J'ai personnellement eu des problèmes avec un bouton dont la propriété IsDefault était définie sur true, mais elle était masquée lors du chargement de la page. Il ne semblait pas vouloir jouer correctement après avoir été affiché, donc je définis simplement la propriété Window.DialogResult comme indiqué ci-dessus à la place et cela fonctionne pour moi.

Nous s
la source
0

Voici la solution simple sans bug (avec le code source), ça marche pour moi.

  1. Dérivez votre ViewModel de INotifyPropertyChanged

  2. Créer une propriété observable CloseDialog dans ViewModel

    public void Execute()
    {
        // Do your task here
    
        // if task successful, assign true to CloseDialog
        CloseDialog = true;
    }
    
    private bool _closeDialog;
    public bool CloseDialog
    {
        get { return _closeDialog; }
        set { _closeDialog = value; OnPropertyChanged(); }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    private void OnPropertyChanged([CallerMemberName]string property = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    }

  3. Attacher un gestionnaire dans View pour cette modification de propriété

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Vous avez maintenant presque terminé. Dans le gestionnaire d'événements,DialogResult = true

    protected void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == "CloseDialog")
        {
            DialogResult = true;
        }
    }
Anil8753
la source
0

Créez un Dependency Propertydans votre View/ any UserControl(ou Windowvous souhaitez fermer). Comme ci-dessous:

 public bool CloseTrigger
        {
            get { return (bool)GetValue(CloseTriggerProperty); }
            set { SetValue(CloseTriggerProperty, value); }
        }

        public static readonly DependencyProperty CloseTriggerProperty =
            DependencyProperty.Register("CloseTrigger", typeof(bool), typeof(ControlEventBase), new PropertyMetadata(new PropertyChangedCallback(OnCloseTriggerChanged)));

        private static void OnCloseTriggerChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            //write Window Exit Code
        }

Et liez-le à partir de la propriété de votre ViewModel :

<Window x:Class="WpfStackOverflowTempProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"  Width="525"
        CloseTrigger="{Binding Path=CloseWindow,Mode=TwoWay}"

Propriété dans VeiwModel:

private bool closeWindow;

    public bool CloseWindow
    {
        get { return closeWindow; }
        set 
        { 
            closeWindow = value;
            RaiseChane("CloseWindow");
        }
    }

Déclenchez maintenant l'opération de fermeture en modifiant la CloseWindowvaleur dans ViewModel. :)

Kylo Ren
la source
-2

Où vous devez fermer la fenêtre, mettez-la simplement dans le modèle de vue:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }
Cătălin Rădoi
la source
Un ViewModel ne doit en aucun cas contenir un UIElement , car cela peut créer des bogues
WiiMaxx
Que se passe-t-il si DataContext est hérité de plusieurs fenêtres?
Kylo Ren
ta-da, ce n'est pas du tout MVVM.
Alexandru Dicu
-10
Application.Current.MainWindow.Close() 

C'est assez!

Alexey
la source
3
-1 Seulement vrai si la fenêtre que vous voulez fermer est la fenêtre principale ... Hypothèse très improbable pour la boîte de dialogue de connexion ...
surfen