在 MVVM 应用程序的父子场景中,DataTemplate 内的命令绑定






4.81/5 (15投票s)
讨论了一种在任何使用 MVVM 设计模式构建的 WPF 或 Silverlight 应用程序的 DataTemplates(父子场景)中绑定 Commands 的简单且可测试的方法。
引言
本文将解决开发人员在模型-视图-视图模型模式的父子场景中,在 DataTemplate
内部绑定命令时可能遇到的一個问题。预期读者对该模式有基本了解。演示应用程序在 Visual Studio 2010 中创建。
示例场景
本文讨论的演示应用程序可在页面顶部下载。它包含一个非常简单的应用程序,允许用户添加、更新和删除品牌,以及添加和删除品牌下的产品。如果用户输入有效的品牌名称并点击“添加品牌”按钮,该品牌将被添加并显示在选项卡中。我使用了可编辑标题选项卡控件(请参阅我之前关于此主题的文章,这次我编辑了 TabItem
的 HeaderTemplate
以添加删除按钮)。在选项卡内,用户可以添加和删除特定品牌下的产品。
现在,为了开发应用程序,我们首先识别 Views
和 ViewModels
。起初,人们可能会认为只有一个 View
和 Viewmodel
(即 BrandsView
和 BrandsViewModel
)。好吧,让我们更深入地看看。
现在清楚了吧?红色边框显示 BrandsView
和 BrandsViewModel
,绿色和蓝色边框分别标识(SingleBrandView
, SingleBrandViewModel
)和(ProductView
, ProductViewModel
)。现在让我们专注于类图。
父子关系很清楚,因为 BrandsViewModel
包含 SingleBrandViewModel
的集合,而 SingleBrandViewModel
又包含 ProductViewModel
的集合。因此,我们有三个 Views
对应于这三个 ViewModels
。让我们来编码 BrandsView
。
<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
呢?如果我们看看我们用边框标出的 Viewmodels
的图,关闭按钮实际上位于 FormattedTabControl
(这里使用的自定义 TabControl
是 FormattedTab
)内部。所以,似乎我们应该将 DeleteBrand
放在 SingleBrandViewModel
中。好的,我同意,但请记住我们的 SingleBrandViewModel
集合在 BrandsViewModel
中。那么,我们如何执行删除操作呢?一个简单的答案是,我们在 SingleBrandViewModel
中放一个事件,当命令执行时触发它,并在我们添加品牌时在 BrandsViewModel
中挂钩该事件。这肯定会奏效,但一个品牌不应该引发一个事件来删除自己。这使得 ViewModel
类耦合且难以测试。我们实际上可以使用 Binding 表达式中的 FindAncestor
或 ElementName
将命令与父 ViewModel
,即 BrandViewModel
绑定。让我们看看如何在 FormattedTabControl
的 ItemContainerStyle
中做到这一点。
<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>
这是 FormattedTab
的 HeaderTemplate
的样式。它包含两列,一列用于 EditableTabHeaderControl
,另一列用于关闭按钮。现在,我们需要将 DeleteBrand
命令绑定到此按钮。FormattedTab
的 ItemSource
与 BrandsViewModel
的 Brands
集合绑定。由于每个 TabItem
都与 SingleBrandViewModel
绑定,因此从这个按钮开始,我们需要找到它的祖先 TabControl
并绑定到祖先的 DataContext
。对于“删除”按钮,情况类似。它的 datacontext
是 ProductViewModel
,但我们将 DeleteProduct
放在 SingleBrandViewModel
中并对其进行绑定。因此,在 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 撰写了如此精彩的文章。
接下来是什么?
我很想听听您对此实现的看法。分享您的想法,请留言...
历史
- 2011 年 2 月 21 日:初始帖子