使用WPF/MVVM Light Toolkit处理窗口关闭事件

159

我想处理窗口的事件(当用户单击右上角的'X'按钮时),最终显示确认消息和/或取消关闭。

我知道如何在代码后台中完成这个操作:订阅窗口的事件,然后使用属性。

但我正在使用MVVM,所以我不确定这是否是正确的方法。

我认为正确的方法是将事件绑定到我的ViewModel中的一个。

我尝试了一下:

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

在我的ViewModel中有一个关联的RelayCommand,但它不起作用(命令代码未被执行)。


3
也对这个问题的回答很感兴趣。 - Sekhat
3
我从codeplex下载了代码并进行调试,发现出现以下错误:"无法将类型为 'System.ComponentModel.CancelEventArgs' 的对象强制转换为类型 'System.Windows.RoutedEventArgs'。"如果您 不需要 CancelEventArgs,则它可以正常工作,但这并不能回答您的问题... - David Hollinshead
我猜测你的代码无法工作是因为你附加触发器的控件没有Closing事件。你的数据上下文不是一个窗口...它可能是一个带有网格或其他内容的数据模板,其中没有Closing事件。所以在这种情况下,dbkk的答案是最好的答案。然而,当事件可用时,我更喜欢使用Interaction/EventTrigger方法。 - NielW
你所拥有的代码在Loaded事件中可以正常工作。 - NielW
14个回答

149

我会在视图构造函数中简单地关联处理程序:

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与View分离。即使事件在View的代码后台中绑定,ViewModel也不依赖于View,并且关闭逻辑可以进行单元测试。

4
我喜欢这个解决方案:只需连接到一个隐藏的按钮 :) (注:这是一个关于在WPF中如何使用MVVM处理事件的博客文章链接) - Benjol
3
对于不使用MVVMLight并且正在寻找如何通知ViewModel 关闭事件的MVVM初学者,如何正确设置dataContext以及在View中获取viewModel对象的链接可能会很有用。如何在View中获取ViewModel的引用?如何在XAML中使用DataContext属性为窗口设置ViewModel ...花了我好几个小时才弄清楚如何在ViewModel中处理一个简单的窗口关闭事件。 - MarkusEgle
24
这个解决方案在MVVM环境下是不相关的。代码后台不应该知道ViewModel的存在。 - Jacob
3
我认为问题更多的是你在ViewModel中获得了一个表单事件处理程序,这使得ViewModel与特定的UI实现耦合。如果他们要使用代码后台,应该检查CanExecute,然后在一个ICommand属性上调用Execute()。 - Evil Pigeon
20
@Jacob代码后端可以很好地了解ViewModel成员,就像XAML代码一样。当您创建到ViewModel属性的绑定时,您认为自己在做什么?只要不在代码后端本身处理关闭逻辑,而是在ViewModel中处理(尽管使用ICommand,如EvilPigeon建议的那样,可能是一个好主意,因为您还可以将其绑定),这个解决方案对于MVVM来说是完全可行的。 - almulo
显示剩余4条评论

84

这段代码完全正常:

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"

2
+1 简单而传统的方法。前往 PRISM 将更好。 - Tri Q Tran
16
这是一个突显WPF和MVVM中存在严重漏洞的情况。 - Damien
1
<i:Interaction.Triggers>中提到i是什么以及如何获取它会非常有帮助。 - Andrii Muzychuk
1
@Chiz,这是一个命名空间,你应该在根元素中声明它,像这样:xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" - Stas
1
这里引用了哪个EventToCommand的实现?答案应该是完整的,或者至少指导用户如何找到相关部分。您使用MVVM Light、Glue、Blend或其他实现之一吗?command命名空间是什么?这个答案只完成了一半。 - Alex
显示剩余5条评论

39

这个选项甚至更加简单,也许适合你。在你的视图模型构造函数中,你可以像下面这样订阅主窗口关闭事件:

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

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

祝你一切顺利。


这是在本问题中提到的其他解决方案中最好的解决方案。谢谢! - Jacob
27
这会导致ViewModel和View之间紧密耦合。-1. - PiotrK
最佳答案! - JokerMartini
7
这不是最好的答案。它违反了MVVM。 - Safiron
3
@Craig 需要一个对主窗口或正在使用的窗口的硬引用。这样做更容易,但意味着视图模型不是解耦的。这不是满足MVVM爱好者与否的问题,而是如果必须打破MVVM模式才能使其工作,那么使用它就没有意义了。 - Alex
显示剩余4条评论

24

如果您不想在ViewModel中使用Window(或其任何事件),则可以根据MVVM模式给出以下答案。

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 不需要知道视图的具体细节,因此关注点保持分离。 - Bernhard Hiller
上下文始终为空! - Shahid Od
@ShahidOd 您的ViewModel需要实现IClosing接口,而不仅仅是实现OnClosing方法。否则DataContext as IClosing转换将失败并返回null - Erik White

