WPF MVVM:如何关闭窗口

85

我有一个 Button,当它被点击时可以关闭我的窗口:

<Button x:Name="buttonOk"  IsCancel="True">Ok</Button>

当我在按钮中添加 Command 后,这样做就没问题了。

<Button x:Name="buttonOk" 
        Command="{Binding SaveCommand}" 
        IsCancel="True">Ok</Button>

现在它没有关闭,可能是因为我正在处理Command。我可以通过添加一个EventHandler并调用this.Close()来修复这个问题。

<Button x:Name="buttonOk" 
        Click="closeWindow" 
        Command="{Binding SaveCommand}" 
        IsCancel="True">Ok</Button>

现在我有一些代码放在我的代码后台,即方法SaveCommand。我正在使用MVVM模式,而SaveCommand是我代码后台中唯一的代码。

如何以不使用代码后台的方式完成这个操作?


17
注意:在确定按钮上设置“IsCancel =“ True”是个不好的主意。该属性应该用于取消按钮。 - Greg D
澄清一下,代码背后的是 closeWindowClick 事件处理程序,而不是 SaveCommand。这似乎是一个笔误。 - sean
22个回答

74

我刚刚完成了一篇关于这个主题的博客文章。简而言之,在你的ViewModel中添加一个带有getset访问器的Action属性。然后在你的View构造函数中定义Action。最后,在绑定命令中调用你的操作以关闭窗口。

在ViewModel中:

public Action CloseAction  { get; set;}

并且在 View 构造函数中:

private View()
{
    InitializeComponent();
    ViewModel vm = new ViewModel();
    this.DataContext = vm;
    if ( vm.CloseAction == null )
        vm.CloseAction = new Action(this.Close);
}

最后,在任何应该关闭窗口的有界命令中,我们可以简单地调用

CloseAction(); // Calls Close() method of the View

这对我有用,看起来是一个相当优雅的解决方案,并且节省了我大量的编码。


这对我不起作用。当我调用CloseAction()时,尽管视图中的代码存在,它仍然显示CloseAction为空。 - Danielle
12
请原谅我的无知,但这样做不违反视图和视图模型解耦的原则吗?如果您在视图中实例化视图模型,那么使用MVVM就没有意义了。我认为最佳实践是单独实例化视图和视图模型,并在视图之外将DataContext设置为视图。 - saegeoff
2
通过将Action设置为静态属性来解决它。Daaaah! - Talal Yousif
15
我知道这个问题已经有些老了,但我认为除非有我不知道的严格定义,否则这种方法并不违反MVVM。最终,MVVM要求VM不知道View,但View必须知道VM。如果要替换View,它不会以任何方式破坏VM。可能会有一个未实例化的Action,但我不认为这是违反MVVM规则的声明。搜索“WPF DataContext Instantiation”会在许多文章中引用到这种方法。 - flyNflip
7
您可以使用构造函数注入而不是属性注入来消除空值检查:http://programmers.stackexchange.com/questions/177649/what-is-constructor-injection。在此之后,您可以在ViewModel的构造函数中将close指派给CloseAction。这样做的好处是使CloseAction成为只读属性。 - DharmaTurtle
显示剩余5条评论

33
非常干净和MVVM的方式是使用InteractionTriggerCallMethodAction,它们定义在Microsoft.Interactivity.Core中。
您需要添加一个新的命名空间如下所示:
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

你需要引用 Microsoft.Xaml.Behaviours.Wpf 程序集,然后以下 XAML 代码将会生效。
<Button Content="Save" Command="{Binding SaveCommand}">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="Click">
      <i:CallMethodAction MethodName="Close"
                           TargetObject="{Binding RelativeSource={RelativeSource
                                                  Mode=FindAncestor,
                                                  AncestorType=Window}}" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</Button>

您不需要任何代码后台或其他东西,也可以调用Window的任何其他方法。


