Never do today what you can put off till tomorrow [DeferredLoadListBox (and StackPanel) help Windows Phone 7 lists scroll smoothly and consistently]


This blog has moved to a new location and comments have been disabled.

All old posts, new posts, and comments can be found on The blog of dlaa.me.

See you there!

Comments (35)
  1. jus says:

    Is there a version for WPF? This would be great!

    Thanks

    Jus

  2. Delay says:

    jus,

    My experience is that WPF doesn't suffer from the underlying performance problem – so you can do things the "default" way there and they'll perform just fine. :) However, if you're seeing similar problems on WPF and want to try DeferredLoadListBox out there, it should compile and work for that platform pretty much as-is. There might be a tweak or two that I'm not thinking of right now, but everything I used and did seems like it will apply equally well to WPF (or Silverlight 4).

    Hope this helps!

  3. jus says:

    Delay,

    thanks for your answer. If you use WPF on a slower machine, scrolling feels a bit "sluggish". Even on faster machines this could happen, when your ListBoxItems are quite complex. I think this lies in the nature of the VirtualizingStackPanel.

    I will try to implement your DeferredLoadListBox in my WPF app and let you know how it works :-)

    Thanks again for your great work!

    Jus

  4. David Burela says:

    Thank you so much for this. I was having performance issues as I had a List with 100 items (2 text boxes and an image). The list would glitch and jerk about as I scrolled. This instantly fixed the issues and it was really as easy as just changing the control name in the xaml. Thanks a lot for this!

  5. Delay says:

    David Burela,

    That's really great to hear – thanks a lot for sharing your success story! :)

  6. Johnny Westlake says:

    Hey, great work, it's bene really helpful with my program!

    One problem I've noticed though with the LowProfileImageLoader is when you navigate back to a page using the back button, it reloads all the images, which considering these programs will be running on mobile devices with limited/slow conenctivity, this isn't ideal. With the normal image control this doesn't happen – is there any solution??

    Thanks,

    John

  7. Delay says:

    Johnny Westlake,

    I suspect what's happening here is that both the "normal" way and the LowProfileImageLoader way are always working the same – the difference is whether the images are cached locally by the downloader or not. When they *are* cached (i.e., when you navigate back), they can show up very quickly in the "normal" scenario since it doesn't need to download them, but LowProfileImageLoader doesn't know this and throttles the "downloads" anyway. There's still probably no network traffic for the cached images, but it's the throttling you're seeing that makes it look slower.

    So if you're concerned about the network traffic, I think it's the same in both cases. But if you're concerned about the perceived speed in the "Back" scenario, that's an issue. It'd be nice if LowProfileImageLoader could tell the images were already downloaded, but I'm not sure it can… I'll add a note to look into this. For now, you might consider augmenting LowProfileImageLoader so you can tell it you're in the "Back" scenario and it can assume everything's present and fast-track the loading process.

  8. Rudi Grobler says:

    Is it possible to tweak LowProfileImageLoader to always cache the image in isolated storage?

  9. Delay says:

    Rudi Grobler,

    Yes, I'd expect that could be done. Off the top of my head, there are two things to keep in mind:

    1. You'd want to have some policy in place for eventually expiring the cached elements if the actual image was updated.

    2. I'm aware of various efforts to implement a "IsolatedStorage as local cache" scheme – your scenario might be better served by using something like that and falling back to LowProfileImageLoader only when it's really necessary to hit the web.

    Hope this helps!

  10. William Wong says:

    I found that in some rare cases, when you are checking for "overlap", the child may not belong to the parent. The following is the exception I captured. I am unsure how to trigger it, I just keep adding/removing stuff on my list.

    The parameter is incorrect.

      at MS.Internal.XcpImports.MethodEx(IntPtr ptr, String name, CValue[] cvData)

      at MS.Internal.XcpImports.MethodPack(IntPtr objectPtr, String methodName, Object[] rawData)

      at MS.Internal.XcpImports.UIElement_TransformToVisual(UIElement element, UIElement visual)

      at Delay.DeferredLoadListBox.Overlap(FrameworkElement parent, FrameworkElement child, Double padding)

      at Delay.DeferredLoadListBox.UnmaskVisibleContent()

      at Delay.DeferredLoadListBox.<PrepareContainerForItemOverride>b__1()

      at System.Reflection.RuntimeMethodInfo.InternalInvoke(RuntimeMethodInfo rtmi, Object obj, BindingFlags invokeAttr, Binder binder, Object parameters, CultureInfo culture, Boolean isBinderDefault, Assembly caller, Boolean verifyAccess, StackCrawlMark& stackMark)

      at System.Reflection.RuntimeMethodInfo.InternalInvoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, StackCrawlMark& stackMark)

      at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)

      at System.Delegate.DynamicInvokeOne(Object[] args)

      at System.MulticastDelegate.DynamicInvokeImpl(Object[] args)

      at System.Windows.Threading.DispatcherOperation.Invoke()

      at System.Windows.Threading.Dispatcher.Dispatch(DispatcherPriority priority)

      at System.Windows.Threading.Dispatcher.OnInvoke(Object context)

      at System.Windows.Hosting.CallbackCookie.Invoke(Object[] args)

      at System.Windows.RuntimeHost.ManagedHost.InvokeDelegate(IntPtr pHandle, Int32 nParamCount, ScriptParam[] pParams, ScriptParam& pResult)

  11. Delay says:

    William Wong,

    The Windows Phone 7 implementation of TransformToVisual seems to have some kind of quirk that leads to issues like this in seemingly unexpected situations. :( I've successfully fixed something very similar for ContextMenu by switching to the LayoutInformation.GetLayoutSlot method (instead of TransformToVisual), so that should work here, too. In fact, I've heard from a customer who did just that with DeferredLoadListBox to avoid an exception with a similar-sounding call stack to yours. If you're able to find a reliable way to reproduce the problem, that would be a great way to be sure the fix was good. I've got a note on my TODO list to look into this, but I haven't had a chance to do so yet – so if you give this a try and have good luck, I'd love to hear it!

    Thanks for the report – hope this helps!

  12. Michael says:

    David – I was getting a very similar stack trace to William when using Peter Torr's LazyListBox inside a Pivot on a Page that I was Navigate()ing to from the Application.Current.RootVisual but I'm not sure if this is 100% reproducible (I can try and create a simple test case, if you need). As per your advice I changed I changed the call to TransformToVisual in ExtensionMethod TestVisibility (included in Torr's LazyListBox project) to GetLayoutSlot and it fixed the issue.

  13. Delay says:

    Michael,

    Great news! It seems like there's a platform bug here, but it's nice to know it can be worked around in user code. :)

    Thanks for sharing!

  14. dinesh, says:

    Thanks for your information, It helps me better understanding.. yes Thanks for this Information.

  15. Holger says:

    Thank you for your description and the sample. It helped me a lot.

    Now I have one problem with DeferredLoadListBox. My user can switch between different lists and this works as expected. However if I switch the list the current scrolling position is preserved. So if I happen to scroll three items down and the other list has only one item the screen looks like empty. I need to scroll up manually to see the one item of the new list.

    I tried DeferredLoadListBox.ScrollIntoView() to scroll to the first item but this will just result in an exception. Do you have an idea how to reset the scroll position of DeferredLoadListBox via code?

  16. Holger says:

    addon: with exchanging the list I meant I will exchange the ItemSource of the DeferredLoadListBox and this is when the current scrolling position is preserved.

  17. Delay says:

    Holger,

    (Just curious: Is there any chance this happens with the standard ListBox as well?)

    Off the top of my head, I'm not sure why DeferredLoadListBox would introduce this behavior. But what you might do to avoid this is set ScrollViewer.VerticalOffset to 0 during the switch to a different ItemsSource (perhaps by using the _scrollViewer member variable DeferredLoadListBox already has). This ought to reset the scroll to the top and resolve the problem! :)

  18. Holger says:

    I think my last answer got lost:

    I made the scrollviewer public and tried the following:

    MyDeferredList.scrollViewer.ScrollToVerticalOffset(-double.MaxValue);

    This will fire the exception. The problem is caused by the method UnmaskVisibleContent of DeferredLoadListBox. This method is fired when I try to scroll to the top of the list. A dirty way around the problem is to implement a switch. This switch will prevent UnmaskVisibleContent from running. After the scroll operation I disable the switch. Not a nice solution but it works as expected.

  19. Delay says:

    Holger,

    I'm glad to hear you found something that works for you!

    FYI that I've made a note to investigate this problem further next time I'm working with this code.

    Thanks!

  20. Holger says:

    David,

    I have another one. I have two pages. The first page is the starter page and the second page has the DeferredLoadListBox. I navigate to this second page and the DeferredList is populated with images. Now I will swipe with my finger and the list is scrolling (I am not using ScrollToVerticalOffset – just my fingers). Within this scroll operation I will hit the back button of the phone. Now the navigation is going back to the starter page – I see the applicationbar of the starter page coming up. But the images of the DeferredList are still on top of the starter page (like an overlay). So the app remains in an unusable state and I need to force the app to shutdown.

    I solved the problem by stopping any scroll operation of the DeferredList before I go back to the previous page:

    protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)

    {

         MyDeferredList.scroll = true;

         MyDeferredList.scrollViewer.ScrollToVerticalOffset(-double.MaxValue);

         MyDeferredList.scroll = false;

         base.OnNavigatedFrom(e);

    }

    Let me know if you need my xap for testing.

  21. Delay says:

    Holger,

    That sounds really weird – and I'm not sure how DeferredLoadListBox could even be responsible: the navigation service should have switched to a new page. If you contact me via the "Email Blog Author" link in the sidebar, I'll reply and let you know how to send a XAP file that I can pass on to the WP7 team.

    Thanks!

  22. Sam says:

    Am I out of luck if I need my listboxitem containers to be different sizes?

  23. Delay says:

    Sam,

    Nope, you should be fine! :) One of the last paragraphs in my original post discusses this: "… it's important to note there's no need for all the containers to have the same fixed height – just that they all need to have a fixed height". In other words, the containers can all be different sizes, *but* those sizes need to be set on the containers directly via Height=N (instead of calculated by the layout system) because of how I originally implemented things.

    Hope this helps!

  24. Geoff says:

    So if Height=N and N is fixed/hard-coded, I'm guessing this may not work well for say twitter search results where you won't know the height ahead of time due to differing data lengths of the tweets? I can set the height to the largest possible height but then of course there will be large gaps for lesser content. I don't believe I can do a binding inside the required Height setter value of the ListBoxItem.

  25. Delay says:

    Geoff,

    I agree the fixed height requirement doesn't play well with the scenario you describe. :( That said, I have just added a note to my TODO list to reconsider whether that limitation is really necessary. It seemed so at the time, but maybe when I revisit this, I'll realize it isn't…

    PS – If you want to do Bindings in a Silverlight Setter, you can use my SetterValueBindingHelper:

    blogs.msdn.com/…/the-taming-of-the-phone-new-settervaluebindinghelper-sample-demonstrates-its-usefulness-on-windows-phone-7-and-silverlight-4.aspx

  26. Geoff says:

    FYI: I occasionally get the below error using the control. It appears to happen sporadically when I'm using threading and I'm refreshing the data for the application while on a panorama item other than the panorama item that contains the DeferredLoadListBox. I customized the error message a bit; the error ws a little misleading as the ItemsPanel is a StackPanel but the condition for throwing the exception is hit. Any ideas?

    System.NotSupportedException: Couldn't find container for item (ItemsPanel should be a StackPanel); panel is of type StackPanel

      at Phone.Common.Controls.DeferredLoadListBox.UnmaskVisibleContent()

      at Phone.Common.Controls.DeferredLoadListBox.<PrepareContainerForItemOverride>b__1()

      at System.Reflection.RuntimeMethodInfo.InternalInvoke(RuntimeMethodInfo rtmi, Object obj, BindingFlags invokeAttr, Binder binder, Object parameters, CultureInfo culture, Boolean isBinderDefault, Assembly caller, Boolean verifyAccess, StackCrawlMark& stackMark)

      at System.Reflection.RuntimeMethodInfo.InternalInvoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, StackCrawlMark& stackMark)

      at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)

      at System.Delegate.DynamicInvokeOne(Object[] args)

      at System.MulticastDelegate.DynamicInvokeImpl(Object[] args)

      at System.Windows.Threading.DispatcherOperation.Invoke()

      at System.Windows.Threading.Dispatcher.Dispatch(DispatcherPriority priority)

      at System.Windows.Threading.Dispatcher.OnInvoke(Object context)

      at System.Windows.Hosting.CallbackCookie.Invoke(Object[] args)

      at System.Windows.RuntimeHost.ManagedHost.InvokeDelegate(IntPtr pHandle, Int32 nParamCount, ScriptParam[] pParams, ScriptParam& pResult)

  27. Delay says:

    Geoff,

    This may be another of those times where the platform calls into some code and things are in a slightly weird state. What's probably happening is that DeferredLoadListBox believes that an item is visible based on its own calculations, but the platform hasn't yet created a ListBoxItem container for it that item. Going by your scenario description above, it *could* be because you're adding new items within the visible part of the list, but the control itself isn't visible, the containers don't get created. It may be easy to check in the debugger – if that's the case, you might look at whether there's an obvious tweak to my DeferredLoadListBox code or perhaps whether it would be possible to defer adding those items until the control becomes visible and the "normal" behavior will be restored.

  28. Prakash says:

    David,

    That is a great work, there. I have few queries on this post.

    I am having a two different layouts that are to be displayed in a list box. Are there any additional tweaks I need to do, since you told you did some optimizations considering fixed height layouts. My app is like a search engine which will list down the results so there might only one sluggish scroll he will face and he will change the query, so the app will not allow him to enjoy a smooth scrolling( in the second or third times). Is there some thing I can do to handle this scenario.

  29. Delay says:

    Prakash,

    If the principles here work well for you, but you want to customize the containers, you can also modify the code I've shared to take that into account. In this case, assuming everything has the same height allows the code to quickly determine which items are on screen based only on the scroll position. As long as you know the heights of the individual items, you can accomplish the same thing with just a bit more math. :)

    Hope this helps!

  30. I am usig defferedListBox + stackpanel. Everything works great while loading images and scrolling data in list (around 25 items) but the problem start coming when i go back from list screen to previous screen and again come to list screen quickly. Now i can't access my database layer methods for some time (8-10 second) as i added AutoResetEvent  OperationOnDatabase.WaitOne(); to avoid multiple thread access on database layer methods. But if i comeback to previous screen and wait for some time(10-12 second) and then go forward then it does not wait on OperationOnDatabase.WaitOne(); and immediately access database layer methods.  It seems unloadig of defferedListBox takes times and start some background threads.Is there anyway to forcefully complete or kill background threads processing or dispose all resources used by this defferedListBox ?

  31. Delay says:

    gbawa,

    It sounds like your scenario has a lot going on!

    What I'd recommend is switching to a normal ListBox and making sure everything is working okay there first. Once it is, switch back to DeferredLoadListBox and hopefully everything keeps working. If not, then I'd recommend looking at the stack trace of your calls that manipulate the AutoResetEvent because it sounds like you've identified that as the cause of the periodic blocking. It's possible some aspect of how DeferredLoadListBox works is interfering with your synchronization code, but I've skimmed the code again just now and it doesn't appear to do anything during unload nor does it start any threads itself. Getting this in the debugger's probably the quickest way to identify the source of the different behavior.

    Hope this helps!

  32. David,

    Thanks for response!

    I checked on ListBox and same problem exist. It seems to be other issue not related to defferedlistbox. I am creating multiple threads which are performing operations on database layer (see below code) and i am using AutoresetEvent  WaitOne() in database methods to avoid multiple thread access to database and here problem starts. It wait so long. I can't provide timeout as it will loose call. Is there anyother way to intract with database in multithread invironment ?

    Thread t = new Thread(new ParameterizedThreadStart(database.readData));

    object[] obj = new object[2] { retval, UserId};

    t.Start(obj);

    public void readData(object o)

    {

    try{

     OperationOnDatabase.WaitOne();

     {

         //Here intract with database table

     }

    }

    Catch{}

    finally{ OperationOnDatabase.Set();}

    }

  33. Delay says:

    gbawa,

    I don't think AutoResetEvent has the semantics you want here as it wakes *all* listeners who are listening *when signalled*. What seems more appropriate for this scenario is a mutex which you can get in .NET via the simple lock(object) { … } syntax. I'd suggest trying that as it should simplify the code and do what you want more reliably.

    Good luck! :)

  34. sanath shetty says:

    Hi David ,

    i am using the DeferredLoadListBox in a panorama application and  first of all the responsiveness of the control is awesome ,

    but  there is a random crash with an exception  Couldn't find container for item (ItemsPanel should be a StackPanel).  

    and below is the stack trace for the same

    at Delay.DeferredLoadListBox.UnmaskVisibleContent()

      at Delay.DeferredLoadListBox.OnVerticalOffsetShadowChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)

      at System.Windows.DependencyObject.RaisePropertyChangeNotifications(DependencyProperty dp, Object oldValue, Object newValue)

      at System.Windows.DependencyObject.UpdateEffectiveValue(DependencyProperty property, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, ValueOperation operation)

      at System.Windows.DependencyObject.RefreshExpression(DependencyProperty dp)

  35. Delay says:

    sanath shetty,

    In cases where the exception is thrown, what IS the type of the container that's causing the code to throw?

Comments are closed.