I’m currently working on a Silverlight 2 application that will go on-line in about a week. It is not a big application, at least not if you look at the functionality in it, but it still has it’s challenges. Especially graphical ones. But this was one that I didn’t actually expect would cause a problem…
Our designer has worked up some nice layout that we need to implement. One of the features in the layout is a list of items that the user can select from. Obviously a ListBox. But the thing that caused problems, was the fact that it was supposed to show a compact information layout for all items except the selected item. The selected item should have a more verbose layout and some extra functionality. Initially that seemed like a tiny problem. That would just be a ItemTemplate with a VisualStateManager. Apparently not…!
Why not? Well…the DataTemplate does not support a VisualStateManager. When you select an item in the ListBox, the VisualStateManager that is used to change the layout is in the items ControlTemplate and not in the items DataTemplate. So the Layout in the DataTemplate does not have the ability to change its layout based on the state…hmm…how do you solve this? Well…this is how I did it…
I started by setting the ListBox’s ItemContainerStyle. In the style I defined the template of the ItemContainer. The easiest way to get something useful to put in the template is either by using Expression Blend or by taking a look at the generic.xaml file using .NET Reflector. My ControlTemplate had a root element of type Grid. Inside the Grid I added a VisualStateManager to handle the different states. Right now I am only interested in the Selected state. So the base of my ControlTemplate looks like this
<ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal" />
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectionStates">
<vsm:VisualState x:Name="Selected">
<Storyboard>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
In my first try I then added my layout in this template and used the VSM to set the visibility of the things I wanted to display depending on the selected state. However, this has a major flaw. The selection of items in the ListBox is not automatically handled anymore and you have to manage that part using codebehind and the MouseLeftButtonDown event. So instead I did something completely different.
The selection functionality is normally handled by the ContentPresenter, and by not using one of those and instead placing the layout in the ControlTemplate I had screwed up the functionality I needed. So why not add a ContentPresenter…well…that takes me back to square one… Not really…
I added 2 ContentPresenters. One visible and one collapsed. Then I set their ContentTemplate to 2 different DataTemplates that I had defined.
<Grid x:Name="LayoutRoot" Background="White">
<Grid.Resources>
<DataTemplate x:Key="Template1">
<Grid>
<TextBlock Text="{Binding Name}" FontSize="10" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Template2">
<Grid>
<TextBlock Text="{Binding Name}" FontSize="20" />
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox x:Name="lb">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="{TemplateBinding Background}">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal" />
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectionStates">
<vsm:VisualState x:Name="Selected">
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<ContentPresenter
x:Name="contentPresenter1"
Content="{TemplateBinding Content}"
ContentTemplate="{StaticResource Template1}"
HorizontalAlignment="Left"
Margin="{TemplateBinding Padding}"
Visibility="Visible"/>
<ContentPresenter
x:Name="contentPresenter2"
Content="{TemplateBinding Content}"
ContentTemplate="{StaticResource Template2}"
HorizontalAlignment="Left"
Margin="{TemplateBinding Padding}"
Visibility="Collapsed"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
After that…all I needed to do is to set the visibility of the 2 ContentPresenters depending on the state. This is very easy to do using the VSM
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal" />
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectionStates">
<vsm:VisualState x:Name="Unselected">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter1" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter2" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="Selected">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter1" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter2" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
This way I have now 2 different layouts depending on the state of the item. This could be extended to all the different states in the ListBoxItem.
The only downside to this is that if there are only parts of the template that is shown or hidden depending on the state, you might end up with a lot of duplicate Xaml. So if you are only showing or hiding parts of it, I would recommend putting only the things that should be changed in the “selected template” and then not hide the “unselected template”. But it depends on the layout. If you are modifying a lot, it might be hard to get this to work, but if you are for example just showing and hiding some extra information or a button or something it would work and make the Xaml simpler…
<Grid x:Name="LayoutRoot" Background="White">
<Grid.Resources>
<DataTemplate x:Key="Template1">
<Grid>
<TextBlock Text="{Binding Name}" FontSize="10" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Template2">
<Grid>
<Button Content="Click me!" />
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox x:Name="lb">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="{TemplateBinding Background}">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal" />
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectionStates">
<vsm:VisualState x:Name="Unselected">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter2" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="Selected">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter2" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentPresenter Grid.Row="0"
x:Name="contentPresenter1"
Content="{TemplateBinding Content}"
ContentTemplate="{StaticResource Template1}"
HorizontalAlignment="Left"
Margin="{TemplateBinding Padding}"
Visibility="Visible"/>
<ContentPresenter Grid.Row="1"
x:Name="contentPresenter2"
Content="{TemplateBinding Content}"
ContentTemplate="{StaticResource Template2}"
HorizontalAlignment="Left"
Margin="{TemplateBinding Padding}"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
Well…that’s all for now… Have to run. I hope it has helped you a little. It seems like a pretty common feature. I don’t know what this is not supported by default by either having VSM support in the DataTemplate or by having support for multiple DataTemplates…