1
这是我迄今为止看到的最干净的方法,因为没有代码后台和没有使ViewModel与View耦合。 它也适用于命令。 您需要部署一些额外的DLL,并且如果您想能够从您的命令中取消关闭,则需要进行额外的工作。 这与在代码后台中具有单击事件并仅调用Close()的情况并没有太大区别,代码后台事件处理程序将更容易处理关闭命令取消关闭事件的情况(例如,如果存在保存数据时出现错误的情况)。 谢谢Massimiliano - Richard Moore
4
由于微软开源了WPF行为并将其移动到Microsoft.Xaml.Behaviors.Wpf NuGet包中,Rajnikant的代码在VS 2019中不再适用。信息来源是这个问题的评论:https://developercommunity.visualstudio.com/content/problem/198075/microsoftexpressioninteractions-is-missing-from-vi.html。重新构建您的代码的详细步骤请参见:https://devblogs.microsoft.com/dotnet/open-sourcing-xaml-behaviors-for-wpf/。 - Eric Wood
1
它更加清晰,但我认为应该由视图模型来控制,而不是视图,因为它属于命令,而不是标准关闭按钮。 - Daniel Möller
1
只有在您始终希望在按钮单击时关闭窗口时,此方法才有效。如果您想决定按钮按下是否真的应该关闭窗口,则无法实现。 - Welcor
太棒了。这绝对是正确的方法。正如其他人已经提到的,一定要通过上述NuGet包使用最新版本的Interactivity命名空间。 - AndyUK

19

正如某人所评论的那样,我发布的代码不符合MVVM规范,第二个解决方案怎么样?

1. 不是MVVM解决方案(我将其保留作为参考)

XAML:

<Button Name="okButton" Command="{Binding OkCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">OK</Button>

视图模型:

public ICommand OkCommand
{
    get
    {
        if (_okCommand == null)
        {
            _okCommand = new ActionCommand<Window>(DoOk, CanDoOk);
        }
        return _okCommand ;
    }
}

void DoOk(Window win)
{
    // Your Code
    win.DialogResult = true;
    win.Close();
}

bool CanDoOk(Window win) { return true; }

第二种,可能更好的解决方案: 使用附加行为

XAML

<Button Content="Ok and Close" Command="{Binding OkCommand}" b:CloseOnClickBehaviour.IsEnabled="True" />

视图模型

public ICommand OkCommand
{
    get { return _okCommand; }
}

行为类

类似于这样:

public static class CloseOnClickBehaviour
{
    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached(
            "IsEnabled",
            typeof(bool),
            typeof(CloseOnClickBehaviour),
            new PropertyMetadata(false, OnIsEnabledPropertyChanged)
        );

    public static bool GetIsEnabled(DependencyObject obj)
    {
        var val = obj.GetValue(IsEnabledProperty);
        return (bool)val;
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    static void OnIsEnabledPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs args)
    {
        var button = dpo as Button;
        if (button == null)
            return;

        var oldValue = (bool)args.OldValue;
        var newValue = (bool)args.NewValue;

        if (!oldValue && newValue)
        {
            button.Click += OnClick;
        }
        else if (oldValue && !newValue)
        {
            button.PreviewMouseLeftButtonDown -= OnClick;
        }
    }

    static void OnClick(object sender, RoutedEventArgs e)
    {
        var button = sender as Button;
        if (button == null)
            return;

        var win = Window.GetWindow(button);
        if (win == null)
            return;

        win.Close();
    }

}

37
你永远不要,和我一起重复,将窗口与视图模型耦合。现在将该句子重复100次 :) - Ignacio Soler Garcia
4
在我看来,这是最好的解决方案:它实现了所需的功能,代码最短,不需要复杂的基础设施,以 MVVM 的方式解决问题。@SoMoS - 这里完全没有耦合。VM 不知道 View 的存在;命令将 Window 作为参数获取,因为它需要知道要关闭什么。 - Ilia Barahovsky
2
+1 @SoMoS 我同意Ilia的观点,这正是解耦的解决方案。我不会将保存和关闭窗口逻辑合并在一起,但这是另一回事。 - makc
9
@Barahovski:Window 是一种 WPF 对象。ViewModel 不应依赖于 WPF 或任何重型框架。如果要进行一个没有 UI 的单元测试来测试它,该如何获取 Window 实例? - g.pickardou
1
优秀的第二解决方案!实际上,您根本不需要设置按钮上的Command属性,因此在ViewModel中不需要该代码。 - Svein Terje Gaup
显示剩余3条评论

