在MVVM(Model-View-ViewModel)模式中,命令绑定是一个核心概念,它允许视图(View)与视图模型(ViewModel)之间的交互。本文将通过一个简单的示例应用程序,探讨在父子场景下如何进行命令绑定。这个应用程序允许用户添加、更新和删除品牌,以及在特定品牌下添加和删除产品。
示例应用程序可以在本文顶部下载。它是一个非常简单的应用程序,用户可以通过它添加、更新和删除品牌,以及在特定品牌下添加和删除产品。如果用户输入一个有效的品牌名称并点击“添加品牌”按钮,该品牌将被添加并显示在标签页中。这里使用了可编辑标题的标签控制(Header Editable Tab Control),在TabItem的HeaderTemplate中添加了一个删除按钮。
在开发应用程序时,首先需要识别视图(Views)和视图模型(ViewModels)。最初,可能会认为只有一个视图和一个视图模型(即BrandsView和BrandsViewModel)。但让深入研究一下。
现在清楚了吗?红色边框显示的是BrandsView和BrandsViewModel,绿色和蓝色边框分别识别的是SingleBrandView、SingleBrandViewModel和ProductView、ProductViewModel。让专注于类图:
父子关系很明显,BrandsViewModel包含一个SingleBrandViewModel的集合,而SingleBrandViewModel包含一个ProductViewModel的集合。因此,有三个视图对应这三个视图模型。让编写BrandsView的代码。
以下是BrandsView的XAML代码。它定义了两个DataTemplate,分别用于ProductViewModel和SingleBrandViewModel。
<UserControl x:Class="DemoApp.Views.BrandsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tab="clr-namespace:FormattedTabControl;assembly=FormattedTabControl"
xmlns:vm="clr-namespace:DemoApp.ViewModels"
xmlns:vw="clr-namespace:DemoApp.Views">
<UserControl.Resources>
<DataTemplate DataType="{x:Type vm:ProductViewModel}">
<vw:ProductView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SingleBrandViewModel}">
<vw:SingleBrandView />
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15*" />
<ColumnDefinition Width="70*" />
<ColumnDefinition Width="15*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Brand Name" Grid.Row="0" Grid.Column="0" Margin="5" />
<TextBox x:Name="txtBrandName" Grid.Row="0" Grid.Column="1" Margin="5" />
<Button Grid.Row="0" Grid.Column="2" Margin="5" Content="Add Brand" Command="{Binding AddBrand}" CommandParameter="{Binding ElementName=txtBrandName, Path=Text}" />
<tab:FormattedTab x:Name="tab" Grid.ColumnSpan="3" Grid.Row="1" ItemSource="{Binding Brands}" />
</Grid>
</UserControl>
绑定AddBrand命令很简单,但DeleteBrand命令呢?如果看视图模型的边框图,关闭按钮实际上在FormattedTabControl中。因此,应该在SingleBrandViewModel中放置DeleteBrand。好的,同意,但请记住SingleBrandViewModel集合在BrandsViewModel中。那么,如何执行删除操作呢?一个简单的答案是,在SingleBrandViewModel中放置一个事件,当命令执行时触发该事件,并在添加品牌时在BrandsViewModel中钩住该事件。这肯定可以工作,但品牌不应该引发一个事件来删除自己。这使得ViewModel类耦合并且难以测试。实际上可以使用FindAncestor或ElementName在Binding表达式中将命令绑定到父ViewModel,BrandViewModel。让看看如何在FormattedTabControl的ItemContainerStyle中做到这一点。
以下是FormattedTabControl的HeaderTemplate的样式。它包含两列,一列用于EditableTabHeaderControl,另一列用于关闭按钮。现在,必须将DeleteBrand命令绑定到这个按钮。FormattedTab的ItemSource绑定到BrandsViewModel的Brands集合。由于每个TabItem绑定到SingleBrandViewModel,因此从这个按钮,必须找到其祖先TabControl并绑定到祖先的DataContext。
<Style x:Key="TabItemHeaderContainerStyle" TargetType="TabItem">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:EditableTabHeaderControl Grid.Column="0" Style="{StaticResource EditableTabHeaderControl}">
<local:EditableTabHeaderControl.Content>
<Binding Path="Header" Mode="TwoWay" />
</local:EditableTabHeaderControl.Content>
</local:EditableTabHeaderControl>
<Button x:Name="cmdTabItemCloseButton" Style="{StaticResource TabItemCloseButtonStyle}" Grid.Column="1" Margin="15,0,0,0" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=DataContext.DeleteBrand}" CommandParameter="{Binding}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
对于产品视图中的删除按钮,其DataContext是ProductViewModel,但在SingleBrandViewModel中放置并绑定了DeleteProduct。因此,在ProductView的删除按钮上,目标祖先将是SingleBrandView中的ListBox。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ProductName}" Margin="5" />
<Button Grid.Column="1" Margin="5" Content="Remove" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}, Path=DataContext.DeleteProduct}" CommandParameter="{Binding}" />
</Grid>
在这两种情况下,请注意CommandParameter。整个绑定的对象作为参数传递,这使得命令的执行变得非常容易。让看看DeleteProduct命令的Execute方法:
private DelegateCommand<ProductViewModel> deleteProduct;
public DelegateCommand<ProductViewModel> DeleteProduct
{
get
{
return this.deleteProduct ?? (
this.deleteProduct = new DelegateCommand<ProductViewModel>(
this.ExecuteDeleteProduct,
(arg) => true
)
);
}
}
private void ExecuteDeleteProduct(ProductViewModel obj)
{
if (this.Products.Contains(obj))
{
this.Products.Remove(obj);
}
}
这就是在MVVM模式中,在父子场景下进行命令绑定的方式,同时保持ViewModels更易于测试。
解决方案包含两个项目,一个用于演示应用程序,另一个用于FormattedTabControl。
在这里,使用了Prism的DelegateCommand类。此外,对于自定义TabControl的关闭按钮的样式,参考了这篇四部分文章关于WPF TabControl。感谢Olaf Rabbachin撰写了这样一篇精彩的文章。