给一些MVVM中的视图命令
让我们想象我有一些用户控制。用户控件有一些子窗口。而用户控制用户想要关闭某种类型的子窗口。有一个在后面的用户控件代码的方法:给一些MVVM中的视图命令
public void CloseChildWindows(ChildWindowType type)
{
...
}
但我不能调用此方法,因为我没有看法的直接访问。
我想到的另一个解决方案是以某种方式将用户控件ViewModel作为其属性之一(这样我可以将它绑定并直接给ViewModel)。但我不希望用户控制用户知道关于用户控件ViewModel的任何信息。
那么解决这个问题的正确方法是什么?实现这一
一种方式是对视图模型来请求子窗口应关闭:
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
}
...
}
所以这里的视图模型可以确保子窗口关闭,而没有任何视图的知识。
我已经把在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
标志,并且管理器类处理打开/关闭任何新窗口。有几十个可能的方式来做到这一点,但它在紧要关头为我工作在过去(当实际执行和我的手机上扔在一起,这是)
我觉得我只是找到了一个相当不错的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>
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>
视图模型,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);
}
}
}
}
打开CloseWindowBehavior:
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
}
}
}
}
啊,我喜欢行为...... – JerKimball 2013-03-22 20:11:40
+1行为 – chrisw 2013-03-24 23:24:52
@adabyron,你为什么不把你的答案作为一个可下载的源代码? – RobinAtTech 2014-11-03 05:31:46
纯粹主义者的合理方式是创建一个处理导航的服务。简短摘要:创建NavigationService,在NavigationService上注册您的视图,并使用视图模型内的NavigationService进行导航。
实施例:
class NavigationService
{
private Window _a;
public void RegisterViewA(Window a) { _a = a; }
public void CloseWindowA() { a.Close(); }
}
要得到的NavigationService你一个参考可以使在其顶部(即INavigationService)的抽象和寄存器/经由的IoC得到它。更恰当地说,你甚至可以做两个抽象,一个包含注册方法(由视图使用)和一个包含执行器(由视图模型使用)的抽象。
对于您可以检查出吉尔Cleeren实施这在很大程度上依赖于国际奥委会更详细的例子:
http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx开始〇时36分30秒
大多数回答这个问题涉及到一个状态变量。由ViewModel控制,并且View对这个变量进行更改。这对于打开或关闭一个窗口,或者只是显示或隐藏某些控件而言是有用的,如有状态命令。尽管如此,它对无状态事件命令不起作用。您可能会在信号的上升沿触发一些操作,但需要再次将信号设置为低(错误),否则将不再触发。
我写了一篇关于ViewCommand pattern的文章,它解决了这个问题。它基本上是从视图到当前ViewModel的常规命令的反向。它涉及一个接口,每个ViewModel都可以实现向所有当前连接的视图发送命令。当DataContext属性更改时,可以扩展View以注册每个分配的ViewModel。此注册将视图添加到ViewModel中的视图列表。每当ViewModel需要在View中运行命令时,它会遍历所有已注册的视图并在其上运行该命令(如果存在)。这使用反射来查找View类中的ViewCommand方法,但在相反的方向上也是如此。
的ViewCommand方法在视图类:
public partial class TextItemView : UserControl
{
[ViewCommand]
public void FocusText()
{
MyTextBox.Focus();
}
}
这是从一个视图模型称为:
private void OnAddText()
{
ViewCommandManager.Invoke("FocusText");
}
的文章可以on my website和旧版本on CodeProject。
包含的代码(BSD许可证)提供了一些措施,以允许在代码混淆期间重命名方法。
您也可以将UserControl的DataContext设置为您的ViewModel,摆脱ViewModel公共属性。这需要在事件注册时进行一些转换,但这是一种很好的做法,因为在MVVM中,您需要将UserControl.DataContext设置为ViewModel。此外,请务必在调用ChildWindowsCloseRequested之前执行一些验证,否则您将收到异常。 – 2013-03-19 19:55:06
没错,我会更新我的答案,欢呼。 – 2013-03-20 17:22:46