13

我个人会使用行为(behaviour)来完成这种事情:

public class WindowCloseBehaviour : Behavior<Window>
{
    public static readonly DependencyProperty CommandProperty =
      DependencyProperty.Register(
        "Command",
        typeof(ICommand),
        typeof(WindowCloseBehaviour));

    public static readonly DependencyProperty CommandParameterProperty =
      DependencyProperty.Register(
        "CommandParameter",
        typeof(object),
        typeof(WindowCloseBehaviour));

    public static readonly DependencyProperty CloseButtonProperty =
      DependencyProperty.Register(
        "CloseButton",
        typeof(Button),
        typeof(WindowCloseBehaviour),
        new FrameworkPropertyMetadata(null, OnButtonChanged));

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public object CommandParameter
    {
        get { return GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    public Button CloseButton
    {
        get { return (Button)GetValue(CloseButtonProperty); }
        set { SetValue(CloseButtonProperty, value); }
    }

    private static void OnButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var window = (Window)((WindowCloseBehaviour)d).AssociatedObject;
        ((Button) e.NewValue).Click +=
            (s, e1) =>
            {
                var command = ((WindowCloseBehaviour)d).Command;
                var commandParameter = ((WindowCloseBehaviour)d).CommandParameter;
                if (command != null)
                {
                    command.Execute(commandParameter);                                                      
                }
                window.Close();
            };
        }
    }

您可以将此附加到您的 WindowButton 上以完成工作:

<Window x:Class="WpfApplication6.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:local="clr-namespace:WpfApplication6"
        Title="Window1" Height="300" Width="300">
    <i:Interaction.Behaviors>
        <local:WindowCloseBehaviour CloseButton="{Binding ElementName=closeButton}"/>
    </i:Interaction.Behaviors>
    <Grid>
        <Button Name="closeButton">Close</Button>
    </Grid>
</Window>

我在这里添加了CommandCommandParameter,以便您可以在Window关闭之前运行命令。


1
我来晚了,但是这可以通过直接将行为放在按钮上进一步简化。您可以为单击事件定义一个处理程序,该处理程序调用 Window.GetWindow(AssociatedObject)?.Close()(当然要适当进行空值检查),并在 OnAttachedOnDetaching 钩子的重写中附加/分离。三个微不足道的函数,零个属性,可以附加到同一(或不同)窗口中的任意数量的按钮。 - bionicOnion
嗯,如果不是将行为附加到窗口并传递按钮,而是将行为附加到_button_并传递窗口,这样设计会更好,不是吗? - Sören Kuklau

9
对于小型应用程序,我使用自己的应用程序控制器来显示、关闭和处理窗口和数据上下文。它是应用程序 UI 的中心点。
它类似于这样:
//It is singleton, I will just post 2 methods and their invocations
public void ShowNewWindow(Window window, object dataContext = null, bool dialog = true)
{
    window.DataContext = dataContext;
    addToWindowRegistry(dataContext, window);

    if (dialog)
        window.ShowDialog();
    else
        window.Show();

}

public void CloseWindow(object dataContextSender)
{
    var correspondingWindows = windowRegistry.Where(c => c.DataContext.Equals(dataContextSender)).ToList();
    foreach (var pair in correspondingWindows)
    {
        pair.Window.Close();              
    }
}

还有从 ViewModels 中调用它们的方法:

// Show new Window with DataContext
ApplicationController.Instance.ShowNewWindow(
                new ClientCardsWindow(),
                new ClientCardsVM(),
                false);

// Close Current Window from viewModel
ApplicationController.Instance.CloseWindow(this);

当然,我的解决方案存在一些限制。再次强调:我将其用于小型项目,已经足够了。如果您有兴趣,我可以在这里或其他地方发布完整的代码。

7

我一直尝试以通用的MVVM方式解决此问题,但最终发现自己使用了不必要的复杂逻辑。为了实现接近的行为,我违反了无代码后台规则,并采用了简单地在代码后台中使用好老式事件:

XAML:

<Button Content="Close" Click="OnCloseClicked" />

代码块:

private void OnCloseClicked(object sender, EventArgs e)
{
    Visibility = Visibility.Collapsed;
}

虽然我希望使用命令/MVVM来实现更好的支持,但我认为没有比使用事件更简单和更清晰的解决方案了。


6
我会尽力为您翻译中文,以下是需要翻译的内容:

我使用发布订阅模式来处理复杂的类依赖关系:

视图模型:

