在MVVM中给View一些命令

19

让我们想象一下,我有一个用户控件。这个用户控件有一些子窗口。用户希望关闭某种类型的子窗口。在用户控件的代码后台有一个方法:

public void CloseChildWindows(ChildWindowType type)
{
   ...
}

但是我无法调用这个方法,因为我没有直接访问视图的权限。

我想到的另一个解决方案是以某种方式公开用户控制ViewModel作为其属性之一(这样我就可以绑定它并直接将命令传递给ViewModel)。但是我不希望用户控制的用户知道任何关于用户控制的ViewModel的信息。

那么,解决这个问题的正确方法是什么?

5个回答

48
我觉得我刚刚找到了一个相当不错的MVVM解决方案。我编写了一个行为,它公开了一种类型属性WindowType和一个布尔属性Open。将后者数据绑定使得ViewModel可以轻松地打开和关闭窗口,而无需知道任何关于View的信息。
必须喜欢这些行为... :)

enter image description here

Xaml:

<UserControl x:Class="WpfApplication1.OpenCloseWindowDemo"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApplication1"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <local:ViewModel />
    </UserControl.DataContext>
    <i:Interaction.Behaviors>
        <!-- TwoWay binding is necessary, otherwise after user closed a window directly, it cannot be opened again -->
        <local:OpenCloseWindowBehavior WindowType="local:BlackWindow" Open="{Binding BlackOpen, Mode=TwoWay}" />
        <local:OpenCloseWindowBehavior WindowType="local:YellowWindow" Open="{Binding YellowOpen, Mode=TwoWay}" />
        <local:OpenCloseWindowBehavior WindowType="local:PurpleWindow" Open="{Binding PurpleOpen, Mode=TwoWay}" />
    </i:Interaction.Behaviors>
    <UserControl.Resources>
        <Thickness x:Key="StdMargin">5</Thickness>
        <Style TargetType="Button" >
            <Setter Property="MinWidth" Value="60" />
            <Setter Property="Margin" Value="{StaticResource StdMargin}" />
        </Style>
        <Style TargetType="Border" >
            <Setter Property="Margin" Value="{StaticResource StdMargin}" />
        </Style>
    </UserControl.Resources>

    <Grid>
        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <Border Background="Black" Width="30" />
                <Button Content="Open" Command="{Binding OpenBlackCommand}" CommandParameter="True" />
                <Button Content="Close" Command="{Binding OpenBlackCommand}" CommandParameter="False" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Border Background="Yellow" Width="30" />
                <Button Content="Open" Command="{Binding OpenYellowCommand}" CommandParameter="True" />
                <Button Content="Close" Command="{Binding OpenYellowCommand}" CommandParameter="False" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Border Background="Purple" Width="30" />
                <Button Content="Open" Command="{Binding OpenPurpleCommand}" CommandParameter="True" />
                <Button Content="Close" Command="{Binding OpenPurpleCommand}" CommandParameter="False" />
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

黄窗口(黑/紫色相似):


<Window x:Class="WpfApplication1.YellowWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="YellowWindow" Height="300" Width="300">
    <Grid Background="Yellow" />
</Window>

ViewModel, ActionCommand:

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace WpfApplication1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool _blackOpen;
        public bool BlackOpen { get { return _blackOpen; } set { _blackOpen = value; OnPropertyChanged("BlackOpen"); } }

        private bool _yellowOpen;
        public bool YellowOpen { get { return _yellowOpen; } set { _yellowOpen = value; OnPropertyChanged("YellowOpen"); } }

        private bool _purpleOpen;
        public bool PurpleOpen { get { return _purpleOpen; } set { _purpleOpen = value; OnPropertyChanged("PurpleOpen"); } }

        public ICommand OpenBlackCommand { get; private set; }
        public ICommand OpenYellowCommand { get; private set; }
        public ICommand OpenPurpleCommand { get; private set; }


        public ViewModel()
        {
            this.OpenBlackCommand = new ActionCommand<bool>(OpenBlack);
            this.OpenYellowCommand = new ActionCommand<bool>(OpenYellow);
            this.OpenPurpleCommand = new ActionCommand<bool>(OpenPurple);
        }

        private void OpenBlack(bool open) { this.BlackOpen = open; }
        private void OpenYellow(bool open) { this.YellowOpen = open; }
        private void OpenPurple(bool open) { this.PurpleOpen = open; }

    }

    public class ActionCommand<T> : ICommand
    {
        public event EventHandler CanExecuteChanged;
        private Action<T> _action;

        public ActionCommand(Action<T> action)
        {
            _action = action;
        }

        public bool CanExecute(object parameter) { return true; }

        public void Execute(object parameter)
        {
            if (_action != null)
            {
                var castParameter = (T)Convert.ChangeType(parameter, typeof(T));
                _action(castParameter);
            }
        }
    }
}

