使用MVVM实现“关闭窗口”命令

44
所以我的第一次尝试是在代码后端完成所有操作,现在我正在尝试重构我的代码以使用MVVM模式,遵循MVVM in the box信息的指导。
我创建了一个与我的视图类匹配的视图模型类,并且正在将代码从代码后端移动到视图模型,从命令开始。
我的第一个问题是尝试实现一个“关闭”按钮,如果数据没有被修改,则关闭窗口。 我已经设置了一个CloseCommand来替换“onClick”方法,一切都很好,除了代码尝试运行this.Close()的地方。 显然,由于代码已从窗口移动到普通类,'this'不是一个窗口,因此无法关闭。 但是,根据MVVM,视图模型不知道视图,因此我无法调用view.Close()
有人能建议我如何从视图模型命令关闭窗口吗?

1
已经讨论了几个选项[https://dev59.com/B2855IYBdhLWcg3wYTP_][在这里]。通常我会使用CommandParameter和相对源返回到调用的窗口的方法。(正如Simone所演示的那样) - Steve Py
这个解决方案需要使用Expression Blend吗?我在这方面有些困难。 - mcalex
请查看此处如何使用附加属性来解决此问题:http://blog.excastle.com/2010/07/25/mvvm-and-dialogresult-with-no-code-behind/ - dvvrd
13个回答

68

我个人使用一个非常简单的方法:针对与可关闭视图相关的每个ViewModel,我创建了一个基本ViewModel,如以下示例:

public abstract class CloseableViewModel
{
    public event EventHandler ClosingRequest;

    protected void OnClosingRequest()
    {
        if (this.ClosingRequest != null)
        {
            this.ClosingRequest(this, EventArgs.Empty);
        }
    }
}

在您的继承自CloseableViewModel的ViewModel中,仅需为Close命令调用this.OnClosingRequest();

在视图中:

public class YourView
{
    ...
    var vm = new ClosableViewModel();
    this.Datacontext = vm;
    vm.ClosingRequest += (sender, e) => this.Close();
}

1
我认为这是一个非常好的做法! - Ben Pretorius
1
不错的解决方案,非常实用。 - Lucy82

29

您无需将View实例传递到ViewModel层。您可以像这样访问主窗口 -

Application.Current.MainWindow.Close()

根据上述所述,我认为在ViewModel类中访问主窗口没有问题。根据MVVM原则,您的View和ViewModel之间不应该有紧密耦合的关系,即它们应该对彼此的操作毫不知情。在这里,我们不会从View传递任何东西到ViewModel。如果您想寻找其他选项,可能会帮助您 - 使用MVVM关闭窗口


4
我喜欢这个,但是视图模型和应用程序之间的耦合是否被允许或批准? - mcalex
3
耦合是指使用实例变量在层间传递数据,但在这里,您正在访问应用程序的静态属性以获取窗口。对我来说,这不违反MVVM的任何规则。 - Rohit Vats
15
@Rohit:您可以通过以下方式将您的视图模型与WPF相结合。(应用程序类) - g.pickardou
8
@RohitVats 这是违反MVVM原则的。将视图和视图模型分开有助于提高可移植性:我可以实现控制台应用程序、后台进程、SilverLight网站、移动应用程序,只需更改视图而不更改任何视图模型即可。因此,在视图模型中调用类似“MainWindow”的内容是完全错误的。在后台进程中,“关闭主窗口”是什么意思?或者在测试用例套件中呢?如果要关闭的窗口不是主窗口呢?在不违反MVVM原则的情况下处理这些问题的最佳方法是通过接口将视图实例注入到视图模型属性中。 - Massimiliano Kraus
这个解决方案对我抛出了一个异常。 - abdou_dev
显示剩余2条评论

26

我在视图模型中想要通过点击按钮关闭窗口的解决方案如下:

在视图模型中:

public RelayCommand CloseWindow;
Constructor()
{
    CloseWindow = new RelayCommand(CloseWin);
}

public void CloseWin(object obj)
{
    Window win = obj as Window;
    win.Close();
}

在View中,按如下设置

<Button Command="{Binding CloseWindowCommand}" CommandParameter="{Binding ElementName=WindowNameTobeClose}" Content="Cancel" />

同意,这样可以节省在视图中编写代码(可能会被遗忘!),也不会强制你在视图中执行DataContext分配。 - cjb110
9
你是否让视图模型(View-Model)意识到了视图(窗口)? - matt-pielat
我总是遇到空引用异常,因为在我的情况下obj为空?但是我在XAML中使用了CommandParameter="{Binding ElementName=myWindow}",可能出了什么问题? - CeOnSql
您没有给窗口命名。 - lorengphd

13

我通过创建一个名为 DialogResult 的附加属性来实现这一点:

public static class DialogCloser
{
    public static readonly DependencyProperty DialogResultProperty =
        DependencyProperty.RegisterAttached(
            "DialogResult",
            typeof(bool?),
            typeof(DialogCloser),
            new PropertyMetadata(DialogResultChanged));

    private static void DialogResultChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var window = d as Window;
        if (window != null && (bool?)e.NewValue == true) 
                window.Close();
    }

    public static void SetDialogResult(Window target, bool? value)
    {
        target.SetValue(DialogResultProperty, value);
    }
}

