18

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

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

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

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

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

4

5 回答 5

46

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

必须爱行为... :)

在此处输入图像描述

xml:

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

YellowWindow(黑色/紫色类似):

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

视图模型,动作命令:

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
            }
        }
    }
}
于 2013-03-20T00:14:51.200 回答
6

过去我通过引入 a 的概念来处理这种情况WindowManager,这是一个可怕的名字,所以让我们将它与 a 配对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标志,并且管理器类处理打开/关闭任何新窗口。有几十种可能的方法可以做到这一点,但它在过去对我来说很有效(当实际实施而不是在我的手机上扔在一起时)

于 2013-03-19T20:12:24.600 回答
5

对于纯粹主义者来说,一个合理的方法是创建一个处理您的导航的服务。简短摘要:创建一个 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

于 2013-03-21T13:10:21.613 回答
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
    }

    ...
}

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

于 2013-03-17T19:47:18.760 回答
2

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

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

View 类中的 ViewCommand 方法:

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

这是从 ViewModel 调用的:

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

这篇文章可以在我的网站上找到,也可以在 CodeProject 上找到旧版本。

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

于 2015-09-08T07:29:00.387 回答