OpenCloseWindowBehavior:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace WpfApplication1
{
    public class OpenCloseWindowBehavior : Behavior<UserControl>
    {
        private Window _windowInstance;

        public Type WindowType { get { return (Type)GetValue(WindowTypeProperty); } set { SetValue(WindowTypeProperty, value); } }
        public static readonly DependencyProperty WindowTypeProperty = DependencyProperty.Register("WindowType", typeof(Type), typeof(OpenCloseWindowBehavior), new PropertyMetadata(null));

        public bool Open { get { return (bool)GetValue(OpenProperty); } set { SetValue(OpenProperty, value); } }
        public static readonly DependencyProperty OpenProperty = DependencyProperty.Register("Open", typeof(bool), typeof(OpenCloseWindowBehavior), new PropertyMetadata(false, OnOpenChanged));

        /// <summary>
        /// Opens or closes a window of type 'WindowType'.
        /// </summary>
        private static void OnOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var me = (OpenCloseWindowBehavior)d;
            if ((bool)e.NewValue)
            {
                object instance = Activator.CreateInstance(me.WindowType);
                if (instance is Window)
                {
                    Window window = (Window)instance;
                    window.Closing += (s, ev) => 
                    {
                        if (me.Open) // window closed directly by user
                        {
                            me._windowInstance = null; // prevents repeated Close call
                            me.Open = false; // set to false, so next time Open is set to true, OnOpenChanged is triggered again
                        }
                    }; 
                    window.Show();
                    me._windowInstance = window;
                }
                else
                {
                    // could check this already in PropertyChangedCallback of WindowType - but doesn't matter until someone actually tries to open it.
                    throw new ArgumentException(string.Format("Type '{0}' does not derive from System.Windows.Window.", me.WindowType));
                }
            }
            else 
            {
                if (me._windowInstance != null)
                    me._windowInstance.Close(); // closed by viewmodel
            }
        }
    }
}

1
@adabyron,为什么不把你的答案作为可下载的源代码呢? - RobinAtTech
2
喜欢这个解决方案!我将其更改为Behavior<ContentControl>,以便能够在Windows和UserControls中使用它。 - Petter
2
我已经多次在其他地方看到了这个答案的链接。既然我一直回来这里,我想指出的是,唯一让我困惑的是它的原因...也就是说,“为什么我要使用这个而不是服务/信使方法?” - DonBoitnott
2
@DonBoitnott 这种方法提供了一些显著的好处。考虑使用服务/信使方法。突然间,您希望为窗口定义一个PlacementTarget(例如鼠标位置或相对于UIControl的左/右/上/下等)。或者您希望为Popup准备偏移量或其他技巧。在服务/信使方法中,这些必须作为服务的公共API中的参数公开,更不用说服务可能会被调用,例如从视图模型内部调用,而该视图模型对UI一无所知。( - Vector Sigma
2
@DonBoitnott () 服务方法(这里这里)通常只停留在传递一个object,它应该是要分配的Window.Content。使用答案的方法还可以让您简单地注册依赖属性,例如PlacementTarget(例如UIElement),突然之间,您可以在XAML中设置它(在那里所有UI信息都可用)。因此,您可以同时从ViewModelUserControl设置内容! - Vector Sigma
显示剩余3条评论

6

我曾经通过引入一个名为WindowManager的概念来处理这种情况,这是一个非常糟糕的名称,因此我们将其与WindowViewModel配对,这个名称略微好些,但基本思想是:

public class WindowManager
{
    public WindowManager()
    {
        VisibleWindows = new ObservableCollection<WindowViewModel>();
        VisibleWindows.CollectionChanged += OnVisibleWindowsChanged;            
    }
    public ObservableCollection<WindowViewModel> VisibleWindows {get; private set;}
    private void OnVisibleWindowsChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        // process changes, close any removed windows, open any added windows, etc.
    }
}

