×
Namespaces

Variants
Actions

LongListSelector with bindable SelectedItem and better scrolling

From Nokia Developer Wiki
Jump to: navigation, search

This article explains an approach to fix the most common problems found in LongListSelector.

WP Metro Icon UI.png
SignpostIcon XAML 40.png
WP Metro Icon WP8.png
Article Metadata
Code Example
Source file: LongListSelectorAdd (GitHub)
Tested with
SDK: Windows Phone 8.0 SDK
Devices(s): Emulator, Lumia 1020
Compatibility
Platform(s):
Windows Phone 8
Article
Created: NicoVermeir (29 Nov 2013)
Last edited: joaocardoso (23 Jul 2014)

Contents

Introduction

The LongListSelector displays a sorted list of items with a mechanism for users to jump to a specific section of the list. For performance reasons, in Windows Phone 8 Microsoft recommends the use of this control instead of the ListBox.

Unfortunately LongListSelector has some known issues, particularly for use in MVVM Light apps. Firstly, its SelectedItem property isn't bindable. Secondly it scrolls all the way to the bottom after items are added to the ItemsSource, instead of displaying the top item as would be more natural for most use-cases (for more information on this problem see this discussion boards post).

There are multiple workarounds for these problems, including using behaviours to trigger a command in XAML or setting the viewmodel property from the SelectionChanged event (this breaks your entire MVVM setup since the code behind of the view is now directly setting properties on the viewmodel).

This article describes a custom LongListSelecter called ExtendedSelector which fixes these problems using an elegant and MVVM-compatible approach.

ExtendedSelector

The ExtendedSelector class derives from LongListSelector. That means it will have all the normal properties, events, methods and so on:

public class ExtendedSelector : LongListSelector

First thing we find in the ExtendedSelector are dependency properties. These define the SelectedItem, SelectionMode (used for Multi or Single selection), and lastly the List's behavior when an item is added (RepositionOnAddStyle).

public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ExtendedSelector), new PropertyMetadata(default(object)));
 
public static readonly DependencyProperty SelectionModeProperty =
DependencyProperty.Register("SelectionMode", typeof(SelectionMode), typeof(ExtendedSelector), new PropertyMetadata(SelectionMode.Single));
 
public static readonly DependencyProperty RepositionOnAddStyleProperty =
DependencyProperty.Register("RepositionOnAddStyle", typeof(PositionOnAdd), typeof(ExtendedSelector), new PropertyMetadata(PositionOnAdd.Default));
 
public PositionOnAdd RepositionOnAddStyle
{
get { return (PositionOnAdd)GetValue(RepositionOnAddStyleProperty); }
set { SetValue(RepositionOnAddStyleProperty, value); }
}
 
public SelectionMode SelectionMode
{
get { return (SelectionMode)GetValue(SelectionModeProperty); }
set { SetValue(SelectionModeProperty, value); }
}
 
public new object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}

RepositionOnAddStyle uses an Enum as type

public enum PositionOnAdd
{
Top,
Default,
NewItem
}

Since LongListSelector already has a property for SelectedItem (just not a dependency property) we need to use the "new" keyword in order to ensure that the SelectedItem in the base LongListSelector is ignored and the new one we just defined is used instead.

In the constructor of ExtendedSelector an event handler is attached to both the Loaded and the SelectionChanged events (both events come from the LongListSelector base class).

public ExtendedSelector()
{
SelectionChanged += (sender, args) =>
{
if (SelectionMode == SelectionMode.Single)
SelectedItem = args.AddedItems[0];
else if (SelectionMode == SelectionMode.Multiple)
{
if (SelectedItem == null)
{
SelectedItem = new List<object>();
}
 
foreach (var item in args.AddedItems)
{
((List<object>)SelectedItem).Add(item);
}
 
foreach (var removedItem in args.RemovedItems)
{
if (((List<object>)SelectedItem).Contains(removedItem))
{
((List<object>)SelectedItem).Remove(removedItem);
}
}
}
};
 
Loaded += (sender, args) =>
{
((INotifyCollectionChanged)ItemsSource).CollectionChanged += (sender2, args2) =>
{
if (ItemsSource.Count > 0 && args2.NewItems != null)
{
switch (RepositionOnAddStyle)
{
case PositionOnAdd.NewItem:
int index = ItemsSource.IndexOf(args2.NewItems[0]);
 
if (index >= 0)
ScrollTo(ItemsSource[index]);
break;
case PositionOnAdd.Top:
ScrollTo(ItemsSource[0]);
break;
}
}
};
};
}

Let's have a closer look at these.

Binding to SelectedItem

First, the SelectionChanged event handler

