Making the ExpanderView more responsive with lots of items

If you haven’t already checked it out, the Silverlight for Windows Phone Toolkit contains a rich set of additional controls to make your life easier when programming the Windows Phone. Not to be missed is the sample application you can download separately that allows you to play around with the controls in the toolkit. The sample I’m going to dive a little deeper into today is the ExpanderView. As the sample declares, the ExpanderView shows sub-items similar to the way the Outlook application does; allowing you to drill down into nested items.

The items in the view are created from a binding to the Data\InboxObject. The sample only shows three conversations with a couple of messages in each conversation. I’m going to shorten the text of the conversations here so that they can be displayed on single lines in this BLOG.

// (c) Copyright Microsoft Corporation.

// This source is subject to the Microsoft Public License (Ms-PL).

// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details.

// All other rights reserved.

using System.Collections.ObjectModel;

namespace PhoneToolkitSample.Data

{

    public class InboxObject : ObservableCollection<ConversationObject>

    {

        public InboxObject() : base()

        {

            ConversationObject one = new ConversationObject("3 messages, 0 unread");

            one.Add(new EmailObject("Anne Wallace", "Lunch Today?", "Sure!"));

            one.Add(new EmailObject("Me", "Lunch Today?", "OK, ask Dave?"));

            one.Add(new EmailObject("Bruno Denuit", "Lunch Today?", "I vote for Thai."));

            ConversationObject two = new ConversationObject("1 messages, 0 unread");

            two.Add(new EmailObject("Adriana Giorgi", "Fun trip?", "Lucky you ..."));

            ConversationObject three = new ConversationObject("4 messages, 1 unread");

            three.Add(new EmailObject("Belinda Newman", "hawaii pics!", "Good times."));

            three.Add(new EmailObject("Richard Carey", "hawaii pics!", "Cool fish."));

            three.Add(new EmailObject("Christof Sprenger", "hawaii pics!", "Can't wait to get back."));

            three.Add(new EmailObject("Melissa Kerr", "hawaii pics!", "Check it and tag"));

            Add(one);

            Add(two);

            Add(three);

        }

    }

}

 

Now, what happens if you have 60 conversations you need to display instead of three? A slight modification to the code will get us there:

public class InboxObject : ObservableCollection<ConversationObject>

{

    public InboxObject() : base()

    {

        for (int i = 0; i < 20; i++)

        {

            ConversationObject one = new ConversationObject("3 messages, 0 unread");

            one.Add(new EmailObject("Anne Wallace", "Lunch Today?", "Sure!"));

            one.Add(new EmailObject("Me", "Lunch Today?", "OK, ask Dave?"));

            one.Add(new EmailObject("Bruno Denuit", "Lunch Today?", "I vote for Thai."));

            ConversationObject two = new ConversationObject("1 messages, 0 unread");

            two.Add(new EmailObject("Adriana Giorgi", "Fun trip?", "Lucky you ..."));

            ConversationObject three = new ConversationObject("4 messages, 1 unread");

            three.Add(new EmailObject("Belinda Newman", "hawaii pics!", "Good times."));

            three.Add(new EmailObject("Richard Carey", "hawaii pics!", "Cool fish."));

            three.Add(new EmailObject("Christof Sprenger", "hawaii pics!", "Can't wait to get back."));

            three.Add(new EmailObject("Melissa Kerr", "hawaii pics!", "Check it and tag"));

            Add(one);

            Add(two);

            Add(three);

        }

    }

}

 

 

In the emulator runs the code runs without any problem. My Windows Phone 7 device is an LG Quantum and running this code there took 11-14 seconds. That’s 11-14 seconds of the user staring at a blank screen. My guess this isn’t the smooth slick user experience you want your users to have, so how do you go about fixing that? 

Digging into the sample, the first thing I did was profile the application on the device itself. It was pretty clear that the UI thread was the culprit (MeasureOverride to be specific). The UI control calculates its layout every time you add an individual item; there currently isn’t a way around this. What I needed was an AsyncObservableCollection that could be smart enough to load the items of the collection when the UI was idle. But how do you detect that the UI is idle on the WP7? 

On the WP7 device, when the UI is inactive, there are no spinning cycles, so if there is no work to do, then nothing is done. In the above code, all of the items are being created and added all on the UI thread, so the application isn’t responsive until the code completes. Additionally, using a construct like Deployment.Current.Dispatcher.BeginInvoke doesn’t help us either since doing this will still create all the objects on the UI thread and then queue the additions to the UI thread as soon as we complete. What we need is a way to put the items onto the UI thread after we’ve created them a little at a time so as not to block the UI thread from user input. That’s where AsyncObservableCollection comes in.

 

//-----------------------------------------------------------------------------

//

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF

// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO

// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A

// PARTICULAR PURPOSE.

//

// Copyright (c) Microsoft Corporation. All rights reserved.

//

//

//-----------------------------------------------------------------------------

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.Linq;

using System.Threading;

using System.Windows;

using System.ComponentModel;

using System;

namespace PhoneToolkitSample.Data

{

    public class AsyncObservableCollection<T> : ObservableCollection<T>

    {

        // Only one of these should exist for all instances

        static AutoResetEvent myEvent = null;

        public void FillAsync(IEnumerable<T> list)

        {

            // Create event on first use

            if (null == myEvent)

            {

                myEvent = new AutoResetEvent(false);

            }

            Thread t = new Thread(new ThreadStart(() =>

            {

                // First wait is to pause the thread to allow non-bound

                // actions to transition into view on the UI thread.

                WaitForUI();

                int i = 0;

                foreach (T item in list)

                {

                    T copy = list.ElementAt(i);

                    Deployment.Current.Dispatcher.BeginInvoke(() => { this.Add(copy); });

                    WaitForUI();

                    i++;

                }

            }));

            t.Name = "FillAsync";

            t.Start();

        }

        private void WaitForUI()

        {

            // Wait for the UI thread to free up before adding another

            Deployment.Current.Dispatcher.BeginInvoke(() => { UIFree(); });

            myEvent.WaitOne();

            // A short sleep to stay responsive on the UI thread

            Thread.Sleep(10);

        }

        public void UIFree()

        {

            // Called on the UI thread as soon as the add is completed

            myEvent.Set();

        }

    }

}

Let me explain what is going on in this class. The main method in this class is FillAsync. FillAsync creates a background thread to add items into the expander view on the UI thread by using the BeginInvoke method on the Dispatcher. After an item is added through this method, we call a separate method called WaitForUI. WaitForUI is a slick way to verify that the UI has completed adding the previously added item and gives the UI a chance to respond to user input, typically scrolling. The AsyncObservableCollection object creates an AutoResetEvent that is triggered once the UI has freed up. To trigger this event, we need to get the UI to set the event when it is free. To do this, we queue up a call to UIFree through the BeginInvoke method on the Dispatcher as well. A picture may help:

The background thread dispatches the Add method followed by a dispatch to FreeUI method and then waits for the event to trigger. The UI thread receives the add call, completes it, calls UIFree which in turn sets the event. This triggers the event and the background thread ends blocking in the WaitAny call, sleeps for 10ms and then continues through the main loop to add the next item. This process repeats until all the items are finally added through the background thread. We give ourselves a 10ms sleep in the background thread to help the UI be a little more responsive.

Now to hook this all up, you need to change the InboxObject so that it derives from AsyncObservableCollection. Then create a new List object called conversations of type ConversationObject and instead of adding the new ConversationObjects to the base of the ObservableCollection through the add method, add the object to this new list object. Once all of the conversations are completed, you can then call FillAsync with your List object and allow the items to be added to the UI in the background. The result is that we immediately begin to see the items populate on the device and we are allowed to start scrolling with just a little stuttering. This is a much better result than waiting 10+ seconds before anything is displayed on the device. The new code for the InboxObject is shown below.

// (c) Copyright Microsoft Corporation.

// This source is subject to the Microsoft Public License (Ms-PL).

// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details.

// All other rights reserved.

using System.Collections.ObjectModel;

using System.Collections.Generic;

namespace PhoneToolkitSample.Data

{

    public class InboxObject : AsyncObservableCollection<ConversationObject>

    {

        public InboxObject() : base()

        {

            List<ConversationObject> conversations = new List<ConversationObject>();

            for (int i = 0; i < 20; i++)

            {

                ConversationObject one = new ConversationObject("3 messages, 0 unread");

                one.Add(new EmailObject("Anne Wallace", "Lunch Today?", "Sure!"));

                one.Add(new EmailObject("Me", "Lunch Today?", "OK, ask Dave?"));

                one.Add(new EmailObject("Bruno Denuit", "Lunch Today?", "I vote for Thai."));

                ConversationObject two = new ConversationObject("1 messages, 0 unread");

                two.Add(new EmailObject("Adriana Giorgi", "Fun trip?", "Lucky you ..."));

                ConversationObject three = new ConversationObject("4 messages, 1 unread");

                three.Add(new EmailObject("Belinda Newman", "hawaii pics!", "Good times."));

                three.Add(new EmailObject("Richard Carey", "hawaii pics!", "Cool fish."));

                three.Add(new EmailObject("C<shorten>r", "h pics!", "Can't wait to get back."));

                three.Add(new EmailObject("Melissa Kerr", "hawaii pics!", "Check it and tag"));

                conversations.Add(one);

                conversations.Add(two);

                conversations.Add(three);

            }

            // Fill the UI in the background

            this.FillAsync(conversations);

        }

    }

}

There is one major drawback with this code as it is presented; you end up making a copy of each of the items before it is presented to the UI thread. For simplicity in demonstrating this technique, I’ve left this drawback in the code.