Обробка події закриття вікна за допомогою WPF / MVVM Light Toolkit


145

Я хотів би обробити Closingподію (коли користувач натискає верхню праву кнопку "X") мого вікна, щоб в кінцевому підсумку відобразити повідомлення про підтвердження або / та скасувати закриття.

Я знаю, як це зробити за кодом: підпишіться на Closingподію вікна, а потім використовуйте CancelEventArgs.Cancelвластивість.

Але я використовую MVVM, тому я не впевнений, що це гарний підхід.

Я думаю, що добрим підходом було б прив’язати Closingподію до Commandмого ViewModel.

Я спробував це:

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

З асоційованим RelayCommandв моєму ViewModel, але він не працює (код команди не виконується).


3
Також цікавий приємною відповіддю, щоб відповісти на це.
— Сехат

3
Я завантажив код з codeplex і при налагодженні він виявив: "Неможливо надати об'єкт типу" System.ComponentModel.CancelEventArgs ", щоб ввести" System.Windows.RoutedEventArgs "." Це добре працює, якщо ви не хочете CancelEventArgs, але це не відповідає на ваше запитання ...
— Девід Холліншад

Я здогадуюсь, що ваш код не працює, оскільки у контролі, до якого ви приєднали тригер, немає події закриття. Ваш контекст даних не є вікном ... Це, мабуть, шаблон даних із сіткою або щось подібне, що не має події закриття. Тож відповідь dbkk - найкраща відповідь у цьому випадку. Однак я віддаю перевагу підходу Interaction / EventTrigger, коли подія доступна.
— NielW

Наприклад, наявний код буде добре працювати на завантаженій події.
— NielW

Відповіді:


126

Я просто асоціюю обробник у конструкторі View:

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

Потім додайте обробник до ViewModel:

using System.ComponentModel;

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

У цьому випадку ви не отримуєте абсолютно нічого, крім складності, використовуючи більш досконалий візерунок з більшою кількістю непрямості (5 додаткових ліній XAML плюс Commandшаблон).

Мантра "нульового коду" - сама по собі не мета, справа полягає в тому, щоб від'єднати ViewModel від Погляду . Навіть коли подія прив’язана до коду позаду Перегляду, ViewModelзначення не залежить від Перегляду, і логіка закриття може бути перевірена одиницею .


4
Мені подобається таке рішення: просто
— зачепись

3
Для початківців mvvm, які не використовують MVVMLight та шукають, як інформувати ViewModel про подію закриття, цікавими можуть бути посилання про те, як правильно встановити dataContext та як отримати об’єкт viewModel у View. Як отримати посилання на ViewModel у Перегляді? і Як встановити ViewModel на вікно в xaml, використовуючи властивість datacontext ... Минуло кілька годин, як можна просто обробляти просту подію закриття вікон у ViewModel.
— MarkusEgle

18
Це рішення не має значення в середовищі MVVM. Код позаду не повинен знати про ViewModel.
— Яків

2
@Jacob Я думаю, що проблема полягає в тому, що ви отримуєте обробник подій форми у своєму ViewModel, який з'єднує ViewModel з певною реалізацією інтерфейсу. Якщо вони збираються використовувати код позаду, вони повинні перевірити CanExecute і потім викликати Execute () на властивості ICommand.
— Злий голуб

14
@Jacob Код позаду може знати про членів ViewModel чудово, просто за допомогою XAML-коду. Або що ви думаєте, чим займаєтесь, створюючи прив'язку до властивості ViewModel? Це рішення ідеально підходить для MVVM, якщо ви не обробляєте логіку закриття в коді позаду себе, а в ViewModel (хоча використання ICommand, як пропонує EvilPigeon, може бути гарною ідеєю, оскільки ви також можете зв’язати до нього)
— almulo

81

Цей код працює чудово:

ViewModel.cs:

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

і в XAML:

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

якщо припустити, що:

  • ViewModel призначається DataContextосновного контейнера.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

1
Забув: отримати аргументи подій у використанні команди PassEventArgsToCommand = «True»
— Стас

2
+1 простий і звичайний підхід. Було б ще краще перейти до PRISM.
— Tri Q Tran

16
Це один із сценаріїв, в якому висвітлюються розбіжні отвори у WPF та MVVM.
— Демієн

1
Було б дуже корисно згадати , що iв <i:Interaction.Triggers>і як його отримати.
— Андрій Музичук

1
@Chiz, це простір імен , ви повинні оголосити в кореневому елементі , як це: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
— Стас

34

Цей варіант ще простіше, і, можливо, підходить саме вам. У конструкторі View Model ви можете підписати подію закриття Main Window таким чином:

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

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

Всього найкращого.


Це найкраще рішення серед інших, що згадуються у цьому питанні. Дякую !
— Яків

Це те, що я шукав. Дякую!
— Ніккі Пенджабі

20
... і це створює тісний зв'язок між ViewModel та View. -1.
— PiotrK

6
Це не найкраща відповідь. Це порушує MVVM.
— Сафірон