SelectionChanged += (sender, args) =>
{
if (SelectionMode == SelectionMode.Single)
SelectedItem = args.AddedItems[0];
else if (SelectionMode == SelectionMode.Multiple)
{
if (SelectedItem == null)
{
SelectedItem = new List<object>();
}
 
foreach (var item in args.AddedItems)
{
((List<object>)SelectedItem).Add(item);
}
 
foreach (var removedItem in args.RemovedItems)
{
if (((List<object>)SelectedItem).Contains(removedItem))
{
((List<object>)SelectedItem).Remove(removedItem);
}
}
}
};

If the SelectionMode is set to Single we just set the SelectedItem property to the first item in the list of AddedItems. That list gets passed in through the event arguments, since SelectionMode is set to single there should always be only one item in that list.

If SelectionMode is set to Multiple we check if SelectedItem is already instantiated, if not we create it as a new List of type object. After the list is created we add all items in the AddedItems list and remove all items from the RemovedItems list.

And with that small piece of code we've added binding to SelectedItem for both single and multi selection modes.

Reposition on add

The Loaded event handler defined in the ExtendedSelector's constructor takes care of the repositioning problem when adding new items.

Loaded += (sender, args) =>
{
((INotifyCollectionChanged)ItemsSource).CollectionChanged += (sender2, args2) =>
{
if (ItemsSource.Count > 0 && args2.NewItems != null)
{
switch (RepositionOnAddStyle)
{
case PositionOnAdd.NewItem:
int index = ItemsSource.IndexOf(args2.NewItems[0]);
 
if (index >= 0)
ScrollTo(ItemsSource[index]);
break;
case PositionOnAdd.Top:
ScrollTo(ItemsSource[0]);
break;
}
}
};
};

First, we take the ItemsSource property from LongListSelector and cast it as a INotifyCollectionChanged. By casting it to INotifyCollectionChanged we get access to the CollectionChanged event. When that event fires we need to reposition the ExtendedSelector according to the RepositionOnAddStyle property.

When RepositionOnAddStyle property is set to NewItem we look for the index of the newly added item in the ItemsSource, if we get a valid index (meaning 0 or higher) we use the ScrollTo method from LongListSelector to jump to the correct item in the list.

When the RepositionOnAddStyle property is set to Top we scroll to the item with index 0 - i.e. the very top of the list. If it's set to default, we do nothing with it, so it will get the default behavior.

Using the class

The original problem with LongListSelector is clearly demonstrated by Depechie's sample project. This is an MVVM Light application that has an ObservableCollection on the MainViewModel that is bound to the original LongListSelector. Once the app is loaded it fills up the data and the LongListSelector jumps right to the bottom.

To demonstrate ExtendedSelector I modified this original code - you can find the updated test code example here.

The only work here was to replace the original LongListSelector with my ExtendedSelector and add a property. I've added the RepositionOnAddStyle property and the SelectedItem property. Don't forget to set the SelectedItem to two-way binding since we're updating the property from the view.

  1. <controls:ExtendedSelector Grid.Row="2"
  2.                            Margin="12,0,12,24"
  3.                            SelectedItem="{Binding SelectedPerson, Mode=TwoWay}"
  4.                            GroupHeaderTemplate="{StaticResource GroupHeaderTemplate}"
  5.                            HideEmptyGroups="True"
  6.                            IsGroupingEnabled="True"
  7.                            ItemTemplate="{StaticResource LLSDataTemplate}"
  8.                            ItemsSource="{Binding Persons}"
  9.                            JumpListStyle="{StaticResource JumpListStyle}"
  10.                            RepositionOnAddStyle="NewItem" />

I've also added a TextBlock that is bound to the same property as SelectedItem on the ExtendedSelector, just to prove that it works, and a button that can add a new person to the list.

<TextBlock Grid.Row="0" Text="{Binding SelectedPerson.Name, StringFormat='Selected person: {0}'}" />
<Button Grid.Row="1" Command="{Binding AddCommand}">Add new person</Button>

The AddCommand is a RelayCommand on the MainViewModel that generates a new object of type Person and adds it to the list. Depending on the option you've selected for RepositionOnAddStyle it will jump to the top or to the newly added item.

When you select an item from the list, the TextBlock should show the name of the selected person thanks to our bindable SelectedItem.

Summary

In this article I've discussed an extension of the LongListSelector I wrote to tackle some of the issues with this control. Most important fixes here are the bindability of SelectedItem and the ability to determine the behavior of the control when a new item is added.

If you want the complete ExtendedSelector class, you can copy it from this here

This page was last modified on 23 July 2014, at 19:41.
499 page views in the last 30 days.
×