miércoles, 24 de noviembre de 2010

Scroll Decoupled Behavior for Silverlight

This post introduces the creation of a behavior in order to have a decoupled scroll with separated buttons for moving to the left or right the ScrollViewer and also a separated Thumb element. At the end of this post, I have pasted all the code.

Firstable, you need to expose in this custom behavior the elements:

- LeftButton: A RepeatButton.
- RightButton: A RepeatButton.
- Thumb: A framework element.
- The List Control that we want to manage.


Secondly, you need to define the movements of the scroll. I have declared an instance in order to set the incremental value for each click action by the user on the buttons. For the Thumb movement, it is necessary to calculate the factor between the area of the Thumb with the ScrollViewer that we want to scroll (probably, the ScrollViewer will be higher).

And finally, I need to update the state for all elements for when the size has changed or some property of this behavior.

The usage is like the following example:

           <i:Interaction.Behaviors>
                <behaviors:ScrollDecoupledBehavior LeftButton="{Binding ElementName=btLeftButton}"
                                                   RightButton="{Binding ElementName=btRightButton}"
                                                   Thumb="{Binding ElementName=ScrollBar_Thumb}"
                                                   ElementToScroll="{Binding ElementName=list}"
                                                   />
            </i:Interaction.Behaviors>


This must be added in the area of the Thumb. And this is the result:

And this is all the code used:
 namespace Examples.Behavior
{
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    using System.Windows.Media;

    public class ScrollDecoupledBehavior : Behavior<UIElement>
    {
        /// <summary>
        /// Left button element property.
        /// </summary>
        public static readonly DependencyProperty LeftButtonProperty = DependencyProperty.Register("LeftButton",
            typeof(RepeatButton), typeof(ScrollDecoupledBehavior),
            new PropertyMetadata(null, OnLeftButtonChanged));

        /// <summary>
        /// Right button element property.
        /// </summary>
        public static readonly DependencyProperty RightButtonProperty = DependencyProperty.Register("RightButton",
            typeof(RepeatButton), typeof(ScrollDecoupledBehavior),
            new PropertyMetadata(null, OnRightButtonChanged));

        /// <summary>
        /// Thumb element property.
        /// </summary>
        public static readonly DependencyProperty ThumbProperty = DependencyProperty.Register("Thumb",
            typeof(FrameworkElement), typeof(ScrollDecoupledBehavior),
            new PropertyMetadata(null, OnThumbElementChanged));

        /// <summary>
        /// Element to scroll property.
        /// </summary>
        public static readonly DependencyProperty ElementToScrollProperty = DependencyProperty.Register("ElementToScroll",
            typeof(FrameworkElement), typeof(ScrollDecoupledBehavior),
            new PropertyMetadata(null, OnElementToScrollChanged));

        /// <summary>
        /// Property for the left button.
        /// </summary>
        public RepeatButton LeftButton
        {
            get { return (RepeatButton)GetValue(LeftButtonProperty); }
            set { SetValue(LeftButtonProperty, value); }
        }

        /// <summary>
        /// Property for the right button.
        /// </summary>
        public RepeatButton RightButton
        {
            get { return (RepeatButton)GetValue(RightButtonProperty); }
            set { SetValue(RightButtonProperty, value); }
        }

        /// <summary>
        /// Property for the Thumb object.
        /// </summary>
        public FrameworkElement Thumb
        {
            get { return (FrameworkElement)GetValue(ThumbProperty); }
            set { SetValue(ThumbProperty, value); }
        }

        /// <summary>
        /// Property for the element to scroll. May be the exactly scrollviewer or an element that contains the scrollviewer.
        /// </summary>
        public FrameworkElement ElementToScroll
        {
            get { return (FrameworkElement)GetValue(ElementToScrollProperty); }
            set { SetValue(ElementToScrollProperty, value); }
        }

        /// <summary>
        /// The scroll to be managed.
        /// </summary>
        private ScrollViewer scrollViewer;

        /// <summary>
        /// The value to increment the scroll.
        /// </summary>
        private double incrementFactor = 10.0;

        /// <summary>
        /// Enable the movement regarding the mouse movement by the user.
        /// </summary>
        private bool isPressingMouse = false;

        /// <summary>
        /// Property change event handler for left element.
        /// </summary>
        private static void OnLeftButtonChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            ScrollDecoupledBehavior behavior = depObj as ScrollDecoupledBehavior;

            behavior.AttachLeftButton(e.NewValue as RepeatButton);
        }

