Introduction
This article will address one of the problems a developer might run into when binding commands inside DataTemplate
in Parent-Child scenario in Model-View-ViewModel pattern. Basic knowledge of this pattern is expected. The demo application is created in Visual Studio 2010.
Example Scenario
The demo application discussed in this article is available for download at the top of this page. It contains a very simple application that allows a user to add, update and remove brand and add, remove product in a brand. If the user types a valid brand name and hits Add Brand button, the brand will be added and shown in the tabs. I have used Header Editable Tab Control (see my previous article on this, this time I edited the HeaderTemplate
of TabItem
to add a delete button). Inside a tab, the user can add and remove product under specific brand.
Now, to develop the application, let's identify the Views
and ViewModels
first. Initially, one can say there is only one View
and Viewmodel
(i.e. BrandsView
and BrandsViewModel
). Well, let's have a deeper look.
It's clear now, right? The red border shows BrandsView
and BrandsViewModel
, the green and blue border identify (SingleBrandView
, SingleBrandViewModel
) and (ProductView
, ProductViewModel
) respectively. Let's concentrate on the class diagram now:
The parent-child relation is clear as BrandsViewModel
contains a collection of SingleBrandViewModel
which has a collection of ProductViewModel
. So, we have three Views
corresponding to these three ViewModels
. Let's code the 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>
So, binding of AddBrand
Command is trivial, but what about DeleteBrand
? If we see the picture where we bordered the Viewmodels
, the close button is actually inside the FormattedTabControl
(FormattedTab
is the custom TabControl
used here). So, it seems that we should put DeleteBrand
inside SingleBrandViewModel
. Ok, I agree, but remember our SingleBrandViewModel
collection is in BrandsViewModel
. So, how do we will perform the delete operation? A simple answer is, we will put an event in SingleBrandViewModel
, fire that when the Command executes and hook the event in BrandsViewModel
when we add a Brand. This will certainly work, but a Brand should not raise an event to delete itself. This makes the ViewModel
class coupled and hard to test. We can actually bind the command with parent ViewModel
, BrandViewModel
here using FindAncestor
or ElementName
in Binding expression. Let's see how we can do that inside the ItemContainerStyle
of FormattedTabControl
.
<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>
This is the style of FormattedTab
's HeaderTemplate
. It contains two columns, one for EditableTabHeaderControl
and the other for a Close Button. Now, we have to bind the DeleteBrand
command with this button. The FormattedTab
's ItemSource
is binded with Brands
collection of BrandsViewModel
. As each TabItem
is binded with SingleBrandViewModel
, so from this button, we have to find its ancestor TabControl
and bind to the ancestor's DataContext
. Similar case arises for the Remove button, it's datacontext
is ProductViewModel
but we put and bind the DeleteProduct
in SingleBrandViewModel
. So, at Remove button of ProductView
, out target ancestor will be the ListBox
which is in SingleBrandView
.
<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>
Have a close look at CommandParameter
in both cases. The whole binded object is passed as parameter, which makes the Command's execution extremely easy. Have a look at Execute
method for DeleteProduct
Command:
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);
}
}
This is the way we can bind commands in parent-child scenario in MVVM pattern keeping the ViewModels
more testable.
Code
The solution contains two projects, one for the demo application and the other for the FormattedTabControl
.
Notes
I have used Prism's DelegateCommand
class here. Also, for the style of close button in the custom TabControl
, I looked at this four part article on WPF TabControl. Thanks to Olaf Rabbachin for writing such a wonderful article.
What Now?
I would like to hear your thoughts about this implementation. Share your ideas, just leave a comment...
History
- 21st February, 2011: Initial post