This technique requires the Blend 4.0 SDK (which is included in Blend 4.0 and is
also available as a
free download.
Someone on StackOverflow asked how to bind a collection of items to a ListBox in
Silverlight where the IsSelected property of the ListBoxItem is bound to an
IsSelected property of the data item.
WPF has the native ability within a style to set the a style’s setter property
IsSelected to a value of the two way binding to an IsSelected Property. Slick.
Silverlight, has no such thing unfortunately.
But, there’s a work around that isn’t too awful. Seriously.
What I’ve done is mapped the look and feel of the ListBoxItem selection to the
DataTemplate for the item and removed it from the standard ListBoxItemContainer
style:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestSilverlightTodoListItem" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" x:Class="TestSilverlightTodoListItem.MainPage"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<local:PeopleList x:Key="PeopleListDataSource" d:IsDataSource="True"/>
<Style x:Key="ListBoxItemStyle1" TargetType="ListBoxItem">
<Setter Property="Padding" Value="3"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="TabNavigation" Value="Local"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
</VisualState>
<VisualState x:Name="Disabled">
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected"/>
<VisualState x:Name="Selected">
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetProperty="Visibility" Storyboard.TargetName="FocusVisualElement">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"/>
<Rectangle x:Name="FocusVisualElement" RadiusY="1" RadiusX="1" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="PersonTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<i:Interaction.Behaviors>
<ei:DataStateBehavior Binding="{Binding IsSelected, Mode=TwoWay}" Value="True" TrueState="Selected" FalseState="Unselected"/>
</i:Interaction.Behaviors>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Selection">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.25"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Selected">
<Storyboard>
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="fillColor" d:IsOptimized="True"/>
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="fillColor2" d:IsOptimized="True"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unselected"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<VisualStateManager.CustomVisualStateManager>
<ei:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<Rectangle x:Name="fillColor" Fill="#FFBADDE9" IsHitTestVisible="False" Opacity="0" RadiusY="1" RadiusX="1" Grid.ColumnSpan="2"/>
<Rectangle x:Name="fillColor2" Fill="#FFBADDE9" IsHitTestVisible="False" Opacity="0" RadiusY="1" RadiusX="1" Grid.ColumnSpan="2"/>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Grid.ColumnSpan="1"/>
<TextBlock x:Name="textBlock" Text="{Binding Name}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" DataContext="{Binding Source={StaticResource PeopleListDataSource}}" >
<ListBox x:Name="myList" ItemsSource="{Binding}"
ItemContainerStyle="{StaticResource ListBoxItemStyle1}"
ItemTemplate="{StaticResource PersonTemplate}"
SelectionMode="Multiple" />
</Grid>
</UserControl>
The real magic is using the DataStateBehavior (which is included in the Blend 4.0
SDK):
<i:Interaction.Behaviors>
<ei:DataStateBehavior Binding="{Binding IsSelected, Mode=TwoWay}" Value="True" TrueState="Selected" FalseState="Unselected"/>
</i:Interaction.Behaviors>
This ties the IsSelected property of the Person class (see below) to two
VisualStates that I defined in the DataTemplate. A “Selected” and an “Unselected”
state.
I grabbed the rectangle from the standard ListBoxItem container template template.
In the code behind, I wired up the selection changed event:
public partial class MainPage : UserControl
{
private PeopleList _items = new PeopleList();
public MainPage()
{
this.DataContext = _items;
InitializeComponent();
myList.SelectionChanged += new SelectionChangedEventHandler(myList_SelectionChanged);
}
void myList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// these both just need to toggle
foreach (object o in e.AddedItems)
{
Person p = o as Person;
p.IsSelected = !p.IsSelected;
}
foreach (object o in e.RemovedItems)
{
Person p = o as Person;
p.IsSelected = !p.IsSelected;
}
}
void myList_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space )
{
if (e.OriginalSource is ListBoxItem)
{
Person p = (e.OriginalSource as ListBoxItem).DataContext as Person;
if (p != null)
{
p.IsSelected = !p.IsSelected;
}
}
}
}
}
The selection changed toggles the state of each item. Without doing that, the
selection doesn’t behave correctly. The SelectedItems list on the listbox no longer
reflects the reality of the bound data items – but that shouldn’t matter in this
case as the property of the item reflects the real state accurately.
For testing:
public class PeopleList : ObservableCollection<Person>
{
public PeopleList()
{
this.Add( new Person { Name = "Henry", IsSelected = true });
this.Add(new Person { Name = "Bonnie", IsSelected = true });
this.Add( new Person { Name = "Clyde", IsSelected = false });
this.Add( new Person { Name = "Ervin", IsSelected = false });
this.Add( new Person { Name = "Timmy", IsSelected = true });
this.Add( new Person { Name = "Jane", IsSelected = true });
}
}
And:
public class Person : INotifyPropertyChanged
{
private bool _isSelected;
private string _name;
public string Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
RaisePropertyChanged("Name");
}
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
RaisePropertyChanged("IsSelected");
}
}
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}