65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (15投票s)

2011年2月21日

CPOL

3分钟阅读

viewsIcon

121066

downloadIcon

2829

讨论了一种在任何使用 MVVM 设计模式构建的 WPF 或 Silverlight 应用程序的 DataTemplates(父子场景)中绑定 Commands 的简单且可测试的方法。

引言

本文将解决开发人员在模型-视图-视图模型模式的父子场景中,在 DataTemplate 内部绑定命令时可能遇到的一個问题。预期读者对该模式有基本了解。演示应用程序在 Visual Studio 2010 中创建。

示例场景

本文讨论的演示应用程序可在页面顶部下载。它包含一个非常简单的应用程序,允许用户添加、更新和删除品牌,以及添加和删除品牌下的产品。如果用户输入有效的品牌名称并点击“添加品牌”按钮,该品牌将被添加并显示在选项卡中。我使用了可编辑标题选项卡控件(请参阅我之前关于此主题的文章,这次我编辑了 TabItem HeaderTemplate 以添加删除按钮)。在选项卡内,用户可以添加和删除特定品牌下的产品。

Sample application to add brand and products

现在,为了开发应用程序,我们首先识别 Views ViewModels 。起初,人们可能会认为只有一个 View Viewmodel (即 BrandsViewBrandsViewModel)。好吧,让我们更深入地看看。

ViewModels identified

现在清楚了吧?红色边框显示 BrandsViewBrandsViewModel,绿色和蓝色边框分别标识(SingleBrandView, SingleBrandViewModel)和(ProductView, ProductViewModel)。现在让我们专注于类图。

Class Diagram of ViewModels

父子关系很清楚,因为 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 表达式中的 FindAncestorElementName 将命令与父 ViewModel,即 BrandViewModel 绑定。让我们看看如何在 FormattedTabControlItemContainerStyle 中做到这一点。

<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>

这是 FormattedTabHeaderTemplate 的样式。它包含两列,一列用于 EditableTabHeaderControl,另一列用于关闭按钮。现在,我们需要将 DeleteBrand 命令绑定到此按钮。FormattedTabItemSource BrandsViewModelBrands 集合绑定。由于每个 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 日:初始帖子
© . All rights reserved.