public class WindowViewModel : INotifyPropertyChanged
{
    private bool _isOpen;
    private WindowManager _manager;
    public WindowViewModel(WindowManager manager)
    {
        _manager = manager;
    }
    public bool IsOpen 
    { 
        get { return _isOpen; } 
        set 
        {
            if(_isOpen && !value)
            {
                _manager.VisibleWindows.Remove(this);
            }
            if(value && !_isOpen)
            {
                _manager.VisibleWindows.Add(this);
            }
            _isOpen = value;
            OnPropertyChanged("IsOpen");
        }
    }    

    public event PropertyChangedEventHandler PropertyChanged = delegate {};
    private void OnPropertyChanged(string name)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

注意:我只是匆忙地把这个东西拼凑在一起;当然,您肯定希望根据自己的具体需求调整这个想法。
但是,基本前提是您的命令可以在WindowViewModel对象上工作,适当地切换IsOpen标志,而管理器类处理打开/关闭任何新窗口。有数十种可能的方法来实现这一点,但在过去(当实际实现而不是在手机上拼凑时)对我来说已经足够了。

5

对于纯粹主义者来说,一种合理的方式是创建一个处理导航的服务。简要概述:创建一个NavigationService,将您的视图注册到NavigationService,并在视图模型中使用NavigationService进行导航。

示例:

class NavigationService
{
    private Window _a;

    public void RegisterViewA(Window a) { _a = a; }

    public void CloseWindowA() { a.Close(); }
}

要获取NavigationService的引用,您可以在其上创建一个抽象层(即INavigationService),并通过IoC进行注册/获取。更好的方法甚至是制作两个抽象层,一个包含注册方法(由视图使用),另一个包含执行器(由视图模型使用)。
有关更详细的示例,您可以查看Gill Cleeren的实现,它严重依赖于IoC: http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx,从00:36:30开始

4

实现这一点的一种方法是让视图模型请求关闭子窗口:

public class ExampleUserControl_ViewModel
{
    public Action ChildWindowsCloseRequested;

    ...
}

视图将订阅其视图模型的事件,并在触发时负责关闭窗口。
public class ExampleUserControl : UserControl
{
    public ExampleUserControl()
    {
        var viewModel = new ExampleUserControl_ViewModel();
        viewModel.ChildWindowsCloseRequested += OnChildWindowsCloseRequested;

        DataContext = viewModel;
    }

    private void OnChildWindowsCloseRequested()
    {
        // ... close child windows
    }

    ...
}

因此,在这里,视图模型可以确保子窗口在不了解视图的情况下关闭。

4
您还可以将UserControl的DataContext设置为您的ViewModel,从而摆脱ViewModel公共属性。这将要求在事件注册过程中进行一些转换,但这是一种良好的实践,因为在MVVM中,您需要将UserControl.DataContext设置为ViewModel。此外,在调用ChildWindowsCloseRequested之前,请务必对其进行一些验证,以确保它不为空,否则您将会收到一个异常。 - Michael Sanderson

2
大多数回答这个问题的方法都涉及到一个状态变量,由ViewModel控制,View对此变量的更改进行操作。这对于像打开或关闭窗口,或者仅仅是显示或隐藏一些控件之类的有状态命令非常有效。但是,对于无状态事件命令来说,它并不起作用。你可以在信号上升沿触发一些操作,但需要再将信号设置为低(false)才能再次触发。

我写了一篇关于ViewCommand模式的文章,它解决了这个问题。它基本上是正常命令的反向方向,正常命令从View到当前的ViewModel。它涉及到一个接口,每个ViewModel都可以实现该接口,向所有当前连接的View发送命令。当其DataContext属性更改时,可以扩展View以注册到每个分配的ViewModel。这个注册将View添加到ViewModel中的View列表中。每当ViewModel需要在View中运行命令时,它通过所有已注册的View,并在其中运行该命令(如果存在)。这利用了反射来查找View类中的ViewCommand方法,但双向绑定也是如此。

View类中的ViewCommand方法:

public partial class TextItemView : UserControl
{
    [ViewCommand]
    public void FocusText()
    {
        MyTextBox.Focus();
    }
}

这是从ViewModel调用的:

private void OnAddText()
{
    ViewCommandManager.Invoke("FocusText");
}

这篇文章可以在我的网站找到,也有一个旧版本在CodeProject上
所包含的代码(BSD许可证)提供了措施,在代码混淆期间允许重命名方法。

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