Selected Item States For LongListSelector In Windows Phone 8

31 December 2012

Adding to the pile of "something that should be really easy but is not", it seems like there isn't a simple way of adding selected item states (ie, highlight an item with a border when it's selected) when you're using the native LongListSelector in Windows Phone 8. Unlike the ListBox control, there aren't any default "selected item" Visual States in the DataTemplate for you to override, and looking at the LongListSelector example code from MSDN doesn't give any indicator as to how to accomplish either.

A trawl of the internet revealed a few "solutions" (that feel more like hacks), but I eventually settled on using the SelectionChanged event to crawl the Visual Tree to call the VisualStateManager on the appropriate UserControl.

Sample code time!

My LongListSelector, with the SelectionChanged event and user control within the DataTemplate:

        <phone:LongListSelector x:Name="MyLongListSelector" Margin="0,0,-12,0" ItemsSource="{Binding MyItems}" SelectionChanged="MyLongListSelector_SelectionChanged" >
            <phone:LongListSelector.ItemTemplate>
                <DataTemplate>    
                    <mycode:CustomUserControl />
                </DataTemplate>
            </phone:LongListSelector.ItemTemplate>
        </phone:LongListSelector>

And then a few visual states configured for my user control (configuring them with Blend is much easier and highly recommended):

<UserControl x:Class="My.UserControls.CustomUserControl"
    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"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    d:DesignHeight="480" d:DesignWidth="480">

    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="Selected">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background)" Storyboard.TargetName="LayoutRoot">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>         
        <StackPanel Margin="0,0,0,17">
            <TextBlock Text="{Binding Title}" TextWrapping="Wrap" Style="{StaticResource PhoneTextSubtleStyle}"/>            
        </StackPanel>
    </Grid>
</UserControl>

And then, the bits to tie it together - the Visual Tree walker, and the selection changed event:

   public static void GetItemsRecursive<T>(DependencyObject lb, ref List<T> objectList) where T : DependencyObject   
    {
        List<DependencyObject> listDepOb = new List<DependencyObject>();
        var childrenCount = VisualTreeHelper.GetChildrenCount(lb);

        for (int i = 0; i < childrenCount; i++)
        {
            var child = VisualTreeHelper.GetChild(lb, i);

            if (child is T)
            {
                objectList.Add(child as T);
            }

            GetItemsRecursive<T>(child, ref objectList);
        }

        return;
    }

    private void MyLongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        List<CustomUserControl> userControlList = new List<CustomUserControl>();
        GetItemsRecursive<CustomUserControl>(MyLongListSelector, ref userControlList);


        if (e.AddedItems.Count > 0 && e.AddedItems[0] != null)
        {
            foreach (CustomUserControl userControl in userControlList)
            {
                if (e.AddedItems[0].Equals(userControl.DataContext))     
                {
                    VisualStateManager.GoToState(userControl, "Selected", true);
                }
            }
        }

        if (e.RemovedItems.Count > 0 && e.RemovedItems[0] != null)
        {
            foreach (CustomUserControl userControl in userControlList)
            {
                if (e.RemovedItems[0].Equals(userControl.DataContext))     
                {
                    VisualStateManager.GoToState(userControl, "Normal", true);
                }
            }

        }

    }

The code above doesn't handle multiple selections (does the LongListSelector itself do so? I'm not sure) and I feel like this will perform terribly with a large number of items in the list, so I'm open to suggestions as to how to improve it. I briefly looked into trying to bind the Name property to the UserControl so I could use .FindName() but I didn't manage to get that going and I'm not 100% that approach would be faster without knowing the internals of how it works.

Or if there's an easier way of getting the associated DataTemplate instance from a DataBound item (since that's all you get from LongListSelector.SelectedItem), I would love to know!

Oh yeah, I almost forgot - happy new year, and best wishes for the coming days of 2013 :)

Tags: LongListSelector, VisualStateManager, VisualTreeHelper, Windows Phone 8

Add a Comment

2 Comments

  • Henry said Reply

    Hey Hayden,

    My machine doesn't have the Windows Phone SDK and bits installed at the moment, so I can't really verify this for you, but it appears that Microsoft *may* have added the SelectionChanged event to the LongListSelector itself, so you should be able to use that directly, as described here: http://code.msdn.microsoft.com/windowsapps/Highlight-a-selected-item-30ced444#content

    That's about as far as I can get without having the bits installed and playing around with it again. Best of luck to you!