【问题标题】:Give some command to View in MVVM给出一些命令在 MVVM 中查看
【发布时间】:2013-03-06 02:25:29
【问题描述】:

假设我有一些用户控制。用户控件有一些子窗口。并且用户控制用户想要关闭某种类型的子窗口。用户控制代码后面有个方法:

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

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

我考虑的另一个解决方案是以某种方式将用户控件 ViewModel 公开为其属性之一(这样我就可以绑定它并直接向 ViewModel 发出命令)。但我不希望用户控件用户了解用户控件 ViewModel 的任何信息。

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

【问题讨论】:

    标签: c# .net wpf mvvm


    【解决方案1】:

    我觉得我刚刚找到了一个相当不错的 MVVM 解决方案来解决这个问题。我写了一个暴露类型属性WindowType 和布尔属性Open 的行为。后者的 DataBinding 允许 ViewModel 轻松打开和关闭窗口,而无需了解有关 View 的任何信息。

    必须热爱行为...... :)

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

    打开关闭窗口行为:

    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
                }
            }
        }
    }
    

    【讨论】:

    • @adabyron,你为什么不把你的答案作为可下载的源代码给出?
    • 喜欢这个解决方案!我将其更改为 Behavior 以便能够将它用于 Windows 和 UserControls
    • 我遇到过几次与其他人链接的这个答案。因此,既然我一直回到这里,我会指出我唯一一直想念的是它的原因......就像“我为什么要在服务/信使方法上使用它?”
    • @DonBoitnott 这种方法提供了一些显着的好处。考虑使用服务/信使方法。突然,您希望为窗口定义一个PlacementTarget(例如鼠标位置,或相对于 UIControl 的左/右/上/下等)。或者,例如,您希望定义偏移量或 Popup 的其他技巧。在服务/信使方法中,这些必须公开作为服务的公共 API 中的参数,更不用说可以调用服务了,例如,从不知道关于用户界面。 (
    • @DonBoitnott (continued) 服务方法(无论如何,oneshere 周围)通常会在传递object 时停止,这应该是@987654333 @ 被分配。使用 this 答案的方法还使您能够简单地注册一个依赖属性,例如PlacementTarget(例如UIElement),并且突然间,您可以在 XAML 中设置它(所有 UI 信息都可用)。因此,您可以同时设置 ViewModelUserControl 中的内容!
    【解决方案2】:

    我过去曾通过引入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 标志,并且管理器类处理打开/关闭任何新窗口。有很多可能的方法可以做到这一点,但它在过去对我来说很有效(当实际实施而不是在我的手机上扔在一起时)

    【讨论】:

      【解决方案3】:

      对于纯粹主义者来说,一个合理的方法是创建一个服务来处理您的导航。简短的总结:创建一个 NavigationService,在 NavigationService 中注册您的视图并使用视图模型中的 NavigationService 进行导航。

      例子:

      class NavigationService
      {
          private Window _a;
      
          public void RegisterViewA(Window a) { _a = a; }
      
          public void CloseWindowA() { a.Close(); }
      }
      

      要获得 NavigationService 的引用,您可以在其之上进行抽象(即 INavigationService)并通过 IoC 注册/获取它。更恰当地说,您甚至可以进行两种抽象,一种包含注册方法(由视图使用),另一种包含执行器(由视图模型使用)。

      对于更详细的示例,您可以查看严重依赖 IoC 的 Gill Cleeren 的实现:

      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
            }
        
            ...
        }
        

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

        【讨论】:

        • 您还可以将 UserControl 的 DataContext 设置为您的 ViewModel,摆脱 ViewModel 公共属性。这将需要对事件注册进行一些强制转换,但这是一个很好的做法,因为在 MVVM 中,无论如何您都需要将 UserControl.DataContext 设置为 ViewModel。此外,请务必在调用 ChildWindowsCloseRequested 之前执行一些验证,以确保它不为空,否则您将收到异常。
        【解决方案5】:

        这个问题的大多数答案都涉及到一个由 ViewModel 控制的状态变量,而 View 会根据这个变量的变化而采取行动。这适用于有状态命令,例如打开或关闭窗口,或者只是显示或隐藏某些控件。但它不适用于 无状态事件命令。您可以在信号的上升沿触发一些动作,但需要再次将信号设置为低电平(假),否则它将永远不会再次触发。

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

        View类中的ViewCommand方法:

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

        这是从 ViewModel 调用的:

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

        文章在on my website 和旧版本on CodeProject 可用。

        包含的代码(BSD 许可证)提供了在代码混淆期间允许重命名方法的措施。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-08-26
          • 2011-11-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多