1
@Craig Це вимагає чіткого посилання на головне вікно або те, в якому вікні воно використовується. Це набагато простіше, але це означає, що модель перегляду не відокремлена. Справа не в тому, щоб задовольнити мотаренів MVVM чи ні, але якщо шаблон MVVM повинен бути порушений, щоб він працював, немає сенсу використовувати його взагалі.
— Олексій

16

Ось відповідь відповідно до шаблону MVVM, якщо ви не хочете знати про Вікно (або будь-яку його подію) у ViewModel.

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

У ViewModel додайте інтерфейс та реалізацію

public bool OnClosing()
{
    bool close = true;

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

    return close;
}

У вікно я додаю подію закриття. Цей код поза не порушує схему MVVM. Перегляд може знати про модель перегляду!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}

Простий, зрозумілий і чистий. ViewModel не повинен знати конкретні точки зору, отже, це стосується залишення розлуки.
— Бернхард Хіллер

контекст завжди недійсний!
— Шахід Од

@ShahidOd Ваш ViewModel повинен реалізувати IClosingінтерфейс, а не просто реалізувати OnClosingметод. Інакше DataContext as IClosingакторський склад вийде з ладу і повернетьсяnull
— Ерік Уайт

10

Гейз, здається, для цього відбувається чимало коду. Стас вище мав правильний підхід за мінімальні зусилля. Ось моя адаптація (за допомогою MVVMLight, але має бути впізнаваною) ... О, і PassEventArgsToCommand = "True" , безумовно, потрібен, як зазначено вище.

(заслуга Лорана Буньйона 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> 

У моделі перегляду:

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

у службі 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 виглядає приблизно як наступне, але в основномуRequestShutdown або все, що воно названо, вирішує, закривати додаток чи ні (що все одно вільно закриє вікно):

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

8

Автор, що задає запитання, повинен використовувати відповідь STAS, але для читачів, які використовують призму і не галактик / mvvmlight, вони можуть спробувати те, що я використав:

У визначенні вгорі для вікна або користувальницького керування тощо визначте простір імен:

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

І трохи нижче цього визначення:

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

Власність у вашій моделі перегляду:

public ICommand WindowClosing { get; private set; }

Додайте команду delegatecomma у вашому конструкторі viewmodel:

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

Нарешті, ваш код, який ви хочете отримати після закриття елемента керування / вікна / будь-якого іншого:

private void OnWindowClosing(object obj)
        {
            //put code here
        }

3
Це не дає доступу до CancelEventArgs, який необхідний для скасування події закриття. Переданий об'єкт - це модель перегляду, яка технічно є тією ж моделлю перегляду, з якої виконується команда WindowClosing.
— stephenbayer

4

Мені б сподобатися використовувати обробник подій у вашому файлі App.xaml.cs, який дозволить вам вирішити, закривати програму чи ні.

Наприклад, тоді у вашому файлі App.xaml.cs може бути щось на зразок наступного коду:

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

}

Тоді в коді MainWindowViewModel у вас можуть бути наступні:

#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]

1
Дякую за детальну відповідь. Однак я не думаю, що це вирішує мою проблему: мені потрібно обробляти закриття вікна, коли користувач натискає верхню праву кнопку "X". Це було б легко зробити в кодовому режимі (я би просто пов'язував подію закриття і встановив CancelEventArgs.Cancel на істинне помилкове), але я хотів би зробити це у стилі MVVM. Вибачте за плутанину
— Олів'є Паєн

1

В основному подія вікна може не бути призначена MVVM. Як правило, кнопка «Закрити» показує діалогове вікно, щоб запитати користувача «зберегти: так / ні / скасувати», і це може не досягти MVVM.

Ви можете зберегти обробник подій OnClosing, де викличете Model.Close.CanExecute () і встановите булевий результат у властивості події. Тож після виклику CanExecute (), якщо це правда, АБО у події OnClosed, викличте Model.Close.Execute ()


1

Я не робив багато тестування з цим, але, здається, працює. Ось що я придумав:

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

1
Що буде тут у сценарії, коли VM хоче скасувати закриття?
— Tri Q Tran

1

Для цього ми використовуємо AttachedCommandBehavior. Ви можете приєднати будь-яку подію до команди на моделі перегляду, уникаючи будь-якого коду позаду.

Ми використовуємо це у всьому нашому рішенні і майже поза нулем код

http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/


1

Використання MVVM Light Toolkit:

Якщо припустити, що в моделі перегляду є команда Exit :

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

Це отримано у вигляді:

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

З іншого боку, я обробляю Closingподії в MainWindow, використовуючи екземпляр ViewModel:

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

CancelBeforeClose перевіряє поточний стан перегляду та повертає істинне, якщо закриття слід зупинити.

Сподіваюся, це комусь допоможе.


-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }

Привіт, додайте трохи пояснень разом із кодом, оскільки це допоможе зрозуміти ваш код. Відповіді з коду нахмурюються лише
— Бхаргав Рао

Оп прямо заявив, що він не зацікавлений у використанні для цього коду події, що знаходиться поза кодом.
— Fer García
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.