une - passer d'un form à un autre en c#




Comment ViewModel doit-il fermer le formulaire? (17)

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 (handling-dialogs-in-wpf-with-mvvm) ...

J'ai un "Login" formulaire écrit en utilisant le modèle MVVM.

Ce formulaire a un ViewModel qui contient le nom d'utilisateur et mot de passe, qui sont liés à la vue dans le XAML en utilisant des 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 se déclenche et envoie des données sur le réseau pour se connecter. Lorsque cette fonction est terminée, il y a 2 actions:

  1. La connexion était invalide - nous montrons simplement un MessageBox et tout va bien

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

Le problème est, le ViewModel ne sait rien sur la vue réelle, alors comment peut-il fermer la vue et lui dire de retourner un DialogResult particulier ?? Je pourrais coller du code dans le CodeBehind, et / ou passer la View à ViewModel, mais il semblerait que cela viderait complètement le point de MVVM ...

Mettre à jour

À la fin je viens de violer la "pureté" du modèle MVVM et j'ai vu la View publier un événement Closed , et exposer une méthode Close . 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 obtient en résolvant un problème tout en étant «pur», je ne suis pas le seul à penser que 200 lignes d'événements, de commandes et de comportements ne servent qu'à éviter une seule ligne le nom de "motifs" et de "pureté" est un peu ridicule ....


Bien que cela ne répond pas à la question de savoir comment faire cela via le viewmodel, cela montre comment le faire en utilisant seulement XAML + le SDK blend.

J'ai choisi de télécharger et d'utiliser deux fichiers du Blend SDK, que vous pouvez tous les deux en tant que paquet de Microsoft à travers 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 une propriété ou d'invoquer une méthode sur votre viewmodel ou une autre cible et d'autres widgets à l'intérieur.

Du 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>

Note that if you're just going for simple OK/Cancel behavior, you can get away w/ using the IsDefault and IsCancel properties as long as the window is shown w/ Window.ShowDialog().
I personally had problems w/ a button that had the IsDefault property set to true, but it was hidden when the page is loaded. It didn't seem to want to play nicely after it was shown, so I just am setting the Window.DialogResult property as shown above instead and it works for me.


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 n'arrive pas à comprendre comment créer une application sans dialogues (c'est peut-être juste un bloc d'esprit). J'étais donc dans une impasse avec MVVM et montrant un dialogue. Donc je suis tombé sur cet article de CodeProject:

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

Ce qui est un UserControl qui permet essentiellement à une fenêtre d'être dans l'arborescence visuelle d'une autre fenêtre (non autorisée dans xaml). Il expose également un DependencyProperty booléen appelé IsShowing.

Vous pouvez définir un style comme, typiquement dans un resourcedictionary, qui affiche essentiellement la boîte de dialogue chaque fois que la propriété Content du contrôle! = Null via des triggers:

<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 voulez afficher la boîte de dialogue, vous avez simplement ceci:

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

Et dans votre ViewModel tout ce que vous avez à faire est de définir la propriété à une valeur (Note: la classe ViewModel doit supporter INotifyPropertyChanged pour que la vue sache que quelque chose s'est passé).

ainsi:

DialogViewModel = new DisplayViewModel();

Pour faire correspondre le ViewModel avec la vue, vous devriez avoir quelque chose comme ça dans un resourcedictionary:

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

Avec tout cela, vous obtenez un code un-ligne pour afficher le 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 placer un événement dans une classe de base ViewModel dont DisplayViewModel hérite et remplace le code ci-dessus.

        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 ça aide, ça m'a sauvé.


De mon point de vue, la question est assez bonne car la même approche serait utilisée non seulement pour la fenêtre "Login", mais pour n'importe quel type d'entre eux. J'ai reçu beaucoup de suggestions et personne ne me convient. S'il vous plaît voir mon genre, qui a été tiré de l' article de modèle de conception MVVM .

Chaque classe ViewModel doit être héritée de WorkspaceViewModel qui a RequestClose envent, et la propriété ICommand type ICommand . L'implémentation par défaut de la propriété CloseCommand l' CloseCommand .

Et pour fermer la fenêtre, la méthode OnLoaded de votre fenêtre devrait être écrasée:

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

ou méthode OnStartup 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 l'événement CloseCommand et l' CloseCommand propriété CloseCommand dans WorkspaceViewModel sont assez clairs, mais je leur montrerai qu'ils sont cohérents:

public abstract class WorkspaceViewModel : ViewModelBase // There are nothing interest in ViewModelBase, it only implements INotifyPropertyChanged interface only
{
    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 traite pas mal pour ces sources, Si j'avais hier ça me sauverait quelques heures ...

PPS Tous les commentaires ou suggestions sont les bienvenus.


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;
        }
    }