11

哎呀,这里好像有很多代码啊。Stas上面的方法是最省力的。这是我的改编版本(使用MVVMLight但应该是可识别的)...哦,PassEventArgsToCommand="True" 绝对是需要的,就像上面所指出的。

(感谢Laurent Bugnionhttp://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看起来像下面这样,但基本上它决定是否关闭应用程序(这将愉快地关闭窗口):

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

10

提问者应该使用STAS答案,但对于使用prism而没有galasoft/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; }

在您的ViewModel构造函数中附加DelegateCommand:

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

最后,你想要在控件/窗口/其他东西关闭时到达的代码:

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

4
这不提供访问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”按钮时处理窗口关闭。在代码后台中很容易实现这一点(我只需链接Closing事件并将CancelEventArgs.Cancel设置为true或false),但我想以MVVM风格实现这一点。对于造成的困惑,我表示抱歉。 - Olivier Payen

1
基本上,窗口事件可能不能分配给MVVM。通常情况下,关闭按钮会显示一个对话框,询问用户“保存:是/否/取消”,而这可能无法通过MVVM实现。
您可以保留OnClosing事件处理程序,在其中调用Model.Close.CanExecute()并在事件属性中设置布尔结果。 因此,在CanExecute()调用后如果为true,或者在OnClosed事件中调用Model.Close.Execute()。

1

1

我从这篇文章中获得了灵感,并将其改编成了一个我正在构建的库,供自己使用(但将公开位于此处:https://github.com/RFBCodeWorks/MvvmControls)。

虽然我的方法有点通过传递给处理程序的“sender”和“eventargs”向ViewModel暴露View,但我使用这种方法只是为了以防其他某些处理需要它。例如,如果处理程序不是ViewModel,而是记录打开/关闭窗口的某个服务,则该服务可能想要了解发送者。如果VM不想知道View,则它只需不检查发送者或参数。

以下是我想出的相关代码,它消除了Code-Behind,并允许在xaml中进行绑定:

Behaviors:WindowBehaviors.IWindowClosingHandler="{Binding ElementName=ThisWindow, Path=DataContext}"

    /// <summary>
    /// Interface that can be used to send a signal from the View to the ViewModel that the window is closing
    /// </summary>
    public interface IWindowClosingHandler
    {
        /// <summary>
        /// Executes when window is closing
        /// </summary>
        void OnWindowClosing(object sender, System.ComponentModel.CancelEventArgs e);

        /// <summary>
        /// Occurs when the window has closed
        /// </summary>
        void OnWindowClosed(object sender, EventArgs e);

    }

    /// <summary>
    /// Attached Properties for Windows that allow MVVM to react to a window Loading/Activating/Deactivating/Closing
    /// </summary>
    public static class WindowBehaviors
    {
        #region < IWindowClosing >

        /// <summary>
        /// Assigns an <see cref="IWindowClosingHandler"/> handler to a <see cref="Window"/>
        /// </summary>
        public static readonly DependencyProperty IWindowClosingHandlerProperty =
            DependencyProperty.RegisterAttached(nameof(IWindowClosingHandler),
                typeof(IWindowClosingHandler),
                typeof(WindowBehaviors),
                new PropertyMetadata(null, IWindowClosingHandlerPropertyChanged)
                );

        /// <summary>
        /// Gets the assigned <see cref="IWindowLoadingHandler"/> from a <see cref="Window"/>
        /// </summary>
        public static IWindowClosingHandler GetIWindowClosingHandler(DependencyObject obj) => (IWindowClosingHandler)obj.GetValue(IWindowClosingHandlerProperty);

        /// <summary>
        /// Assigns an <see cref="IWindowClosingHandler"/> to a <see cref="Window"/>
        /// </summary>
        public static void SetIWindowClosingHandler(DependencyObject obj, IWindowClosingHandler value)
        {
            if (obj is not null and not Window) throw new ArgumentException($"{nameof(IWindowClosingHandler)} property can only be bound to a {nameof(Window)}");
            obj.SetValue(IWindowClosingHandlerProperty, value);
        }

        private static void IWindowClosingHandlerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Window w = d as Window;
            if (w is null) return;
            if (e.NewValue != null)
            {
                w.Closing += W_Closing;
                w.Closed += W_Closed;
            }
            else
            {
                w.Closing -= W_Closing;
                w.Closed -= W_Closed;
            }
        }

        private static void W_Closed(object sender, EventArgs e)
        {
            GetIWindowClosingHandler(sender as DependencyObject)?.OnWindowClosed(sender, e);
        }

        private static void W_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            GetIWindowClosingHandler(sender as DependencyObject)?.OnWindowClosing(sender, e);
        }

        #endregion
    }

相当有趣。挑衅地说,“通过EventArgs公开的可视化元素”并不是什么大不了的事情,因为第二个最受欢迎的答案也这样做。 - CommonSense

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接