    public class ViewModel : ViewModelBase
    {
        public ViewModel()
        {
            CloseComand = new DelegateCommand((obj) =>
                {
                    MessageBus.Instance.Publish(Messages.REQUEST_DEPLOYMENT_SETTINGS_CLOSED, null);
                });
        }
}

窗口:

public partial class SomeWindow : Window
{
    Subscription _subscription = new Subscription();

    public SomeWindow()
    {
        InitializeComponent();

        _subscription.Subscribe(Messages.REQUEST_DEPLOYMENT_SETTINGS_CLOSED, obj =>
            {
                this.Close();
            });
    }
}

你可以利用Bizmonger.Patterns来获取MessageBus。 MessageBus
public class MessageBus
{
    #region Singleton
    static MessageBus _messageBus = null;
    private MessageBus() { }

    public static MessageBus Instance
    {
        get
        {
            if (_messageBus == null)
            {
                _messageBus = new MessageBus();
            }

            return _messageBus;
        }
    }
    #endregion

    #region Members
    List<Observer> _observers = new List<Observer>();
    List<Observer> _oneTimeObservers = new List<Observer>();
    List<Observer> _waitingSubscribers = new List<Observer>();
    List<Observer> _waitingUnsubscribers = new List<Observer>();

    int _publishingCount = 0;
    #endregion

    public void Subscribe(string message, Action<object> response)
    {
        Subscribe(message, response, _observers);
    }

    public void SubscribeFirstPublication(string message, Action<object> response)
    {
        Subscribe(message, response, _oneTimeObservers);
    }

    public int Unsubscribe(string message, Action<object> response)
    {
        var observers = new List<Observer>(_observers.Where(o => o.Respond == response).ToList());
        observers.AddRange(_waitingSubscribers.Where(o => o.Respond == response));
        observers.AddRange(_oneTimeObservers.Where(o => o.Respond == response));

        if (_publishingCount == 0)
        {
            observers.ForEach(o => _observers.Remove(o));
        }

        else
        {
            _waitingUnsubscribers.AddRange(observers);
        }

        return observers.Count;
    }

    public int Unsubscribe(string subscription)
    {
        var observers = new List<Observer>(_observers.Where(o => o.Subscription == subscription).ToList());
        observers.AddRange(_waitingSubscribers.Where(o => o.Subscription == subscription));
        observers.AddRange(_oneTimeObservers.Where(o => o.Subscription == subscription));

        if (_publishingCount == 0)
        {
            observers.ForEach(o => _observers.Remove(o));
        }

        else
        {
            _waitingUnsubscribers.AddRange(observers);
        }

        return observers.Count;
    }

    public void Publish(string message, object payload)
    {
        _publishingCount++;

        Publish(_observers, message, payload);
        Publish(_oneTimeObservers, message, payload);
        Publish(_waitingSubscribers, message, payload);

        _oneTimeObservers.RemoveAll(o => o.Subscription == message);
        _waitingUnsubscribers.Clear();

        _publishingCount--;
    }

