相较于上一篇的数据绑定问题,这次也提及相关的问题,只不过是在View层面追溯到ViewModel的绑定问题。这次主要是关于RelayCommand,以及数据绑定的问题。
在实际运用之中,我们时常需要把一个程序的一个空间包装成一个用户控件来单独编写,再添加到主窗体之中,不少这种用户控件内部都需要有指令(Command)来触发一系列动作,但是主Model和主ViewModel都在MainWindow的工程之中,这就涉及到用户控件与主窗体在MVVM模式中事件的触发与绑定。
首先假设我们有一个Menu的UserControl,这个用户控件里面只有一个Menu,然后把这个工程引用到主窗体之中,并且把空间命名为uc,那么它在主窗体的引用如下:
<uc:MenuControl Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="3"
DataContext="{Binding Menu}">
<!-- Others -->
</uc:MenuControl>
十分简单明了的引用,其中DataContext绑定的MenuControl在主窗体之中的ViewModel,这个ViewModel负责读取总体的Model以及所有MenuControl的逻辑。
现在架设一个情况,我们在点击Menu里的一个MenuItem之后需要出发一个指令,改变一个值,并且通知主窗体来完成一些逻辑。在之前的文章中我们已经说过,值在用户控件与主窗体之间的绑定依靠的是暴露成依赖属性来绑定到主窗体的ViewModel上,通过设定绑定的Mode来达到各种效果,在此就不贴代码了。那么问题就来了,UserControl本身就是一个黑盒,他自身内部的ViewModel实现的逻辑并不能与主窗体进行交互。这个时候采取一个比较巧妙的办法,我们在UserControl的后台文件(MenuControl.xaml.cs)后台定义一个事件,在View中触发这个指令的时候我们让这个指令执行这个事件,自定义路由事件(RoutedEvent)是与依赖属性一样暴露在控件之外的,可以与ViewModel中的属性绑定,再通过MVVM模式中EventToCommand标签来把事件转化为指令,再将这个指令与总窗体的ViewModel里的RelayCommand所绑定,就达到的传出的目的:
<uc:MenuControl Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="3"
DataContext="{Binding Menu}">
<i:EventTrigger EventName="EventInMenuControl">
<command:EventToCommand Command="{Binding RelayCommandInMainWindow}"></command:EventToCommand>
</i:EventTrigger>
</uc:MenuControl>
其中在后台代码中定义路由事件:
public event EventHandler EventInMenuControl
{
add { AddHandler(EventInMenuControlEvent, value); }
remove { RemoveHandler(EventInMenuControlEvent, value); }
}
public static readonly RoutedEvent EventInMenuControlEvent = EventManager.RegisterRoutedEvent(
"EventInMenuControl", RoutingStrategy.Bubble, typeof(EventHandler), typeof(MapEditorControl));
而在绑定到主窗体的RelayCommand的时候有一个MVVM小bug,一开始我遇到也是百思不得其解,后来Revert之后逐步复位才找出。
假设在主窗体MenuViewModel.cs文件中定义的RelayCommandInMainWindow如下:
public class MenuViewModel : ViewModelBase
{
public RelayCommand RelayCommandInMainWindow { get; set; }
private XmlDocument File;
public MenuViewModel()
{
RelayCommandInMainWindow = new RelayCommand(() =>
{
File = new XmlDocument();
File.Load("D:\\File.xml");
});
}
}
在这种情况下触发这个RelayCommand的时候这个指令并不会被执行,这个是MVVM不支持弱引用的一个BUG,不能闭包引用外围变量,需要在内部定义,这个bug在5.4.0中得到了解决,但是5.4.0一直是alpha版本,详情见MVVM ToolKit。
===========================================================
接下来继续讨论MenuControl内部的问题,我们在上一篇说过动态绑定数据并显示的方法,但是运用到目录中就会遇到问题,先看代码:
<Menu Margin="0,0,0,0" SnapsToDevicePixels="True" DataContext="{Binding Source={StaticResource Locator}, Path=MenuControl}">
<MenuItem Header="Test " ItemsSource="{Binding MenuItem}">
<MenuItem.ItemTemplate>
<DataTemplate>
<MenuItem Header="{Binding MenuItemText}" Command="{Binding MenuCommand}"/>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
</Menu>
按照我们上次说的,这个代码已经完美的完成了绑定,的确他可以很好的显示动态数据的内容,但是你会发现指令并不会被执行。因为在MenuItem处已经指定了ItemsSource的内容,下面所有的绑定内容都会从ItemsSource绑定的集合中寻找,所以Header能够直接绑定元素名称,但是内部并没有MenuCommand这个东西。在这里需要用到RelativeSource的内容,先贴代码:
<MenuItem Header="Test" ItemsSource="{Binding MenuItem}">
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command"
Value="{Binding DataContext.MenuCommand,
RelativeSource={RelativeSource AncestorType={x:Type MenuItem},
Mode=FindAncestor, AncestorLevel=1}}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
用RelativeSource寻找Item的祖先,把对象定义为Menu中的一个MenuItem,然后绑定到这个资源的DataContext.MenuCommand就行了,其实是很简单的问题,主要看书。
引以为戒。
欢迎指正