Obsługa zdarzenia zamknięcia okna za pomocą zestawu narzędzi WPF / MVVM Light

145

Chciałbym obsłużyć Closingzdarzenie (gdy użytkownik kliknie prawy górny przycisk „X”) mojego okna, aby ostatecznie wyświetlić komunikat potwierdzający lub / i anulować zamknięcie.

Wiem, jak to zrobić w kodzie: zapisz się na Closingzdarzenie okna, a następnie użyj CancelEventArgs.Cancelwłaściwości.

Ale używam MVVM, więc nie jestem pewien, czy to dobre podejście.

Myślę, że dobrym podejściem byłoby powiązanie Closingzdarzenia z a Commandw moim ViewModel.

Próbowałem tego:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding CloseCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

Z skojarzonym RelayCommandw moim ViewModelu, ale nie działa (kod polecenia nie jest wykonywany).

Olivier Payen
źródło
3
Zainteresowany również miłą odpowiedzią na to pytanie.
Sekhat,
3
Pobrałem kod z codeplex i debugowanie ujawniło: „Nie można rzutować obiektu typu„ System.ComponentModel.CancelEventArgs ”na typ„ System.Windows.RoutedEventArgs ”. Działa dobrze, jeśli nie chcesz CancelEventArgs, ale to nie odpowiada na twoje pytanie ...
David Hollinshead
Zgaduję, że twój kod nie działa, ponieważ formant, do którego dołączyłeś wyzwalacz, nie ma zdarzenia zamknięcia. Kontekst danych nie jest oknem ... To prawdopodobnie szablon danych z siatką lub czymś podobnym, który nie ma zdarzenia Closing. Zatem odpowiedź dbkk jest najlepszą odpowiedzią w tym przypadku. Jednak wolę podejście Interaction / EventTrigger, gdy zdarzenie jest dostępne.
NielW,
Na przykład kod, który masz, będzie działał dobrze w przypadku zdarzenia Loaded.
NielW

Odpowiedzi:

126

Po prostu skojarzyłbym handler w konstruktorze View:

MyWindow() 
{
    // Set up ViewModel, assign to DataContext etc.
    Closing += viewModel.OnWindowClosing;
}

Następnie dodaj procedurę obsługi do ViewModel:

using System.ComponentModel;

public void OnWindowClosing(object sender, CancelEventArgs e) 
{
   // Handle closing logic, set e.Cancel as needed
}

W takim przypadku nie zyskujesz dokładnie nic poza złożonością, używając bardziej złożonego wzorca z większą liczbą pośrednią (5 dodatkowych wierszy XAML plus Commandwzorzec).

Mantra „za kodem zerowym” nie jest celem samym w sobie, chodzi o oddzielenie ViewModel od widoku . Nawet jeśli zdarzenie jest powiązane w kodzie z widokiem View, ViewModelnie zależy od widoku, a logika zamykająca może być testowana jednostkowo .

dbkk
źródło
4
Podoba mi się to rozwiązanie: wystarczy podłączyć ukryty przycisk :)
Benjol
3
Dla początkujących mvvm, którzy nie używają MVVMLight i szukają sposobu informowania ViewModel o zdarzeniu Closing, linki, jak poprawnie skonfigurować dataContext i jak uzyskać obiekt viewModel w widoku mogą być interesujące. Jak uzyskać odwołanie do ViewModel w widoku? i jak ustawić ViewModel w oknie w XAML przy użyciu właściwości datacontext ... Zajęło mi kilka godzin, jak można obsłużyć proste zdarzenie zamknięcia okna w ViewModel.
MarkusEgle
18
To rozwiązanie nie ma znaczenia w środowisku MVVM. Kod znajdujący się za nim nie powinien wiedzieć o ViewModel.
Jacob
2
@Jacob Myślę, że problem polega na tym, że w ViewModel otrzymujesz program obsługi zdarzeń formularza, który łączy ViewModel z określoną implementacją interfejsu użytkownika. Jeśli zamierzają użyć kodu w tle, powinni sprawdzić CanExecute, a następnie zamiast tego wywołać Execute () na właściwości ICommand.
Evil Pigeon
14
@Jacob Kod związany z kodem może dobrze wiedzieć o członkach ViewModel, tak jak robi to kod XAML. Albo co myślisz, że robisz podczas tworzenia powiązania z właściwością ViewModel? To rozwiązanie jest idealne dla MVVM, o ile nie obsługujesz logiki zamykającej w samym kodzie, ale w ViewModel (chociaż użycie ICommand, jak sugeruje EvilPigeon, może być dobrym pomysłem, ponieważ możesz również powiązać to it)
almulo
81

