metadevblog

Windows Phone 7 App Development

Long List Selector (Jump List) Example

with 8 comments

I am developing an App that will require the Long List Selector (LLS) functionality that is demonstrated in the  WP7 People selector.  The LLS also provides a good UI for breaking up even modest sized lists in to easy on the eye groups.

The Windows Phone Toolkit on Codeplex (http://silverlight.codeplex.com/releases/view/75888) provides a LLS but there is no documentation to explain how to use it.

The LLS is not a simple control – there is a fair amount of setup and code required before it will work, however I’ve learnt a lot and am quite pleased with the result.

I have replaced the standard List control in my Master Key application with a LLS but have created a cut down version of the App just to focus on using LLS.  I’ve kept the Transitions code in place as well from the previous post.

Before going into the details of LLS I should explain some of the other code that is in the example I’m going to use.

I make extensive use of XML in my applications when working with data, I find the ease with which I can create a robust and arbitrarily complex data model using XML documents far outweighs any disadvantages. Linq for XML is brilliant to work with and over the years I’ve created a set of simple helper extensions which simplify the extraction of values from XML Elements and Attributes.

I always add a copy of XElementHelpers.cs to my projects when I’m using Linq as it removes some of the pain involved when the XML is missing elements. It is a helper class which adds some useful methods for retrieving the values of XML Elements and Attributes. The most commonly used being .ToStringNull() which will return either the Element value or an empty string, however it also returns an empty string even if the Element does not exist (is null).

I’ve also learnt that using a Data Access Layer (DAL) even for small projects brings long term benefits in terms of maintainability and extensibility so my code examples will be slightly more complex as a result.

I’ve put the source for the project here: www.metadev.co.uk/Downloads/PhoneAppLLS.zip please feel free to download and use it.

I have included enough code in the example, which is a cut down version of my Master Key App, to demonstrate the full lifecycle for list management allowing entries to be displayed, selected and edited as well as being able to add and delete entries.

The idea behind the Master Key App is pretty simple – it allows the user to keep a list of keys, passwords and other info in an encrypted form on the phone.  A single ‘master key’ decrypts the information giving access to the entries.  I’ve called the list a Vault which is made up of individual Vault Entries so you will see plenty of references to Vaults in the code!

For this sample I have removed the encryption and much of the other persistence code as it distracts from the LLS.

LLSScreenShot

The LLS groups items under a title.  The title can be arbitrary but for this example I have stuck to grouping the entries based on the first character of the caption.  The classification is however completely open and any parent can be defined.

The Vault is maintained in the DAL as a sorted Dictionary of <Caption, VaultEntry> pairs.  In the previous version these entries where then simply dropped into a standard ListBox.  Sorting is not something that comes for free with either the ListBox or the LLS (or even the Dictionary object for that matter) so maintaining the sorted list is part of the DAL.

        public void SortEntries()
        {
            try
            {
                // sort the keys - Silverlight Dictionary does not have a sort option
                string[] array = new string[VaultEntries.Keys.Count];
                VaultEntries.Keys.CopyTo(array, 0);
                List<string> list = new List<string>(array);
                list.Sort();

                // now recreate the dictioanry                
                Dictionary<string, VaultEntry> sortedEntries = new Dictionary<string, VaultEntry>();
                VaultEntry entry = new VaultEntry();

                foreach (string key in list)
                {
                    VaultEntries.TryGetValue(key, out entry);
                    sortedEntries.Add(key, entry);
                }

                VaultEntries = sortedEntries;
            }
            catch
            {
                throw;
            }
        }
 
A generic Group class is used to hold the entries for the LLS.  The Group class looks like this:

The Group class has two principle properties, a Title which provides the group heading and a List of entries which provide the rest of the group content.

    /// <summary>
    /// Generic Group class which has a string Title and a generic list of items
    /// that are related to the title - for example the Title could be 'k' and then
    /// all the items could have entry such as a persons surname that being with 'k'
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class Group<T> : IEnumerable<T>
    {
        /// <summary>
        /// Group title - this provides the jump list titles
        /// </summary>
        public string Title
        {
            get;
            set;
        }

        /// <summary>
        /// List of items in the group
        /// </summary>
        public IList<T> Items
        {
            get;
            set;
        }

        /// <summary>
        /// True when there are items - it's used primarily to create the tiles in the jump list
        /// to indicate visibly where items exist
        /// </summary>
        public bool HasItems
        {
            get
            {
                return Items.Count > 0;
            }
        }

        /// <summary>
        /// This is used to colour the tiles - greying out those that have no entries
        /// </summary>
        public Brush GroupBackgroundBrush
        {
            get
            {
                return (SolidColorBrush)Application.Current.Resources[(HasItems) ? 
                            "PhoneAccentBrush" : "PhoneChromeBrush"];
            }
        }

        /// <summary>
        /// Group constructor
        /// </summary>
        /// <param name="name"></param>
        /// <param name="items"></param>
        public Group(string name, IEnumerable<T> items)
        {
            this.Title = name;
            this.Items = new List<T>(items);
        }

        /// <summary>
        /// Test for Title equality (and that object exists)
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            Group<T> that = obj as Group<T>;
            return (that != null) && (this.Title.Equals(that.Title));
        }

        /// <summary>
        /// When overriding Equals the GetHashCode has to also be overridden
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return this.Title.GetHashCode();
        }

        /// <summary>
        /// Item collection enumerator
        /// </summary>
        /// <returns></returns>
        public IEnumerator<T> GetEnumerator()
        {
            return this.Items.GetEnumerator();
        }

        /// <summary>
        /// Item collection enumerator
        /// </summary>
        /// <returns></returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.Items.GetEnumerator();
        }
    }

 

The properties HasItems and GroupBackgroundBrush are primarily used when displaying the jump list:

LLSJumpList

A requirement for the LLS list is for the list elements to be IEnumerable and this is provided with the methods at the bottom of the class.

As the Group class is generic it needs to be solidified to provide the specific functionality needed for this application and this is achieved with this class:

    /// <summary>
    /// This class is responsible for building the specific a-z jump list. If grouping is required around
    /// a different classification then another implementation has to be written.
    /// This implementation is efficient in that there are only two loops through the data, one to create the
    /// Group list with the specific titles and then a second pass through the data itself.
    /// </summary>
    public class AGroupedVaultEntry : List<Group<VaultEntry>>
    {
        private static readonly string Groups = "#abcdefghijklmnopqrstuvwxyz";

        /// <summary>
        /// Build and return a Grouped list based on the current VaultEntry Dictionary
        /// </summary>
        public AGroupedVaultEntry()
        {
            DataAccessLayer DAL = (Application.Current as App).DAL;

            // create the groups by simply iterating over the group string
            foreach (char c in Groups)
            {
                Group<VaultEntry> g = new Group<VaultEntry>(c.ToString(), new List<VaultEntry>());
                this.Add(g);
            }

            List<Group<VaultEntry>> list = this;

            // now iterate over the VaultEntries and add them to the appropriate group
            foreach (VaultEntry vaultEntry in DAL.VaultEntries.Values)
            {
                // get the first char (lower case) of the entry
                string c = VaultEntry.GetCaptionGroup(vaultEntry);
                
                // calculate the index into the list
                int index = (c == "#") ? 0 : c[0] - 'a' + 1;

                // and add the entry
                list[index].Items.Add(vaultEntry);
            }
        }
    }

 