J'ai été inspiré par la réponse de Thejuan pour écrire une propriété attachée plus simple. Aucun style, aucun déclencheur; Au lieu de cela, 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 l'avait fait correctement et a fait de DialogResult une propriété de dépendance en premier lieu. Juste mettre un bool? DialogResult bool? DialogResult propriété sur votre ViewModel et implémenter INotifyPropertyChanged, et voilà, votre ViewModel peut fermer la fenêtre (et définir son DialogResult) juste 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 aussi posté ceci sur mon blog .


J'ai fini par mélanger la réponse de Joe White et un peu de code de la réponse d' Adam Mills , puisque j'avais besoin de 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 de l'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 de l'utilisateur s'il n'était pas attaché à 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_ );
    }
  }
}

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 pire.

Vous pouvez gérer cela avec classe DialogService dont la responsabilité est d'afficher la boîte de dialogue et de retourner 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 facile, plus lisible et enfin, mais pas moins facile à déboguer que EventAggregator ou d'autres solutions similaires?

Comme vous pouvez le voir, Dans ma vue les modèles, 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, DialogService.ShowDialog doit avoir plus d'options pour configurer la boîte de dialogue, par exemple les boutons et les commandes qu'ils doivent exécuter. Il y a différentes façons de le faire, mais c'est hors de portée :)



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

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 peut-être difficile à tester (car il est difficile de se moquer / stub une statique), mais il est plus propre (IMHO) que les autres solutions.

Erick


La façon dont je le manipulerais est d'ajouter un gestionnaire d'événement dans mon ViewModel. Lorsque l'utilisateur a été connecté avec succès je déclencherais l'événement. Dans ma vue, je voudrais attacher à cet événement et quand il a tiré, je fermerais la fenêtre.


Pour info, je suis tombé sur ce même problème et je pense avoir trouvé un travail qui n'exige pas de globalité ou de statique, bien que ce ne soit pas la meilleure réponse. Je laisse les gars décider par vous-même.

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

Donc ce que j'ai fait était de créer une propriété sur le LoginFormViewModel qui était de type ICommand (Appelons cela CloseWindowCommand). Ensuite, avant d'appeler .ShowDialog () sur la fenêtre, j'ai défini la propriété CloseWindowCommand sur le LoginFormViewModel à la méthode window.Close () de la fenêtre I instanciée. Ensuite, à l'intérieur du 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! :)


Pourquoi ne pas simplement passer la fenêtre comme 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}" />

Voici ce que j'ai fait au début, qui fonctionne, mais il semble plutôt long et moche (tout ce qui est statique 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);
}

J'ai ensuite supprimé tout ce code, et juste eu le LoginFormViewModel appeler la méthode Close sur son affichage. Il a fini par être beaucoup plus agréable et plus facile à suivre. IMHO le point de modèles est de donner aux gens un moyen plus facile de comprendre ce que fait votre application, et dans ce cas, MVVM rendait beaucoup plus difficile à comprendre que si je ne l'avais pas utilisé, et était maintenant un anti- modèle.


Vous pouvez faire en sorte que ViewModel expose un événement auquel la vue s'inscrit. Ensuite, lorsque le ViewModel décide de l'heure de fermer la vue, il déclenche cet événement qui provoque la fermeture de la vue. Si vous souhaitez qu'une valeur de résultat spécifique soit renvoyée, vous disposez alors d'une propriété dans ViewModel.


Create a Dependency Property in your View /any UserControl (or Window you want to close). 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
        }

And bind it from your ViewModel's property :

<Window x:Class="WpfTempProject.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}"

Property In VeiwModel :

private bool closeWindow;

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

Now trigger the close operation by changing the CloseWindow value in ViewModel. :)


Here is the simple bug free solution (with source code), It is working for me.

  1. Derive your ViewModel from INotifyPropertyChanged

  2. Create a observable property CloseDialog in 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. Attach a Handler in View for this property change

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
    
  4. Now you are almost done. In the event handler make DialogResult = true

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

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();
  }
}






mvvm