Ten kod działa dobrze:

ViewModel.cs:

public ICommand WindowClosing
{
    get
    {
        return new RelayCommand<CancelEventArgs>(
            (args) =>{
                     });
    }
}

i w XAML:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <command:EventToCommand Command="{Binding WindowClosing}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers>

przy założeniu, że:

  • ViewModel jest przypisany do DataContextgłównego kontenera.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Staś
źródło
1
Zapomniałem: aby pobrać argumenty zdarzenia w poleceniu, użyj PassEventArgsToCommand = "True"
Stas
2
+1 proste i konwencjonalne podejście. Byłoby jeszcze lepiej udać się do PRISM.
Tri Q Tran
16
Jest to jeden scenariusz, w którym wyróżniono ziejące dziury w WPF i MVVM.
Damien
1
Byłoby naprawdę pomocne wspomnieć, co jest iw <i:Interaction.Triggers>środku i jak to zdobyć.
Andrii Muzychuk
1
@Chiz, to przestrzeń nazw, którą należy zadeklarować w elemencie głównym w następujący sposób: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
34

Ta opcja jest jeszcze łatwiejsza i może być dla Ciebie odpowiednia. W konstruktorze modelu widoku możesz zasubskrybować zdarzenie zamykające okno główne w następujący sposób:

Application.Current.MainWindow.Closing += new CancelEventHandler(MainWindow_Closing);

void MainWindow_Closing(object sender, CancelEventArgs e)
{
            //Your code to handle the event
}

Wszystkiego najlepszego.

PILuaces
źródło
To najlepsze rozwiązanie spośród innych wymienionych w tym numerze. Dziękuję Ci !
Jacob
To jest to, czego szukałem. Dzięki!
Nikki Punjabi
20
... a to tworzy ścisłe powiązanie między ViewModel i View. -1.
PiotrK
6
To nie jest najlepsza odpowiedź. Łamie MVVM.
Safiron
1
@Craig Wymaga twardego odniesienia do okna głównego lub dowolnego okna, w którym jest używany. Jest to znacznie łatwiejsze, ale oznacza, że ​​model widoku nie jest odłączony. Nie jest to kwestia zadowolenia nerdów MVVM, czy nie, ale jeśli wzorzec MVVM musi zostać złamany, aby działał, nie ma sensu go w ogóle używać.
Alex
16

Oto odpowiedź zgodnie ze wzorcem MVVM, jeśli nie chcesz wiedzieć o oknie (lub jakimkolwiek jego zdarzeniu) w ViewModel.

public interface IClosing
{
    /// <summary>
    /// Executes when window is closing
    /// </summary>
    /// <returns>Whether the windows should be closed by the caller</returns>
    bool OnClosing();
}

W ViewModel dodaj interfejs i implementację

public bool OnClosing()
{
    bool close = true;

    //Ask whether to save changes och cancel etc
    //close = false; //If you want to cancel close

    return close;
}

W oknie dodaję wydarzenie zamknięcia. Ten kod nie przerywa wzorca MVVM. Widok może wiedzieć o modelu widoku!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
AxdorphCoder
źródło
Prosty, przejrzysty i czysty. ViewModel nie musi znać specyfiki widoku, dlatego obawy pozostają oddzielone.
Bernhard Hiller
kontekst jest zawsze pusty!
Shahid Od
@ShahidOd Twój ViewModel musi zaimplementować IClosinginterfejs, a nie tylko zaimplementować OnClosingmetodę. W przeciwnym razie DataContext as IClosingobsada się nie powiedzie i powrócinull
Erik White
10

Rany, wygląda na to, że dzieje się tutaj dużo kodu. Staś powyżej miał właściwe podejście przy minimalnym wysiłku. Oto moja adaptacja (używająca MVVMLight, ale powinna być rozpoznawalna) ... Och, a PassEventArgsToCommand = "True" jest zdecydowanie potrzebne, jak wskazano powyżej.

(Podziękowania dla Laurent Bugnion http://blog.galasoft.ch/archive/2009/10/18/clean-shutdown-in-silverlight-and-wpf-applications.aspx )

   ... MainWindow Xaml
   ...
   WindowStyle="ThreeDBorderWindow" 
    WindowStartupLocation="Manual">



<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding WindowClosingCommand}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers> 

W modelu widoku:

///<summary>
///  public RelayCommand<CancelEventArgs> WindowClosingCommand
///</summary>
public RelayCommand<CancelEventArgs> WindowClosingCommand { get; private set; }
 ...
 ...
 ...
        // Window Closing
        WindowClosingCommand = new RelayCommand<CancelEventArgs>((args) =>
                                                                      {
                                                                          ShutdownService.MainWindowClosing(args);
                                                                      },
                                                                      (args) => CanShutdown);

w ShutdownService

    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void MainWindowClosing(CancelEventArgs e)
    {
        e.Cancel = true;  /// CANCEL THE CLOSE - let the shutdown service decide what to do with the shutdown request
        RequestShutdown();
    }

RequestShutdown wygląda mniej więcej tak, jak poniżej, ale w zasadzieRequestShutdown lub jakakolwiek inna nazwa decyduje o zamknięciu aplikacji, czy nie (co i tak wesoło zamknie okno):

...
...
...
    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void RequestShutdown()
    {

        // Unless one of the listeners aborted the shutdown, we proceed.  If they abort the shutdown, they are responsible for restarting it too.

        var shouldAbortShutdown = false;
        Logger.InfoFormat("Application starting shutdown at {0}...", DateTime.Now);
        var msg = new NotificationMessageAction<bool>(
            Notifications.ConfirmShutdown,
            shouldAbort => shouldAbortShutdown |= shouldAbort);

        // recipients should answer either true or false with msg.execute(true) etc.

        Messenger.Default.Send(msg, Notifications.ConfirmShutdown);

        if (!shouldAbortShutdown)
        {
            // This time it is for real
            Messenger.Default.Send(new NotificationMessage(Notifications.NotifyShutdown),
                                   Notifications.NotifyShutdown);
            Logger.InfoFormat("Application has shutdown at {0}", DateTime.Now);
            Application.Current.Shutdown();
        }
        else
            Logger.InfoFormat("Application shutdown aborted at {0}", DateTime.Now);
    }
    }
AllenM
źródło
8

Pytający powinien użyć odpowiedzi STAS, ale czytelnicy, którzy używają pryzmatu i nie mają galasoft / mvvmlight, mogą chcieć wypróbować to, czego użyłem:

W definicji u góry okna lub kontroli użytkownika itp. Zdefiniuj przestrzeń nazw:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

A tuż pod tą definicją:

<i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding WindowClosing}" CommandParameter="{Binding}" />
        </i:EventTrigger>
</i:Interaction.Triggers>

Właściwość w Twoim modelu widoku:

public ICommand WindowClosing { get; private set; }

Dołącz delegatecommand w konstruktorze ViewModel:

this.WindowClosing = new DelegateCommand<object>(this.OnWindowClosing);

Wreszcie kod, do którego chcesz dotrzeć po zamknięciu kontrolki / okna / cokolwiek:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Chris
źródło
3
Nie daje to dostępu do CancelEventArgs, który jest niezbędny do anulowania zdarzenia zamykającego. Przekazanym obiektem jest model widoku, który technicznie jest tym samym modelem widoku, z którego jest wykonywane polecenie WindowClosing.
stephenbayer
4

Kusiło mnie, aby użyć programu obsługi zdarzeń w pliku App.xaml.cs, który pozwoli Ci zdecydować, czy zamknąć aplikację, czy nie.

Na przykład w pliku App.xaml.cs możesz mieć coś takiego jak następujący kod:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    // Create the ViewModel to attach the window to
    MainWindow window = new MainWindow();
    var viewModel = new MainWindowViewModel();

    // Create the handler that will allow the window to close when the viewModel asks.
    EventHandler handler = null;
    handler = delegate
    {
        //***Code here to decide on closing the application****
        //***returns resultClose which is true if we want to close***
        if(resultClose == true)
        {
            viewModel.RequestClose -= handler;
            window.Close();
        }
    }
    viewModel.RequestClose += handler;

    window.DataContaxt = viewModel;

    window.Show();

}

Następnie w kodzie MainWindowViewModel możesz mieć następujące elementy:

#region Fields
RelayCommand closeCommand;
#endregion