        /// <summary>
        /// Attach the events for the left button.
        /// </summary>
        /// <param name="repeatButton"></param>
        private void AttachLeftButton(RepeatButton element)
        {
            this.LeftButton = element;

            if (this.LeftButton != null)
            {
                this.LeftButton.Click += new RoutedEventHandler(this.MoveScrollToLeft);

                this.UpdateState();
            }
        }

        /// <summary>
        /// Property change event handler for right element.
        /// </summary>
        private static void OnRightButtonChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            ScrollDecoupledBehavior behavior = depObj as ScrollDecoupledBehavior;

            behavior.AttachRightButton(e.NewValue as RepeatButton);
        }

        /// <summary>
        /// Attach the events for the right button.
        /// </summary>
        /// <param name="repeatButton"></param>
        private void AttachRightButton(RepeatButton element)
        {
            this.RightButton = element;

            if (this.RightButton != null)
            {
                this.RightButton.Click += new RoutedEventHandler(this.MoveScrollToRight);

                this.UpdateState();
            }
        }

        /// <summary>
        /// Property change event handler for ThumbElement.
        /// </summary>
        private static void OnThumbElementChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            ScrollDecoupledBehavior behavior = depObj as ScrollDecoupledBehavior;

            behavior.AttachThumbElement(e.NewValue as FrameworkElement);
        }

        /// <summary>
        /// Attach the events for the ThumbElement.
        /// </summary>
        /// <param name="repeatButton"></param>
        private void AttachThumbElement(FrameworkElement element)
        {
            this.Thumb = element;

            if (this.Thumb != null)
            {
                this.Thumb.MouseLeftButtonDown += new MouseButtonEventHandler(this.PrepareMovementScroll);
                this.Thumb.MouseLeftButtonUp += new MouseButtonEventHandler(this.UnprepareMovementScroll);
                this.AssociatedObject.MouseLeave += new MouseEventHandler(this.UnprepareMovementScroll);
                this.AssociatedObject.MouseMove += new MouseEventHandler(this.MoveScroll);

                this.UpdateState();
            }
        }

        /// <summary>
        /// Property change event handler for ElementToScroll.
        /// </summary>
        private static void OnElementToScrollChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            ScrollDecoupledBehavior behavior = depObj as ScrollDecoupledBehavior;

            behavior.AttachElementToScroll(e.NewValue as FrameworkElement);
        }

        /// <summary>
        /// Attach the events for the ElementToScroll.
        /// </summary>
        /// <param name="repeatButton"></param>
        private void AttachElementToScroll(FrameworkElement element)
        {
            this.ElementToScroll = element;

            if (this.ElementToScroll != null)
            {
                this.ElementToScroll.Loaded += new RoutedEventHandler(AttachScrollViewer);

                this.UpdateState();
            }
        }

        /// <summary>
        /// Attach the scrollviewer inside of the element to scroll.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void AttachScrollViewer(object sender, RoutedEventArgs e)
        {
            if (this.ElementToScroll is ScrollViewer)
            {
                this.scrollViewer = this.ElementToScroll as ScrollViewer;
            }
            else
            {
                this.scrollViewer = this.GetChild<ScrollViewer>(this.ElementToScroll);
            }

            if (this.scrollViewer != null)
            {
                this.scrollViewer.SizeChanged += new SizeChangedEventHandler(List_SizeChanged);
            }

            this.UpdateState();
        }

        void List_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            this.UpdateState();
        }

        /// <summary>
        /// Attach the specified element where the behavior will start.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
        }

        /// <summary>
        /// Dettach the specified element where the behavior will start.
        /// </summary>
        protected override void OnDetaching()
        {
            base.OnDetaching();

            // Deregister event handlers
            if (this.LeftButton != null)
            {
                this.LeftButton.Click -= new RoutedEventHandler(this.MoveScrollToLeft);
            }

            if (this.RightButton != null)
            {
                this.RightButton.Click -= new RoutedEventHandler(this.MoveScrollToRight);
            }

            if (this.Thumb != null)
            {
                this.Thumb.MouseRightButtonDown -= new MouseButtonEventHandler(this.PrepareMovementScroll);
                this.Thumb.MouseRightButtonUp -= new MouseButtonEventHandler(this.UnprepareMovementScroll);
                this.Thumb.MouseMove -= new MouseEventHandler(this.MoveScroll);
            }
        }

        /// <summary>
        /// End the movement of the thumb regarding the mouse events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void UnprepareMovementScroll(object sender, MouseEventArgs e)
        {
            this.isPressingMouse = false;
        }

        /// <summary>
        /// Start the movement of the thumb regarding the mouse events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void PrepareMovementScroll(object sender, MouseButtonEventArgs e)
        {
            this.isPressingMouse = true;
        }

        /// <summary>
        /// Move the scroll to the right.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void MoveScrollToRight(object sender, RoutedEventArgs e)
        {
            if (this.scrollViewer != null)
            {
                var shift = this.scrollViewer.HorizontalOffset + this.incrementFactor;

                this.scrollViewer.ScrollToHorizontalOffset(shift);

                this.UpdateThumb(shift);
            }

            this.UpdateState();
        }

        /// <summary>
        /// Move the scroll to the left.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void MoveScrollToLeft(object sender, RoutedEventArgs e)
        {
            if (this.scrollViewer != null)
            {
                var shift = this.scrollViewer.HorizontalOffset - this.incrementFactor;

                this.scrollViewer.ScrollToHorizontalOffset(shift);

                this.UpdateThumb(shift);
            }

            this.UpdateState();
        }

        /// <summary>
        /// Update the thumb after clicking the repeat buttons.
        /// </summary>
        /// <param name="shift"></param>
        private void UpdateThumb(double shift)
        {
            var parent = this.AssociatedObject as FrameworkElement;

            // Move the scroll.
            if (this.scrollViewer != null && parent != null)
            {
                double factor = this.scrollViewer.ScrollableWidth / (parent.ActualWidth - this.Thumb.ActualWidth);

                var transform = new CompositeTransform();
                transform.TranslateX = shift / factor;
                this.Thumb.RenderTransform = transform;
            }
        }

        /// <summary>
        /// Move the scroll and the thumb.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void MoveScroll(object sender, MouseEventArgs e)
        {
            var parent = this.AssociatedObject as FrameworkElement;

            if (this.isPressingMouse && parent != null)
            {
                double maxLeft = parent.ActualWidth - this.Thumb.ActualWidth;
                double minLeft = 0.0;

                double positionToMove = e.GetPosition(parent).X - (this.Thumb.ActualWidth / 2);

                if (positionToMove < 0.0)
                {
                    positionToMove = minLeft;
                }
                else if (positionToMove > maxLeft)
                {
                    positionToMove = maxLeft;
                }

                // Move the thumb.
                var transform = new CompositeTransform();
                transform.TranslateX = positionToMove;
                this.Thumb.RenderTransform = transform;

                // Move the scroll.
                if (this.scrollViewer != null)
                {
                    double factor = this.scrollViewer.ScrollableWidth / maxLeft;

                    this.scrollViewer.ScrollToHorizontalOffset(positionToMove * factor);

                    this.UpdateState();
                }

            }
        }

        /// <summary>
        /// Update the states of all elements.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void UpdateState()
        {
            if (this.LeftButton != null)
            {
                if (this.scrollViewer != null)
                {
                    this.LeftButton.IsEnabled = (this.scrollViewer.HorizontalOffset - this.incrementFactor) >= 0.0;
                }

                this.LeftButton.Visibility = this.IsNecessaryVisibility();
            }

            if (this.RightButton != null)
            {
                if (this.scrollViewer != null)
                {
                    this.RightButton.IsEnabled = (this.scrollViewer.HorizontalOffset + this.incrementFactor) <= this.scrollViewer.ScrollableWidth;
                }

                this.RightButton.Visibility = this.IsNecessaryVisibility();
            }

            this.AssociatedObject.Visibility = this.IsNecessaryVisibility();
        }

        /// <summary>
        /// Return Visible if it is necessary to show the scrolls components.
        /// </summary>
        /// <param name="necessary"></param>
        /// <returns></returns>
        private Visibility IsNecessaryVisibility()
        {
            return this.scrollViewer == null || this.scrollViewer.ScrollableWidth <= 0.0 ? Visibility.Collapsed : Visibility.Visible;
        }

        /// <summary>
        /// Get the child element of the type T.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="frameworkElement"></param>
        /// <returns></returns>
        public T GetChild<T>(DependencyObject frameworkElement) where T : DependencyObject
        {
            if (frameworkElement is T)
            {
                return frameworkElement as T;
            }
            else
            {
                var parent = frameworkElement;
                if (frameworkElement is ContentControl)
                {
                    parent = (frameworkElement as ContentControl).Content as FrameworkElement;
                }

                T element = null;

                bool found = false;
                int index = 0;
                while (index < VisualTreeHelper.GetChildrenCount(parent) && !found)
                {
                    element = GetChild<T>(VisualTreeHelper.GetChild(parent, index));
                    found = (element != null);

                    index++;
                }

                return element;
            }
        }
    }
}

No hay comentarios:

Publicar un comentario