This class is designed to create and return a List of Group VaultEntries with each Group header being a letter of the alphabet (plus # for all others). It will then populate the Group Items with the actual VaultEntries based on their group Title (a-z) – this association is made using the GetCaptionGroup which has been added specifically to the VaultEntry class for this purpose:

/// <summary>
/// Get the first letter of the caption or #
/// </summary>
public static string GetCaptionGroup(VaultEntry Current)
{
    char key = char.ToLower(Current.Caption[0]);
    if (key < 'a' || key > 'z')
    {
        key = '#';
    }
    return key.ToString();
}
 
Because the Vault Dictionary is already sorted by the Caption then the code for adding the appropriate 
entries is simplified and each VaultEntry can be added to the list in a single pass.
 
So far all the code discussed has been written to provide suitable data for the LLS, the final part is to set up the control itself so that it can make use of it.
 

The LLS can be used as a standard list quite simply, this XAML will display the Vault Entries without any header decoration:

<toolkit:LongListSelector VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
                          Margin="6,6" 
                          Name="longListSelectorVaultEntries" 
                          IsFlatList="False"  IsBouncy="True"
                          Loaded="OnLoaded" SelectionChanged="OnSelectionChanged">

    <toolkit:LongListSelector.ItemTemplate>
        <DataTemplate>
            <Grid Background="Transparent">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Image Width="55" Source="/Toolkit.Content/LOCKED.png" 
                       VerticalAlignment="Top"/>
                <StackPanel Grid.Column="1"  VerticalAlignment="Top">
                    <TextBlock Text="{Binding Caption}" 
                               Style="{StaticResource PhoneTextLargeStyle}" 
                               Margin="0,8,0,0"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </toolkit:LongListSelector.ItemTemplate>

</toolkit:LongListSelector>

 

The TextBlock.Text is bound to the Caption in the Vault Entry it is currently iterating over.  I’ll show later that multiple items can be displayed here.

There are two events defined – OnLoaded which is used to fetch and display the current Vault Entry list and OnSelectionChanged which is used to capture the user selecting an entry for display/editing.

The OnLoaded event looks like this:

private void OnLoaded(object sender, RoutedEventArgs e)
{
    DataAccessLayer DAL = (Application.Current as App).DAL;

    // when the page is being resumed this event will fire but it's not
    // appropriate to display the info as we actually want to hide it.
    if (DAL.ResumeFromDeactivation)
    {
        return;
    }

    // if the key has changed then reload the value entries
    if (DAL.ListReloadRequired || longListSelectorVaultEntries.ItemsSource == null)
    {
        ShowVault();

        DAL.ListReloadRequired = false;
    }
}
 
The AGroupedVaultEntry method is called and the IEnumerable Group<> is applied to the LLS ItemsSource.
 
The SelectionChanged event simply returns the selected item as the VaultEntry object from the Group List
 
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // edit the selected item
    VaultEntry entry = (VaultEntry)longListSelectorVaultEntries.SelectedItem;
    if (entry == null) return;

    longListSelectorVaultEntries.SelectedItem = null;

    // pass the get to NewItem so that it can be looked up
    this.NavigationService.Navigate(new Uri(string.Format("/EditEntry.xaml?type=edit&key={0}", entry.Caption), 
              UriKind.Relative));
}
 

The SelectedItem is set to null again so that it can be reselected.
 
LLSStandardList
 
In order to provide the grouping and the jump list additional DataTemplates have to be added to the XAML none of which are particularly complex:
 
<toolkit:LongListSelector VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="6,6" 
                            Name="longListSelectorVaultEntries" 
                            IsFlatList="False"  IsBouncy="True"
                            Loaded="OnLoaded" SelectionChanged="OnSelectionChanged">

    <toolkit:LongListSelector.GroupItemsPanel>
        <ItemsPanelTemplate>
            <toolkit:WrapPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </toolkit:LongListSelector.GroupItemsPanel>

    <toolkit:LongListSelector.GroupItemTemplate>
        <DataTemplate>
            <Border Background="{Binding GroupBackgroundBrush}" Width="99" Height="99" Margin="6" 
                                IsHitTestVisible="{Binding HasItems}">
                <TextBlock Text="{Binding Title}" 
                            Style="{StaticResource PhoneTextExtraLargeStyle}"
                            Margin="{StaticResource PhoneTouchTargetOverhang}"
                            Foreground="{StaticResource PhoneForegroundBrush}"                                        
                            VerticalAlignment="Bottom"/>
            </Border>
        </DataTemplate>
    </toolkit:LongListSelector.GroupItemTemplate>

    <toolkit:LongListSelector.GroupHeaderTemplate>
        <DataTemplate>
            <Border Background="Transparent">
                <Border Background="{StaticResource PhoneAccentBrush}" 
                        Width="55" Height="55" HorizontalAlignment="Left">
                    <TextBlock Text="{Binding Title}" 
                                Foreground="{StaticResource PhoneForegroundBrush}" 
                                Style="{StaticResource PhoneTextLargeStyle}"
                                VerticalAlignment="Bottom"/>
                </Border>
            </Border>
        </DataTemplate>
    </toolkit:LongListSelector.GroupHeaderTemplate>

    <toolkit:LongListSelector.ItemTemplate>
        <DataTemplate>
            <Grid Background="Transparent">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Image Width="55" Source="/Toolkit.Content/LOCKED.png" VerticalAlignment="Top"/>
                <StackPanel Grid.Column="1"  VerticalAlignment="Top">
                    <TextBlock Text="{Binding Caption}" 
                               Style="{StaticResource PhoneTextLargeStyle}" Margin="0,8,0,0"/>
                    <TextBlock Text="{Binding Identity}" 
                               Style="{StaticResource PhoneTextNormalStyle}" Foreground="DarkGray"/>
                    <TextBlock Text="{Binding OtherInfo}" 
                               Style="{StaticResource PhoneTextNormalStyle}" Foreground="DarkGray"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </toolkit:LongListSelector.ItemTemplate>

    <!--<toolkit:LongListSelector.ListFooterTemplate>
                    <DataTemplate>
                        <Border Background="Transparent" Height="159">
                            <TextBlock Text="End..." />
                        </Border>
                    </DataTemplate>
                </toolkit:LongListSelector.ListFooterTemplate>-->

</toolkit:LongListSelector>
 
There is a lot of it but it’s pretty straight forward – I copied it pretty much verbatim from the Toolkit example code.
 
The GroupItemsPanel is used to display the Group headers as shown above.  The Orientation=”Horizontal” forces the panel items to be displayed as a horizontal list, when Vertical is used then the items are centred as a long scrolling list – useful for when there are wide group headings.  Horizontal is perfect for the a-z listing.
 
The GroupItemTemplate is used to create the individual items that will be shown in the Panel and is bound to the HasItems method in the Group class.  The Background is bound  GroupBackgroundBrush in the same class (it makes use of HasItems internally to choose the appropriate colour) as is the Title (Title in turn has been set previously from the Vault Entry Caption).
 
The GroupHeaderTemplate is used to create the heading for each Group that has items associated with it, empty groups are omitted.
 
That’s it! I hope this explanation has been useful and that it will help you get up to speed more quickly than I was able to with LLS.
 
One final thing though – I believe there is a bug in the LLS that prevents it from showing the last group in the list correctly.  If I click on a group header to display the jump list and then click ‘w’ the display looks like this:
 
LLSLastW
 
There are another two entries below this that are not displayed – i.e when the LLS displays this last list entry it should start a couple of rows higher.  I got round this by adding a footer to the LLS:
 
<toolkit:LongListSelector.ListFooterTemplate>
    <DataTemplate>
        <Border Background="Transparent" Height="159">
            <TextBlock Text="End..." />
        </Border>
    </DataTemplate>
</toolkit:LongListSelector.ListFooterTemplate>
 

This still isn’t perfect but at least it does seem to hide the problem somewhat.

All in all the Long List Selector is a great control that adds a lot of functionality to the mundane list control.

Oh – I forgot to show that the LLS can show more than lists – you will have seen this already in the LLS in the Toolkit example.  All you need to do is add the extra info to the StackPanel:

 
<StackPanel Grid.Column="1"  VerticalAlignment="Top">
    <TextBlock Text="{Binding Caption}" 
                Style="{StaticResource PhoneTextLargeStyle}" Margin="0,8,0,0"/>
    <TextBlock Text="{Binding Identity}" 
                Style="{StaticResource PhoneTextNormalStyle}" Foreground="DarkGray"/>
    <TextBlock Text="{Binding OtherInfo}" 
                Style="{StaticResource PhoneTextNormalStyle}" Foreground="DarkGray"/>
</StackPanel>
Advertisements

Written by metadevblog

December 27, 2011 at 11:58 am

Posted in c#, WP7

8 Responses

Subscribe to comments with RSS.

  1. Great thank you! This makes LLS a lot easier to understand. I plan on using this for a dictionary type application. Thanks again for sharing.

    Dante

    March 5, 2012 at 8:34 pm

  2. Hello, I absolutely love you example of your WP7 LLS application. I’ve been pulling my hair out the last 5 hours trying to full understand it and I have been able to replicate what you’ve done.

    Is it not possible to have the LLS control in a Pivot or Panorama app? I can’t seem to get it to work and I’m wondering if its not an option. Any help would be really appreciated. Thank you. Dante

    Dante

    March 6, 2012 at 12:22 pm

  3. great post!!!

    i have one question. how did you do your selected item for the selection changed event?

    my list box alone works fine as i a have it stored as a model it my viewmodel ,

    but once LLS comes in play,it totally screws everything up. Whenever i click an alphabet which is supposedly supposed to bring up the stack panel,it gets a null expression error

    Dhina

    March 29, 2012 at 12:03 pm

    • A big thank you to the project

      miroslav

      February 19, 2013 at 12:48 pm

  4. Hi,

    Very nice article. I have a lot of items to display in a LongListSelector (around 8000). I want to partially load items (and eventually also dispose items when they are out of the visible view). I know also the content of the jump list. At initialisation I load 20 items. While the user scrolls down, I load more items (implemented the ItemsRealized event). Now when the JumpList is closed, I want to know which item of the jump list is selected in order to build the list before and after the jump position.
    How do I know the selected jump item after the jump list is closed?

    Luc Verstreken

    December 4, 2013 at 10:15 am

  5. Phuong Duy

    May 22, 2014 at 12:58 am


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: