Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Scroll Synchronization

0.00/5 (No votes)
28 Dec 2013 1  
This is an alternative for Scroll Synchronization

Introduction

I came across Scroll Synchronization which I utilized in my own project. But it needed to have its capabilities expanded and a couple of flaws fixed...

Background

Please refer to the original article this is in response to, to understand the basics which this article builds upon.

The original only covers the use of basic synchronized ScrollViewers and doesn't handle situations like programmatic generation of visual elements etc... You end up with scroll bars not being set correctly etc... or not updating properly and it also only handles ScrollViewers... Whatever happened to using ScrollBars as well???

So I thought everyone might get some use out of this expanded ScrollSynchronizer... which addresses these issues.

Using the code

Once again it is the same simple class structure as specified in the original article. It uses the same simple attached property...

<ScrollViewer 
    Name="ScrollViewer1" 
    scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollViewer>
<ScrollViewer 
    Name="ScrollViewer2" 
    scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollViewer>

But it now supports attaching the property to ScrollBars as well... at the same time!!!

<ScrollBar 
    Name="ScrollBar1" 
    scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollBar>

I now have two ScrollViewers and one ScrollBar, all linked together via "Group1".

How neat is that!!! And everything behaves the way you think it would. If it doesn't go ahead and hack the relevant code...

You can also mix and match the orientations or the ScrollBars and the visibility of the ScrollViewer scrollbars. The code makes sure only the relevant oriented scrollbars are updated etc...

Demo Project

I've supplied a simple demo project so you can see the linked scrollviewers and scrollbars all linked together and working. So download and have a play.

Note I've purposely made the scrollviewers and the scrollbars all different sizes so you can see that it all still works together as you would expect.

Code

So here is the code:

