Using LongListSelector without LINQ

The LongListSelector control is an awesome thing, but using it without LINQ I found a tricky business. Here is what I learned.

[Updated 11/20 to grossly simplify the code]

I am pretty new to Silverlight and to DataBinding, and I was happy when I started a few months ago that I got the a VS Wizard-generated ListBox UI working with an asynchronous web service. However for large lists a simple ListBox isn't usable, so I tried using the LongListSelector (LLS) from the recently released Silverlight Toolkit: I wanted an alphabetic "jump list". I read the sample code, it seemed ok, then I tried adding it to my code. It all went horribly wrong due to my lack of experience in these areas, so then I tried rolling my own version of a jump list, and for asynchronous data that didn't work out well either. Then I found this great article by WindowsPhoneGeek and the fog began to clear.

I took the Group class from that article, I changed my data source from ObservableCollection<MyType> to ObservableCollection<Group<MyType>>, updated the XAML and tried it with a hard-coded list of items to start with. Unlike the Toolkit sample code, and unlike the same code in the article, I could not use LINQ to build my data source as it is asynchronous. Weirdly my code died while adding items to my collection, with this callstack:

  Microsoft.Phone.Controls.Toolkit.dll!Microsoft.Phone.Controls.LongListSelector.OnRootCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + 0x182 bytes 
  System.Windows.dll!System.Collections.ObjectModel.ObservableCollection<SampleApp.Group<SampleApp.ItemViewModel>>.OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + 0x1c bytes 
  System.Windows.dll!System.Collections.ObjectModel.ObservableCollection<SampleApp.Group<SampleApp.ItemViewModel>>.InsertItem(int index, SampleApp.Group<SampleApp.ItemViewModel> item) + 0x37 bytes 
  mscorlib.dll!System.Collections.ObjectModel.Collection<SampleApp.Group<SampleApp.ItemViewModel>>.Add(SampleApp.Group<SampleApp.ItemViewModel> item) + 0x28 bytes 

and none of that code is mine. This stumped me entirely, until someone on a list at work determined that the data source must also inherit from IList. Not IList<T> but the old-school IList interface. I added that to the Group class, implemented the bare miniumum of methods, and now my test code worked! I rushed to hook it up to my asynchronous population code, and that resulted in an entirely empty list. No crashes, but no data visible at all. Debugging revealed that the LLS was NOT hooked up to the change notification of the collection, which is why as things were added, nothing in the list updated.

This turned out to be because the data source also has to be a member of INotificationCollectionChanged. Once I generated those notifications, the list was populated asynchronously and the LLS worked fabulously. (LINQ really does perform magic, when you realize how much code you have to write when you don't use it). You can see the original code I wrote at the end, which was in the first version of this post. However someone who actually understands this stuff (thanks John!) pointed out to me that I had basically re-implemented ObservableCollection, so the updated code looks like this:

    public class Group<T> : ObservableCollection<T>

    {

        public string Title

        {

            get;

            set;

        }

 

        public bool HasItems

        {

            get

            {

                return Count != 0;

            }

            private set

            {

            }

        }

 

        public Group(string name)

        {

            this.Title = name;

        }

    }

Original Code

For reference here is my original version of the Group class. You'll notice that many methods throw NotImplemented exceptions, I only implemented them as they were hit during development. You may find additional work is required for your scenario (I only need to Clear and Add to my collection), but this should get you up and running.

    public class Group<T> : IEnumerable<T>, IList, INotifyCollectionChanged

    {

        public Group(string name)

        {

            this.Title = name;

            this.Items = new List<T>();

        }

 

        public Group(string name, IEnumerable<T> items)

        {

            this.Title = name;

            this.Items = new List<T>(items);

        }

 

        public override bool Equals(object obj)

        {

            Group<T> that = obj as Group<T>;

            return (that != null) && (this.Title.Equals(that.Title));

        }

 

        public string Title

        {

            get;

            set;

        }

 

        public IList<T> Items

        {

            get;

            set;

        }

 

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()

        {

            return this.Items.GetEnumerator();

        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

        {

            return this.Items.GetEnumerator();

        }

        #endregion

 

        #region IList Members

        public int Add(object value)

        {

            if (value is T)

            {

                this.Items.Add((T)value);

                int index = this.Items.Count - 1;

               NotifyCollectionChanged_Add((T)value, index);

                return index;

            }

            else

                throw new InvalidOperationException("Adding wrong type");

        }

 

        public void Clear()

        {

            this.Items.Clear();

            this.NotifyCollectionChanged_Reset();

        }

 

        public bool Contains(object value)

        {

            throw new NotImplementedException();

        }

 

        public int IndexOf(object value)

        {

            throw new NotImplementedException();

        }

 

        public void Insert(int index, object value)

        {

            throw new NotImplementedException();

        }

 

        public bool IsFixedSize

        {

            get { return false; }

        }

 

        public bool IsReadOnly

        {

            get { return false; }

        }

 

        public void Remove(object value)

        {

            throw new NotImplementedException();

        }

 

        public void RemoveAt(int index)

        {

            throw new NotImplementedException();

        }

 

        public object this[int index]

        {

            get

            {

                return Items[index];

            }

            set

            {

                throw new NotImplementedException();

  }

        }

 

        public void CopyTo(Array array, int index)

        {

            throw new NotImplementedException();

        }

 

        public int Count

        {

            get { return Items.Count; }

        }

 

        public bool IsSynchronized

        {

            get { return false; }

        }

 

        public object SyncRoot

        {

            get { return this; }

        }

        #endregion

 

        public event NotifyCollectionChangedEventHandler CollectionChanged;

 

    private void NotifyCollectionChanged_Reset()

        {

            NotifyCollectionChangedEventHandler handler = CollectionChanged;

            if (null != handler)

            {

                handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

            }

        }

 

        private void NotifyCollectionChanged_Add(T item, int index)

        {

            NotifyCollectionChangedEventHandler handler = CollectionChanged;

            if (null != handler)

          {

                handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));

            }

        }

 

    }