How to bind ListBoxItem.IsSelected to a bound item in Silverlight 4.0

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.

image

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