MVVM模式下命令绑定的深入探讨

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的代码

以下是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的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撰写了这样一篇精彩的文章。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485