Wednesday, May 23, 2012

Adventures with Telerik's RadLoopingList: Displaying Directions

Recently I've been trying to use the RadLoopingList custom control by Telerik for my WP7 app, "Where's My Game?", but I ran into several complications that made it hard to get the list to update when the user scrolled to an item that existed off the page.  I found this problem pretty tricky to solve and my particular usage seemed more complicated than the online examples/documentation (I used a mashup of these and trial-and-error to solve the problem).  A few other people have been posting similar questions to mine online, so I wanted to share the problem and my solution here, in case others find it helpful too.

The Problem
The control is very fast/efficient because it only renders items that are visible, so the off-the-page item would be rendered on the fly.  The first time I tried to use the control, I ended up with the off-the-page items being replaced by the first items in the list.  For example, given the size of the screen and the font sizes I used, I ended up rendering items 0-7 on the screen.  Then when I scrolled to see item 8, I would instead see item 0.  Item 9 would be item 1, and so forth.  After some fiddling around, I managed to improve the situation by instead seeing item 8 for item 8, item 8 for item 9, and so on to the end of the list.  Here's what I did to fix it.
My RadLoopingList custom control showing the first 7 items in my list.
Problem 1: Item 8, 9, and 10 are replaced with item 0, 1, and 2
Problem 2: Items 9 and 10 are replaced with item 8, repeated.
The Solution
There are three pieces you need when using the control:
  1. Use ItemNeeded() and ItemUpdated() methods to get/update list items when the list is populated/scrolled on by the user.
  2. Use the INotifyPropertyChanged interface to make sure the data you bind to is updated appropriately
  3. Some additional logic to determine the data to populate an existing list item that was off the page prior to being scrolled.
You can see a very simple example of #1 as provided by Telerik here.  For #2, you can take a look at this MSDN page on INotifyPropertyChanged for some examples and explanation of the interface.  However, even with these two pieces I still had the problems shown in the screenshots above.

The main modifications are in the ItemNeeded() and ItemUpdated() methods.  There are two main ideas here:
  1. Use a bool, "moved", to keep track of whether or not we have moved the enumerator forward in the data list or not.  The reason is that even when following the recommended guidelines for #1 and #2 in my solution items above, when updating the last item in the list the enumerator will not be moved forward, since we only make a call to ItemUpdated and not ItemNeeded in that case.  You need to keep track with a bool because if you simply move the enumerator each time you call ItemNeeded and ItemUpdated, you will move the enumerator twice as often as necessary when first populating the list.
  2. Use the parameter "args" to get the index of the list item that is being updated: "args.Index".  You can't just move the enumerator forward every time since the user might have scrolled up the list, in which case you need to move it backwards.  However, there are no built in methods to move it backwards from its current position.  My solution is to reset the enumerator to 0 and move it forward until it is at the position indicated by args.Index.  This seems to work pretty well.
Here is an excerpt of the code to give you an idea of how it works:
src.ItemNeeded += (sender, args) = > { itineraryListEnumerator.Reset(); for (int i = 0; i <= args.Index; i++) { itineraryListEnumerator.MoveNext(); moved = true; } DirectionsPanel dp = new DirectionsPanel(); dp.distanceInfo = itineraryListEnumerator.Current.Summary.Distance.ToString() + " mi"; dp.directionsText = parseXML(itineraryListEnumerator.Current.Text); dp.itemNumber = args.Index.ToString(); args.Item = new DirectionsPanel() { distanceInfo = dp.distanceInfo, itemNumber = dp.itemNumber, directionsText = dp.directionsText }; }; src.ItemUpdated += (sender, args) => { if (moved == false) { itineraryListEnumerator.Reset(); for (int i = 0; i <= args.Index; i++) { itineraryListEnumerator.MoveNext(); moved = true; } } DirectionsPanel dp = new DirectionsPanel(); dp.distanceInfo = itineraryListEnumerator.Current.Summary.Distance.ToString() + " mi"; dp.directionsText = parseXML(itineraryListEnumerator.Current.Text); dp.itemNumber = args.Index.ToString(); (args.Item as DirectionsPanel).distanceInfo = dp.distanceInfo; (args.Item as DirectionsPanel).directionsText = dp.directionsText; (args.Item as DirectionsPanel).itemNumber = dp.itemNumber; moved = false; };

Just a note here: I've actually populated the RadLoopingList with a stackpanel with nested panels inside it which contain the actual data.  This is a nice way to customize the control and include more than just text.

I don't want this post to get too long and if you've made it this far, kudos to you!  I'll post more in the coming days on how I customized the control and how to use INotifyPropertyChanged on your data.  Look for more details on my solution in Part 2 of this post, coming soon.

No comments: