Menangani acara penutupan jendela dengan WPF / MVVM Light Toolkit

145

Saya ingin menangani Closingacara (ketika pengguna mengklik tombol 'X' kanan atas) dari jendela saya untuk akhirnya menampilkan pesan konfirmasi atau / dan membatalkan penutupan.

Saya tahu bagaimana melakukan ini di kode-belakang: berlangganan Closingacara jendela kemudian gunakan CancelEventArgs.Cancelproperti.

Tapi saya menggunakan MVVM jadi saya tidak yakin itu pendekatan yang baik.

Saya pikir pendekatan yang baik adalah dengan mengikat Closingacara ke Commanddalam ViewModel saya.

Saya mencobanya:

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

Dengan yang terkait RelayCommanddalam ViewModel saya tetapi tidak berfungsi (kode perintah tidak dijalankan).

Olivier Payen
sumber
3
Juga tertarik pada jawaban yang bagus untuk menjawab ini.
Sekhat
3
Saya mengunduh kode dari codeplex dan debugging mengungkapkan: "Tidak dapat membuang objek tipe 'System.ComponentModel.CancelEventArgs' untuk mengetik 'System.Windows.RoutedEventArgs'." Ini berfungsi dengan baik jika Anda tidak ingin CancelEventArgs tetapi itu tidak menjawab pertanyaan Anda ...
David Hollinshead
Saya menduga kode Anda tidak berfungsi karena kontrol yang Anda lampirkan pemicu tidak memiliki acara Penutupan. Konteks data Anda bukan jendela ... Ini mungkin templat data dengan kisi atau sesuatu, yang tidak memiliki acara Penutupan. Jadi jawaban dbkk adalah jawaban terbaik dalam kasus ini. Namun, saya lebih suka pendekatan Interaksi / EventTrigger ketika acara tersedia.
NielW
Kode yang Anda miliki akan berfungsi dengan baik pada acara Loaded, misalnya.
NielW

Jawaban:

126

Saya hanya akan mengasosiasikan pawang di konstruktor View:

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

Kemudian tambahkan handler ke ViewModel:

using System.ComponentModel;

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

Dalam hal ini, Anda tidak mendapatkan apa pun kecuali kompleksitas dengan menggunakan pola yang lebih rumit dengan lebih banyak tipuan (5 garis tambahan Commandpola XAML plus ).

Mantra "nol kode di belakang" bukanlah tujuan itu sendiri, intinya adalah untuk memisahkan ViewModel dari View . Bahkan ketika peristiwa terikat dalam kode di belakang tampilan, ViewModeltidak tergantung pada tampilan dan logika penutupan dapat diuji unit .

dbkk
sumber
4
Saya suka solusi ini : cukup kaitkan ke tombol tersembunyi :)
Benjol
3
Untuk pemula mvvm yang tidak menggunakan MVVMLight dan mencari cara untuk menginformasikan ViewModel tentang acara Penutupan, tautan cara mengatur dataContext dengan benar dan cara mendapatkan objek viewModel dalam Tampilan mungkin menarik. Bagaimana cara mendapatkan referensi ke Model View di View? dan Bagaimana cara mengatur ViewModel pada jendela di xaml menggunakan properti datacontext ... Butuh waktu beberapa jam, bagaimana acara penutupan jendela sederhana dapat ditangani di ViewModel.
MarkusEgle
18
Solusi ini tidak relevan di lingkungan MVVM. Kode di belakang seharusnya tidak tahu tentang ViewModel.
Yakub
2
@ Jacob Saya pikir masalahnya adalah Anda mendapatkan formulir event handler di ViewModel Anda, yang memasangkan ViewModel ke implementasi UI tertentu. Jika mereka akan menggunakan kode di belakang, mereka harus memeriksa CanExecute dan kemudian memanggil Execute () pada properti ICommand sebagai gantinya.
Evil Pigeon
14
@ Jacob Kode di belakang dapat mengetahui tentang anggota ViewModel baik-baik saja, seperti halnya kode XAML. Atau apa yang Anda pikir Anda lakukan saat membuat Binding ke properti ViewModel? Solusi ini baik-baik saja untuk MVVM, selama Anda tidak menangani logika penutupan di belakang kode itu sendiri, tetapi di ViewModel (meskipun menggunakan ICommand, seperti yang disarankan EvilPigeon, bisa menjadi ide yang bagus karena Anda juga dapat mengikat untuk itu)
almulo
81

Kode ini berfungsi dengan baik:

ViewModel.cs:

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

dan di XAML:

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

berasumsi bahwa:

  • ViewModel ditugaskan ke salah DataContextsatu wadah utama.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
sumber
1
Lupa: untuk mendapatkan argumen acara dalam perintah, gunakan PassEventArgsToCommand = "True"
Stas
2
+1 pendekatan sederhana dan konvensional. Akan lebih baik untuk menuju ke PRISM.
Tri Q Tran
16
Ini adalah salah satu skenario yang menyoroti lubang menganga di WPF dan MVVM.
Damien
1
Akan sangat membantu untuk menyebutkan apa yang ada idi dalam <i:Interaction.Triggers>dan bagaimana cara mendapatkannya.
Andrii Muzychuk
1
@ Chiz, ini namespace yang harus kamu deklarasikan di elemen root seperti ini: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
34

Opsi ini bahkan lebih mudah, dan mungkin cocok untuk Anda. Di konstruktor View Model Anda, Anda dapat berlangganan acara penutupan Jendela Utama seperti ini:

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

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

Semua yang terbaik.

PILuaces
sumber
Ini adalah solusi terbaik di antara yang lain yang disebutkan dalam masalah ini. Terima kasih !
Yakub
Ini yang saya cari. Terima kasih!
Nikki Punjabi
20
... dan ini menciptakan hubungan yang erat antara ViewModel dan View. -1.
PiotrK
6
Ini bukan jawaban terbaik. Ini merusak MVVM.
Safiron
1
@Craig Ini membutuhkan referensi keras ke jendela utama, atau jendela apa pun yang sedang digunakan. Ini jauh lebih mudah, tetapi itu berarti model tampilan tidak dipisahkan. Ini bukan masalah memuaskan kutu buku MVVM atau tidak, tetapi jika pola MVVM harus diputus untuk membuatnya bekerja, tidak ada gunanya menggunakannya sama sekali.
Alex
16

Berikut ini adalah jawaban sesuai dengan pola-MVVM jika Anda tidak ingin tahu tentang Window (atau acara apa pun) di ViewModel.

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

Dalam ViewModel tambahkan antarmuka dan implementasi

public bool OnClosing()
{
    bool close = true;

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

    return close;
}

Di Jendela saya menambahkan acara Penutupan. Kode di belakang ini tidak merusak pola MVVM. View dapat mengetahui tentang model view!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
AxdorphCoder
sumber
Sederhana, jelas, dan bersih. ViewModel tidak perlu tahu secara spesifik tampilan, karenanya masalah tetap terpisah.
Bernhard Hiller
konteksnya selalu nol!
Shahid Od
@ShahidOd ViewModel Anda perlu mengimplementasikan IClosingantarmuka, bukan hanya mengimplementasikan OnClosingmetode. Kalau tidak, para DataContext as IClosingpemain akan gagal dan kembalinull
Erik White
10

Ya ampun, sepertinya banyak kode yang terjadi di sini untuk ini. Spa di atas memiliki pendekatan yang tepat untuk upaya minimal. Berikut ini adalah adaptasi saya (menggunakan MVVMLight tapi harus dikenali) ... Oh dan PassEventArgsToCommand = "True" yang pasti diperlukan seperti ditunjukkan di atas.

(kredit untuk 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> 

Dalam model tampilan:

///<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);

dalam 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 terlihat seperti berikut ini tetapi pada dasarnyaRequestShutdown atau apa pun namanya dinamai memutuskan apakah akan mematikan aplikasi atau tidak (yang dengan senang hati akan menutup jendela):

...
...
...
    /// <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
sumber
8

Penanya harus menggunakan jawaban STAS, tetapi bagi pembaca yang menggunakan prisma dan tanpa galasoft / mvvmlight, mereka mungkin ingin mencoba apa yang saya gunakan:

Dalam definisi di bagian atas untuk jendela atau kontrol pengguna, dll, tentukan namespace:

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

Dan tepat di bawah definisi itu:

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

Properti di viewmodel Anda:

public ICommand WindowClosing { get; private set; }

Lampirkan delegatkomandalam konstruktor viewmodel Anda:

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

Akhirnya, kode yang ingin Anda jangkau pada saat menutup kontrol / jendela / apa pun:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Chris
sumber
3
Ini tidak memberikan akses ke CancelEventArgs yang diperlukan untuk membatalkan acara penutupan. Objek yang diteruskan adalah model tampilan, yang secara teknis adalah model tampilan yang sama dengan perintah WindowClosing dieksekusi.
stephenbayer
4

Saya akan tergoda untuk menggunakan event handler dalam file App.xaml.cs Anda yang memungkinkan Anda memutuskan apakah akan menutup aplikasi atau tidak.

Misalnya Anda kemudian dapat memiliki sesuatu seperti kode berikut di file App.xaml.cs Anda:

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

}

Kemudian dalam kode MainWindowViewModel Anda, Anda dapat memiliki yang berikut ini:

#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
sumber
1
Terima kasih atas jawaban terinci. Namun, saya tidak berpikir itu menyelesaikan masalah saya: Saya harus menangani penutupan jendela ketika pengguna mengklik tombol 'X' kanan atas. Akan mudah untuk melakukan ini dalam kode-belakang (saya hanya akan menautkan acara Penutupan dan mengatur CancelEventArgs.Cancel menjadi true of false) tetapi saya ingin melakukan ini dalam gaya MVVM. Maaf atas kebingungan
Olivier Payen
1

Pada dasarnya, window event mungkin tidak ditugaskan ke MVVM. Secara umum, tombol Tutup menunjukkan kotak Dialog untuk meminta pengguna "save: yes / no / cancel", dan ini mungkin tidak dapat dicapai oleh MVVM.

Anda dapat menyimpan pengendali acara OnClosing, tempat Anda memanggil Model.Close.CanExecute () dan mengatur hasil boolean di properti acara. Jadi setelah panggilan CanExecute () jika benar, ATAU dalam acara OnClosed, panggil Model.Close.Execute ()

Echtelion
sumber
1

Saya belum melakukan banyak pengujian dengan ini tetapi tampaknya berhasil. Inilah yang saya pikirkan:

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
sumber
1
Apa yang akan terjadi di sini dalam skenario di mana VM ingin membatalkan penutupan?
Tri Q Tran
1

Menggunakan MVVM Light Toolkit:

Dengan asumsi bahwa ada perintah Exit dalam model tampilan:

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

Ini diterima dalam tampilan:

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

Di sisi lain, saya menangani Closingacara di MainWindow, menggunakan instance dari ViewModel:

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

CancelBeforeClose memeriksa model tampilan saat ini dan mengembalikan true jika penutupan harus dihentikan.

Semoga ini bisa membantu seseorang.

Ron
sumber
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Mattias Sturebrand
sumber
Hai, jangan tambahkan sedikit penjelasan bersama dengan kode karena ini membantu untuk memahami kode Anda. Kode hanya jawaban yang disukai
Bhargav Rao
Op secara eksplisit menyatakan bahwa dia tidak tertarik menggunakan kode acara di belakang kode untuk ini.
Fer García