    private void Publish(List<Observer> observers, string message, object payload)
    {
        Debug.Assert(_publishingCount >= 0);

        var subscribers = observers.Where(o => o.Subscription.ToLower() == message.ToLower());

        foreach (var subscriber in subscribers)
        {
            subscriber.Respond(payload);
        }
    }

    public IEnumerable<Observer> GetObservers(string subscription)
    {
        var observers = new List<Observer>(_observers.Where(o => o.Subscription == subscription));
        return observers;
    }

    public void Clear()
    {
        _observers.Clear();
        _oneTimeObservers.Clear();
    }

    #region Helpers
    private void Subscribe(string message, Action<object> response, List<Observer> observers)
    {
        Debug.Assert(_publishingCount >= 0);

        var observer = new Observer() { Subscription = message, Respond = response };

        if (_publishingCount == 0)
        {
            observers.Add(observer);
        }
        else
        {
            _waitingSubscribers.Add(observer);
        }
    }
    #endregion
}

}

Subscription

public class Subscription
{
    #region Members
    List<Observer> _observerList = new List<Observer>();
    #endregion

    public void Unsubscribe(string subscription)
    {
        var observers = _observerList.Where(o => o.Subscription == subscription);

        foreach (var observer in observers)
        {
            MessageBus.Instance.Unsubscribe(observer.Subscription, observer.Respond);
        }

        _observerList.Where(o => o.Subscription == subscription).ToList().ForEach(o => _observerList.Remove(o));
    }

    public void Subscribe(string subscription, Action<object> response)
    {
        MessageBus.Instance.Subscribe(subscription, response);
        _observerList.Add(new Observer() { Subscription = subscription, Respond = response });
    }

    public void SubscribeFirstPublication(string subscription, Action<object> response)
    {
        MessageBus.Instance.SubscribeFirstPublication(subscription, response);
    }
}

4
我曾经在这个问题上纠结了一段时间,最终选择了最简单的方法,并且仍与MVVM保持一致:使按钮执行完成所有繁重工作的命令,并使按钮的Click处理程序关闭窗口。
XAML
<Button x:Name="buttonOk" 
        Click="closeWindow" 
        Command="{Binding SaveCommand}" />

XAML.cs

public void closeWindow() 
{
    this.DialogResult = true;
}

SaveCommand.cs

 // I'm in my own file, not the code-behind!

没错,虽然还有一些代码在后台运行,但这并没有什么本质的不好。从面向对象的角度来看,让窗口自己关闭是最合理的。


4
我们在 .xaml 定义中有一个 name 属性:
x:Name="WindowsForm"

然后我们有一个按钮:
<Button Command="{Binding CloseCommand}" 
CommandParameter="{Binding ElementName=WindowsForm}" />

接下来在ViewModel中:

public DelegateCommand <Object>  CloseCommand { get; private set; }

Constructor for that view model:
this.CloseCommand = new DelegateCommand<object>(this.CloseAction);

然后最后,是行动方法:
private void CloseAction (object obj)
{
  Window Win = obj as Window;
  Win.Close();

}

我使用了这段代码来关闭应用程序中的弹出窗口。

4
这个任务有一个非常有用的行为,不会破坏MVVM,它是Expression Blend 3引入的一种称为“Behavior”的方法,允许View与完全在ViewModel中定义的命令进行连接。该行为演示了一种简单的技术,允许ViewModel在Model-View-ViewModel应用程序中管理View的关闭事件。这样,您可以在View(UserControl)中挂接一个行为,以便控制控件窗口,从而使ViewModel能够通过标准ICommands控制窗口是否可以关闭。使用行为允许ViewModel在M-V-VM中管理View生命周期 http://gallery.expression.microsoft.com/WindowCloseBehavior/ 上面的链接已经被存档到http://code.msdn.microsoft.com/Window-Close-Attached-fef26a66#content

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