WP7: Trigger ActionMessage when scrolling ListBox

Topics: Actions & Coroutines
Jan 20, 2012 at 11:42 AM

It's a no-brainer in WPF but how can I do that in Silverlight/WP7? In WPF the ListBox contains a ScrollViewer which raises the ScrollChanged RoutedEvent. With a custom RoutedEventTrigger its easy to fire the message. But you cannot do that in Silverlight/WP7. There is no ScrollChanged event and I guess the missing support for attached events does not improve the situation.

I want to load data dynamically when the user scrolls down the list.

I found two solutions to that problem which I both don't like.

I can use the Silverlight Commanding support and an attached property

http://danielvaughan.orpius.com/post/Scroll-Based-Data-Loading-in-Windows-Phone-7.aspx

Or I can use a derived control

http://blogs.msdn.com/b/ptorr/archive/2010/10/12/procrastination-ftw-lazylistbox-should-improve-your-scrolling-performance-and-responsiveness.aspx

But I would prefer a solution solely based on Caliburn.

Any ideas?

Coordinator
Jan 20, 2012 at 1:55 PM

You might try writing a custom Trigger based on the code that Daniel shows in the first article.

Jan 20, 2012 at 9:52 PM

Brilliant! That works like a charm :)

Thanks!

Apr 3, 2012 at 11:28 PM

Weberse - any chance you could post your solution?

Apr 4, 2012 at 6:38 AM
Edited Apr 4, 2012 at 6:39 AM

I was playing around with that topic and ended up with two different samples. Can't remember which one worked better.

 

    public class ScrollingStateChangedTrigger : EventTriggerBase<ListBox>
    {
        private bool alreadyHookedScrollEvents;

        protected override string GetEventName()
        {
            return "ScrollingStateChanged";
        }

        protected override void OnAttached()
        {
            var element = (FrameworkElement)this.AssociatedObject;

            element.Loaded += OnLoaded;
        }

        private static VisualStateGroup FindVisualState(FrameworkElement element, string name)
        {
            if (element == null)
                return null;

            IList groups = VisualStateManager.GetVisualStateGroups(element);

            return groups.OfType<VisualStateGroup>().FirstOrDefault(@group => @group.Name == name);
        }

        private static T FindSimpleVisualChild<T>(DependencyObject element) where T : class
        {
            while (element != null)
            {

                if (element is T)
                {
                    return element as T;
                }

                element = VisualTreeHelper.GetChild(element, 0);
            }

            return null;
        } 

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            if (alreadyHookedScrollEvents)
                return;

            alreadyHookedScrollEvents = true;
            ScrollViewer viewer = FindSimpleVisualChild<ScrollViewer>(this.AssociatedObject);

            if (viewer != null)
            {
                // Visual States are always on the first child of the control template
                FrameworkElement element = VisualTreeHelper.GetChild(viewer, 0) as FrameworkElement;
                if (element != null)
                {
                    VisualStateGroup group = FindVisualState(element, "ScrollStates");
                    if (group != null)
                    {
                        group.CurrentStateChanging += OnStateChanging;
                    }
                }
            }
        }

        private void OnStateChanging(object s, VisualStateChangedEventArgs args)
        {
            base.OnEvent(args);
        }
    }

 

and this one

    public class ScrollingTrigger : EventTriggerBase<ListBox>
    {
        /// <summary>
        /// ScrollChanged
        /// </summary>
        private const string ScrollChangedEventName = "ScrollChanged";

        /// <summary>
        /// VerticalOffset
        /// </summary>
        private const string VerticalOffsetPropertyName = "VerticalOffset";

        protected override string GetEventName()
        {
            return ScrollChangedEventName;
        }

        protected override void OnAttached()
        {
            FrameworkElement element = (FrameworkElement)this.AssociatedObject;
            
            element.Loaded += this.OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            FrameworkElement element = (FrameworkElement)sender;
            element.Loaded -= this.OnLoaded;

            ScrollViewer scrollViewer = FindChildOfType<ScrollViewer>(element);

            if (scrollViewer == null)
            {
                throw new InvalidOperationException("ScrollViewer not found.");
            }

            var listener = new DependencyPropertyListener();

            listener.Changed += (sender1, e1) =>
                {
                    var scea = new ScrollChangedEventArgs
                        {
                            ExtentHeight = scrollViewer.ExtentHeight,
                            ExtentWidth = scrollViewer.ExtentWidth,
                            HorizontalOffset = scrollViewer.HorizontalOffset,
                            VerticalOffset = scrollViewer.VerticalOffset,
                            ViewportHeight = scrollViewer.ViewportHeight,
                            ViewportWidth = scrollViewer.ViewportWidth
                        };

                    this.OnEvent(scea);
                };

            Binding binding = new Binding(VerticalOffsetPropertyName) { Source = scrollViewer };
            listener.Attach(scrollViewer, binding);
        }

        private static T FindChildOfType<T>(DependencyObject root) where T : class
        {
            var queue = new Queue<DependencyObject>();
            queue.Enqueue(root);

            while (queue.Count > 0)
            {
                DependencyObject current = queue.Dequeue();
                for (int i = VisualTreeHelper.GetChildrenCount(current) - 1; 0 <= i; i--)
                {
                    var child = VisualTreeHelper.GetChild(current, i);
                    var typedChild = child as T;
                    if (typedChild != null)
                    {
                        return typedChild;
                    }

                    queue.Enqueue(child);
                }
            }

            return null;
        }
    }

If you need some more information you can find the WP7 sample in the source code of the TecX project here on codeplex. Its in the TecX.Agile.Phone solution.