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

Using Visual State Manager from a UserControl

To aid in an answer on StackOverflow that I had recently answered, I’m providing part of the response here.

The question was essentially, “what’s a way to use DataTriggers in Silverlight, without DataTriggers?”

I had suggested one idea would just to use VisualStates and a code behind file.

That’s what I’ve done here. I created an enum of type AnimateState, which has three possible values, Top, Left, and Right. By clicking on one of three buttons on the simple UI, it changes the value of the property, which in turn calls one of the VisualStates defined in the XAML.

In the example, it animates the position of the orange ellipse to various positions on the canvas.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace DemoShowVisualStateManager
{    
    public partial class MainPage : UserControl
    {
        private AnimateState _animateState;

        public MainPage()
        {
            InitializeComponent();
        }

        public AnimateState State
        {
            get { return _animateState; }

            set
            {
                if (_animateState != value && Enum.IsDefined(typeof(AnimateState), value))
                {
                    _animateState = value;

                    VisualStateManager.GoToState(this, value.ToString(), true);
                }
            }

        }

        private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            AnimateState state;
            if (AnimateState.TryParse((sender as Button).Content.ToString(), out state)) 
            {
                State = state;
            }
            
        }
    }

    public enum AnimateState
    {
        Top,
        Left,
        Right
    }
}

 

<UserControl x:Class="DemoShowVisualStateManager.MainPage"
    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"
    mc:Ignorable="d"
    Width="500" Height="500">

    <UserControl.Resources>
    </UserControl.Resources>
    <Canvas x:Name="LayoutRoot" Background="White">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="Positions">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="0:0:3"/>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="Right">
                    <Storyboard>
                        <DoubleAnimation Duration="0" To="389" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="ball" d:IsOptimized="True"/>
                        <DoubleAnimation Duration="0" To="198" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="ball" d:IsOptimized="True"/>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="Left">
                    <Storyboard>
                        <DoubleAnimation Duration="0" To="-199" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="ball" d:IsOptimized="True"/>
                        <DoubleAnimation Duration="0" To="390" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="ball" d:IsOptimized="True"/>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="Top"/>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Ellipse x:Name="ball" Stroke="#FFFFD9B8" RenderTransformOrigin="0.5,0.5" Height="100" VerticalAlignment="Top" Canvas.Top="10" Width="100" Canvas.Left="200">
            <Ellipse.RenderTransform>
                <CompositeTransform/>
            </Ellipse.RenderTransform>
            <Ellipse.Fill>
                <RadialGradientBrush Center="0.388,0.328" GradientOrigin="0.388,0.328" RadiusX="0.63">
                    <GradientStop Color="#FFF97200" Offset="1"/>
                    <GradientStop Color="#FFFFD9B8"/>
                </RadialGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <Button Content="Left" Height="33" Canvas.Left="200" Canvas.Top="229" Width="100" Click="Button_Click"/>
        <Button Content="Top" Height="33" Canvas.Left="200" Canvas.Top="266" Width="100" Click="Button_Click"/>
        <Button Content="Right" Height="33" Canvas.Left="200" Canvas.Top="303" Width="100" Click="Button_Click"/>

    </Canvas>
</UserControl>

image

Visual Studio 2010 Add-in Settings Swapper

Challenged by a coworker recently, I created a simple add-in for Visual Studio 2010 to modify the settings for code formatting based on the file extension. The usage scenario is simple: allow for different language formatting rules based on the file file type (or extension). In this case, he wanted to have a different standard for curly brace usage in MVC ASPX pages overriding the default.

The addin, called Settings Swapper may be downloaded from CodePlex and is licensed using the new BSD license.

The documentation is here.

The settings he used for MVC pages are included in the documentation. If you have any questions or issues, either leave a comment here or add something to codeplex.

Enjoy.