Обработка события закрытия окна с помощью WPF / MVVM Light Toolkit

145

Я хотел бы обработать Closingсобытие (когда пользователь нажимает верхнюю правую кнопку «X») моего окна, чтобы в конечном итоге отобразить подтверждающее сообщение или / и отменить закрытие.

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

Но я использую MVVM, поэтому я не уверен, что это хороший подход.

Я думаю, что хорошим подходом было бы связать Closingсобытие с a 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, но это не отвечает на ваш вопрос ...
Дэвид Холлинсхед
Я предполагаю, что ваш код не работает, потому что элемент управления, к которому вы подключили триггер, не имеет события Closing. Ваш контекст данных - это не окно ... Это, вероятно, шаблон данных с сеткой или чем-то еще, у которого нет события закрытия. Так что ответ dbkk - лучший ответ в этом случае. Однако я предпочитаю подход Interaction / EventTrigger, когда событие доступно.
NielW
Например, ваш код будет нормально работать с событием Loaded.
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 дополнительных строк Commandшаблона XAML плюс ).

Мантра «с нулевым выделением кода» сама по себе не является целью, цель состоит в том, чтобы отделить ViewModel от View . Даже когда событие связано с выделенным кодом в представлении, ViewModelоно не зависит от представления, и логика закрытия может быть проверена модулем .

dbkk
источник
4
Мне нравится это решение: просто подключите скрытую кнопку :)
Benjol
3
Для начинающих mvvm, не использующих MVVMLight и ищущих, как сообщить ViewModel о событии Closing, могут быть интересны ссылки, как правильно настроить dataContext и как получить объект viewModel в View. Как получить ссылку на ViewModel в View? и Как установить 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 вы можете подписать событие закрытия главного окна следующим образом:

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

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

Всего наилучшего.

PILuaces
источник
Это лучшее решение среди других упомянутых в этом выпуске. Спасибо !
Джейкоб
Это то, что я искал. Спасибо!
Никки Пенджаби
20
... и это создает тесную связь между ViewModel и View. -1.
PiotrK
6
Это не лучший ответ. Это ломает MVVM.
Safiron
1
@Craig Требуется жесткая ссылка на главное окно или на то окно, для которого оно используется. Это намного проще, но это означает, что модель представления не отделена. Это не вопрос удовлетворения ботаников MVVM или нет, но если шаблон MVVM должен быть сломан, чтобы заставить его работать, нет никакого смысла использовать его вообще.
Alex
16

Вот ответ в соответствии с MVVM-шаблоном, если вы не хотите знать о Window (или любом его событии) в 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();
    }
}
AxdorphCoder
источник
Просто, понятно и чисто. 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);

в службе выключения

    /// <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);
    }
    }
AllenM
источник
8

Запрашивающий должен использовать ответ STAS, но для читателей, которые используют призму и не используют galasoft / mvvmlight, они могут попробовать то, что я использовал:

В определении вверху для окна или usercontrol и т. Д. Определите пространство имен:

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>

Недвижимость в вашей viewmodel:

public ICommand WindowClosing { get; private set; }

Прикрепите делегатскую команду в свой конструктор viewmodel:

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

Наконец, ваш код вы хотите достичь при закрытии элемента управления / окна / что угодно:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Крис
источник
3
Это не дает доступа к CancelEventArgs, который необходим для отмены события закрытия. Переданный объект является моделью представления, которая технически является той же моделью представления, из которой выполняется команда WindowClosing.
Стивенбайер
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]
ChrisBD
источник
1
Спасибо за подробный ответ. Однако я не думаю, что это решает мою проблему: мне нужно обработать закрытие окна, когда пользователь нажимает верхнюю правую кнопку «X». Было бы легко сделать это в коде (я просто связал событие Closing и установил для CancelEventArgs.Cancel значение true, равное false), но я бы хотел сделать это в стиле MVVM. Извините за путаницу
Оливье Пэйен
1

По существу, оконное событие не может быть назначено MVVM. В общем случае кнопка «Закрыть» отображает диалоговое окно с запросом пользователя «сохранить: да / нет / отмена», а MVVM может этого не достичь.

Вы можете сохранить обработчик события OnClosing, где вы вызываете Model.Close.CanExecute () и устанавливаете логический результат в свойстве события. Поэтому после вызова CanExecute (), если true, ИЛИ в событии OnClosed, вызовите Model.Close.Execute ()

Echtelion
источник
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
Что будет здесь в сценарии, когда виртуальная машина желает отменить закрытие?
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 проверяет текущее состояние модели представления и возвращает true, если закрытие должно быть остановлено.

Надеюсь, это кому-нибудь поможет.

Рон
источник
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Маттиас Стуребранд
источник
Привет, добавь немного объяснения вместе с кодом, так как это помогает понять твой код. Ответы только по коду не одобряются
Бхаргав Рао
Оператор явно заявил, что он не заинтересован в использовании кода события code-behind для этого.
Фер Гарсия