Обробка події закриття вікна за допомогою 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.