#region CloseCommand
/// <summary>
/// Returns the command that, when invoked, attempts
/// to remove this workspace from the user interface.
/// </summary>
public ICommand CloseCommand
{
    get
    {
        if (closeCommand == null)
            closeCommand = new RelayCommand(param => this.OnRequestClose());

        return closeCommand;
    }
}
#endregion // CloseCommand

#region RequestClose [event]

/// <summary>
/// Raised when this workspace should be removed from the UI.
/// </summary>
public event EventHandler RequestClose;

/// <summary>
/// If requested to close and a RequestClose delegate has been set then call it.
/// </summary>
void OnRequestClose()
{
    EventHandler handler = this.RequestClose;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

#endregion // RequestClose [event]
ChrisBD
źródło
1
Dzięki za szczegółową odpowiedź. Nie sądzę jednak, aby to rozwiązało mój problem: muszę obsługiwać zamykanie okna, gdy użytkownik kliknie prawy górny przycisk „X”. Byłoby to łatwe do zrobienia w kodzie (po prostu połączyłbym zdarzenie Closing i ustawiłem CancelEventArgs.Cancel na true lub false), ale chciałbym to zrobić w stylu MVVM. Przepraszam za zamieszanie
Olivier Payen
1

Zasadniczo zdarzenie okna nie może być przypisane do MVVM. Ogólnie rzecz biorąc, przycisk Zamknij powoduje wyświetlenie okna dialogowego z zapytaniem użytkownika „zapisz: tak / nie / anuluj”, co może nie zostać osiągnięte przez MVVM.

Możesz zachować procedurę obsługi zdarzenia OnClosing, w której wywołujesz Model.Close.CanExecute () i ustawia się wynik boolowski we właściwości zdarzenia. Dlatego po wywołaniu CanExecute (), jeśli true, LUB w zdarzeniu OnClosed, wywołaj Model.Close.Execute ()

Echtelion
źródło
1

Nie wykonałem zbyt wielu testów, ale wydaje się, że działa. Oto, co wymyśliłem:

namespace OrtzIRC.WPF
{
    using System;
    using System.Windows;
    using OrtzIRC.WPF.ViewModels;

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private MainViewModel viewModel = new MainViewModel();
        private MainWindow window = new MainWindow();

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

            viewModel.RequestClose += ViewModelRequestClose;

            window.DataContext = viewModel;
            window.Closing += Window_Closing;
            window.Show();
        }

        private void ViewModelRequestClose(object sender, EventArgs e)
        {
            viewModel.RequestClose -= ViewModelRequestClose;
            window.Close();
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            window.Closing -= Window_Closing;
            viewModel.RequestClose -= ViewModelRequestClose; //Otherwise Close gets called again
            viewModel.CloseCommand.Execute(null);
        }
    }
}
Brian Ortiz
źródło
1
Co stanie się w przypadku scenariusza, w którym maszyna wirtualna chce anulować zamknięcie?
Tri Q Tran
1

Korzystanie z zestawu narzędzi MVVM Light:

Zakładając, że w modelu widoku jest polecenie Wyjście :

ICommand _exitCommand;
public ICommand ExitCommand
{
    get
    {
        if (_exitCommand == null)
            _exitCommand = new RelayCommand<object>(call => OnExit());
        return _exitCommand;
    }
}

void OnExit()
{
     var msg = new NotificationMessageAction<object>(this, "ExitApplication", (o) =>{});
     Messenger.Default.Send(msg);
}

Odbiera się to w widoku:

Messenger.Default.Register<NotificationMessageAction<object>>(this, (m) => if (m.Notification == "ExitApplication")
{
     Application.Current.Shutdown();
});

Z kolei Closingzdarzenie obsługuję w MainWindow, korzystając z instancji ViewModel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ 
    if (((ViewModel.MainViewModel)DataContext).CancelBeforeClose())
        e.Cancel = true;
}

CancelBeforeClose sprawdza aktualny stan modelu widoku i zwraca wartość true, jeśli zamykanie powinno zostać zatrzymane.

Mam nadzieję, że to komuś pomoże.

Ron
źródło
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Mattias Sturebrand
źródło
Cześć, dodaj trochę wyjaśnienia wraz z kodem, ponieważ pomaga to zrozumieć kod. Odpowiedzi zawierające tylko kod są
źle widziane
Operator wyraźnie stwierdził, że nie jest zainteresowany wykorzystaniem do tego kodu zdarzenia związanego z kodem.
Fer García,