Please note I like to use capitalized keywords like Double instead of double - it looks nicer in the editor...

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace Temp
{
    public class ScrollSynchronizer : DependencyObject
    {
        // Variables
        public static DependencyProperty ScrollGroupProperty;
        static Dictionary<Object, String> m_Scrollers;
        static Dictionary<String, List<Object>> m_GroupScrollers;
        static Dictionary<String, Double> m_HorizontalScrollPositions;
        static Dictionary<String, Double> m_HorizontalScrollLengths;
        static Dictionary<String, Double> m_VerticalScrollPositions;
        static Dictionary<String, Double> m_VerticalScrollLengths;

        // Properties
        public String ScrollGroup
        {
            get { return (String)GetValue(ScrollGroupProperty); }
            set { SetValue(ScrollGroupProperty, value); }
        }

        // Constructors
        static ScrollSynchronizer()
        {
            ScrollGroupProperty = DependencyProperty.RegisterAttached("ScrollGroup", typeof(String),
                typeof(ScrollSynchronizer),
                new PropertyMetadata(new PropertyChangedCallback(OnScrollGroupChanged)));
            m_Scrollers = new Dictionary<Object, String>();
            m_GroupScrollers = new Dictionary<String, List<Object>>();
            m_HorizontalScrollPositions = new Dictionary<String, Double>();
            m_HorizontalScrollLengths = new Dictionary<String, Double>();
            m_VerticalScrollPositions = new Dictionary<String, Double>();
            m_VerticalScrollLengths = new Dictionary<String, Double>();
        }

        // Get/Set
        public static void SetScrollGroup(DependencyObject obj, String nScrollGroup)
        {
            obj.SetValue(ScrollGroupProperty, nScrollGroup);
        }
        public static String GetScrollGroup(DependencyObject obj)
        {
            return (String)obj.GetValue(ScrollGroupProperty);
        }

        static void OnScrollGroupChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer sv = obj as ScrollViewer;
            ScrollBar sb = obj as ScrollBar;
            if ((sv == null) && (sb == null))
                return;

            String ov = (String)e.OldValue;
            String nv = (String)e.NewValue;

            if (!String.IsNullOrEmpty(ov))
            {
                if ((sv != null) && (m_Scrollers.ContainsKey(sv)))
                {
                    sv.ScrollChanged -= new ScrollChangedEventHandler(ScrollViewer_ScrollChanged);
                    m_Scrollers.Remove(sv);
                    m_GroupScrollers[ov].Remove(sv);
                }

                if ((sb != null) && (m_Scrollers.ContainsKey(sb)))
                {
                    sb.IsEnabled = false;
                    sb.Scroll -= new ScrollEventHandler(ScrollBar_Scroll);
                    m_Scrollers.Remove(sb);
                    m_GroupScrollers[ov].Remove(sb);
                }

                // Kill off if nobody left in the group
                if (m_GroupScrollers[ov].Count == 0)
                {
                    m_GroupScrollers.Remove(ov);
                    m_HorizontalScrollPositions.Remove(ov);
                    m_HorizontalScrollLengths.Remove(ov);
                    m_VerticalScrollPositions.Remove(ov);
                    m_VerticalScrollLengths.Remove(ov);
                }
            }

            if (!String.IsNullOrEmpty(nv))
            {
                // Prepare the group
                if (!m_GroupScrollers.ContainsKey(nv))
                    m_GroupScrollers.Add(nv, new List<Object>());

                if (sv != null)
                {
                    if (sv.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled)
                    {
                        if (m_HorizontalScrollPositions.ContainsKey(nv))
                            SetScrollViewerHorizontalPosition(sv, m_HorizontalScrollPositions[nv]);
                        else
                        {
                            m_HorizontalScrollPositions.Add(nv, GetScrollViewerHorizontalPosition(sv));
                            m_HorizontalScrollLengths.Add(nv, GetScrollViewerHorizontalLength(sv));
                        }
                    }

                    if (sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
                    {
                        if (m_VerticalScrollPositions.ContainsKey(nv))
                            SetScrollViewerVerticalPosition(sv, m_VerticalScrollPositions[nv]);
                        else
                        {
                            m_VerticalScrollPositions.Add(nv, GetScrollViewerVerticalPosition(sv));
                            m_VerticalScrollLengths.Add(nv, GetScrollViewerVerticalLength(sv));
                        }
                    }

                    m_Scrollers.Add(sv, nv);
                    m_GroupScrollers[nv].Add(sv);

                    sv.ScrollChanged += new ScrollChangedEventHandler(ScrollViewer_ScrollChanged);
                }

                if (sb != null)
                {
                    sb.IsEnabled = true;

                    if (sb.Orientation == Orientation.Horizontal)
                    {
                        if (m_HorizontalScrollPositions.ContainsKey(nv))
                        {
                            SetScrollBarPosition(sb, m_HorizontalScrollPositions[nv]);
                            SetScrollBarLength(sb, m_HorizontalScrollLengths[nv]);
                        }
                        else
                        {
                            m_HorizontalScrollPositions.Add(nv, GetScrollBarPosition(sb));
                            m_HorizontalScrollLengths.Add(nv, GetScrollBarLength(sb));
                        }
                    }
                    else
                    {
                        if (m_VerticalScrollPositions.ContainsKey(nv))
                        {
                            SetScrollBarPosition(sb, m_VerticalScrollPositions[nv]);
                            SetScrollBarLength(sb, m_VerticalScrollLengths[nv]);
                        }
                        else
                        {
                            m_VerticalScrollPositions.Add(nv, GetScrollBarPosition(sb));
                            m_VerticalScrollLengths.Add(nv, GetScrollBarLength(sb));
                        }
                    }

                    m_Scrollers.Add(sb, nv);
                    m_GroupScrollers[nv].Add(sb);

                    sb.Scroll += new ScrollEventHandler(ScrollBar_Scroll);
                }
            }
        }
        static void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)
        {
            Scroll(sender as ScrollViewer, (e.HorizontalChange != 0), (e.VerticalChange != 0));
        }
        static void ScrollBar_Scroll(Object sender, ScrollEventArgs e)
        {
            Scroll(sender as ScrollBar, false, false);
        }
        static void Scroll(Object nChangeScroller, Boolean nHorzChange, Boolean nVertChange)
        {
            String group = m_Scrollers[nChangeScroller];

            ScrollViewer svc = nChangeScroller as ScrollViewer;
            ScrollBar sbc = nChangeScroller as ScrollBar;

            // Record the position and length
            if (svc != null)
            {
                if (svc.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled)
                {
                    m_HorizontalScrollPositions[group] = GetScrollViewerHorizontalPosition(svc);
                    m_HorizontalScrollLengths[group] = GetScrollViewerHorizontalLength(svc);
                }

                if (svc.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
                {
                    m_VerticalScrollPositions[group] = GetScrollViewerVerticalPosition(svc);
                    m_VerticalScrollLengths[group] = GetScrollViewerVerticalLength(svc);
                }
            }

            if (sbc != null)
            {
                if (sbc.Orientation == Orientation.Horizontal)
                {
                    m_HorizontalScrollPositions[group] = GetScrollBarPosition(sbc);
                    m_HorizontalScrollLengths[group] = GetScrollBarLength(sbc);
                }
                else
                {
                    m_VerticalScrollPositions[group] = GetScrollBarPosition(sbc);
                    m_VerticalScrollLengths[group] = GetScrollBarLength(sbc);
                }
            }

            // Modify each scroller in the group
            foreach (Object obj in m_GroupScrollers[group])
            {
                ScrollViewer sv = obj as ScrollViewer;
                ScrollBar sb = obj as ScrollBar;

                // Skip changing myself
                if ((sv == nChangeScroller) || (sb == nChangeScroller))
                    continue;

                // Modify an existing scrollviewer
                if (sv != null)
                {
                    // Modify from a scrollviewer (scrollviewer -> scrollviewer)
                    if (svc != null)
                    {
                        // Modify horizontal
                        if ((sv.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled) && nHorzChange
                            && (svc.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled))
                            SetScrollViewerHorizontalPosition(sv, GetScrollViewerHorizontalPosition(svc));

                        // Modify vertical
                        if ((sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled) && nVertChange
                            && (svc.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled))
                            SetScrollViewerVerticalPosition(sv, GetScrollViewerVerticalPosition(svc));
                    }

                    // Modify from a scrollbar (scrollbar -> scrollviewer)
                    if (sbc != null)
                    {
                        // Modify horizontal
                        if ((sv.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled)
                            && (sbc.Orientation == Orientation.Horizontal))
                            SetScrollViewerHorizontalPosition(sv, GetScrollBarPosition(sbc));

                        // Modify vertical
                        if ((sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
                            && (sbc.Orientation == Orientation.Vertical))
                            SetScrollViewerVerticalPosition(sv, GetScrollBarPosition(sbc));
                    }
                }

                // Modify an existing scrollbar
                if (sb != null)
                {
                    // Modify from a scrollviewer (scrollviewer -> scrollbar)
                    if (svc != null)
                    {
                        // Modify horizontal
                        if ((sb.Orientation == Orientation.Horizontal) 
                            && (svc.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled))
                        {
                            SetScrollBarPosition(sb, GetScrollViewerHorizontalPosition(svc));
                            SetScrollBarLength(sb, GetScrollViewerHorizontalLength(svc));
                        }

                        // Modify vertical
                        if ((sb.Orientation == Orientation.Vertical) 
                            && (svc.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled))
                        {
                            SetScrollBarPosition(sb, GetScrollViewerVerticalPosition(svc));
                            SetScrollBarLength(sb, GetScrollViewerVerticalLength(svc));
                        }
                    }

                    // Modify from a scrollbar (scrollbar -> scrollbar)
                    if (sbc != null)
                    {
                        // Modify same orientation
                        if (sb.Orientation == sbc.Orientation)
                        {
                            SetScrollBarPosition(sb, GetScrollBarPosition(sbc));
                            SetScrollBarLength(sb, GetScrollBarLength(sbc));
                        }
                    }
                }
            }
        }

        static Double GetScrollViewerHorizontalPosition(ScrollViewer nScrollViewer)
        {
            if (nScrollViewer.ViewportWidth >= nScrollViewer.ExtentWidth)
                return 0;

            return nScrollViewer.HorizontalOffset / nScrollViewer.ScrollableWidth;
        }
        static Double GetScrollViewerHorizontalLength(ScrollViewer nScrollViewer)
        {
            if (nScrollViewer.ViewportWidth >= nScrollViewer.ExtentWidth)
                return 1;

            if (nScrollViewer.ExtentWidth <= 0)
                return 0;

            return nScrollViewer.ViewportWidth / nScrollViewer.ExtentWidth;
        }
        static Double GetScrollViewerVerticalPosition(ScrollViewer nScrollViewer)
        {
            if (nScrollViewer.ViewportHeight >= nScrollViewer.ExtentHeight)
                return 0;

            return nScrollViewer.VerticalOffset / nScrollViewer.ScrollableHeight;
        }
        static Double GetScrollViewerVerticalLength(ScrollViewer nScrollViewer)
        {
            if (nScrollViewer.ViewportHeight >= nScrollViewer.ExtentHeight)
                return 1;

            if (nScrollViewer.ExtentHeight <= 0)
                return 0;

            return nScrollViewer.ViewportHeight / nScrollViewer.ExtentHeight;
        }
        static Double GetScrollBarPosition(ScrollBar nScrollBar)
        {
            Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;

            return (nScrollBar.Value - nScrollBar.Minimum) / tracklen;
        }
        static Double GetScrollBarLength(ScrollBar nScrollBar)
        {
            Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;

            return nScrollBar.ViewportSize / (tracklen + nScrollBar.ViewportSize);
        }
        static void SetScrollViewerHorizontalPosition(ScrollViewer nScrollViewer, Double nPosition)
        {
            nScrollViewer.ScrollToHorizontalOffset(nPosition * nScrollViewer.ScrollableWidth);
        }
        static void SetScrollViewerVerticalPosition(ScrollViewer nScrollViewer, Double nPosition)
        {
            nScrollViewer.ScrollToVerticalOffset(nPosition * nScrollViewer.ScrollableHeight);
        }
        static void SetScrollBarPosition(ScrollBar nScrollBar, Double nPosition)
        {
            Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;

            nScrollBar.Value = nPosition * tracklen + nScrollBar.Minimum;
        }
        static void SetScrollBarLength(ScrollBar nScrollBar, Double nLength)
        {
            Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;

            if (nLength < 1)
            {
                nScrollBar.ViewportSize = nLength * tracklen / (1 - nLength);
                nScrollBar.LargeChange = nScrollBar.ViewportSize;
                nScrollBar.IsEnabled = true;
            }
            else
            {
                nScrollBar.ViewportSize = Double.MaxValue;
                nScrollBar.IsEnabled = false;
            }
        }
    }
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here