然后将这个写入你的XAML,在window标签中

WindowActions:DialogCloser.DialogResult="{Binding Close}"

最后在ViewModel中执行

    private bool _close;
    public bool Close
    {
        get { return _close; }
        set
        {
            if (_close == value)
                return;
            _close = value;
            NotifyPropertyChanged("Close");
        }
    }

如果您将Close更改为true,则窗口将被关闭。

Close = True;

谢谢,我会尝试一下。NotifyPropertyChanged位于哪里,它是什么样子的? - mcalex

7

这是最简单和纯净的MVVM解决方案

视图模型代码

public class ViewModel
{
    public Action CloseAction { get; set; }

    private void CloseCommandFunction()
    {
        CloseAction();
    }
}

这是XAML视图代码。
public partial class DialogWindow : Window
{
    public DialogWindow()
    {
        ViewModel vm = new ViewModel();
        this.DataContext = vm;

        vm.CloseAction = Close;
    }
}

simple and effective :) - freakydinde
非常好的方法! - Amir Touitou
1
最后一行可以简化为 vm.CloseAction = Close; - Mustafa Özçetin

4
这个解决方案快速简便,但是不足之处在于各层之间存在一定的耦合性。
在你的视图模型中:
public class MyWindowViewModel: ViewModelBase
{


    public Command.StandardCommand CloseCommand
    {
        get
        {
            return new Command.StandardCommand(Close);
        }
    }
    public void Close()
    {
        foreach (System.Windows.Window window in System.Windows.Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
            }
        }
    }
}

我也喜欢这种方法,但有点担心可扩展性。 - cjb110

4

使用自定义消息通知的MVVM-light,以避免窗口处理每个通知消息

在ViewModel中:

public class CloseDialogMessage : NotificationMessage
{
    public CloseDialogMessage(object sender) : base(sender, "") { }
}

private void OnClose()
{
    Messenger.Default.Send(new CloseDialogMessage(this));
}

在窗口构造函数中注册消息:
Messenger.Default.Register<CloseDialogMessage>(this, nm =>
{
    Close();
});

2
使用 MVVM-light 工具包:
在 ViewModel 中:
 public void notifyWindowToClose()
{
    Messenger.Default.Send<NotificationMessage>(
        new NotificationMessage(this, "CloseWindowsBoundToMe")
    );
}

在视图中:

 Messenger.Default.Register<NotificationMessage>(this, (nm) =>
{
    if (nm.Notification == "CloseWindowsBoundToMe")
    {
        if (nm.Sender == this.DataContext)
            this.Close();
    }
});

2

这与eoldre的答案非常相似。它的功能相同,因为它查找具有视图模型作为其数据上下文的窗口的Windows集合; 但我使用了RelayCommand和一些LINQ来实现相同的结果。

public RelayCommand CloseCommand
{
    get
    {
        return new RelayCommand(() => Application.Current.Windows
            .Cast<Window>()
            .Single(w => w.DataContext == this)
            .Close());
    }
}

0
这是从ken2k的回答中提取的(感谢!),只需将 CloseCommand 添加到基本的 CloseableViewModel 中即可。
public class CloseableViewModel
{
    public CloseableViewModel()
    {
        CloseCommand = new RelayCommand(this.OnClosingRequest);
    }

    public event EventHandler ClosingRequest;

    protected void OnClosingRequest()
    {
        if (this.ClosingRequest != null)
        {
            this.ClosingRequest(this, EventArgs.Empty);
        }
    }

    public RelayCommand CloseCommand
    {
        get;
        private set;
    }
}

你的视图模型,继承它

public class MyViewModel : CloseableViewModel

然后在你的视图上

public MyView()
{
    var viewModel = new StudyDataStructureViewModel(studyId);
    this.DataContext = viewModel;

    //InitializeComponent(); ...

    viewModel.ClosingRequest += (sender, e) => this.Close();
}

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