Click here to Skip to main content
15,893,337 members
Articles / Desktop Programming / WPF
Tip/Trick

Scroll Synchronization

Rate me:
Please Sign up or sign in to vote.
4.57/5 (7 votes)
28 Dec 2013CPOL2 min read 30.4K   236   16   7
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...

XML
<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!!!

XML
<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...

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Unknown
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionwhere is the source code Pin
fredatcodeproject27-Dec-13 11:28
professionalfredatcodeproject27-Dec-13 11:28 
AnswerRe: where is the source code Pin
immortalus27-Dec-13 12:28
immortalus27-Dec-13 12:28 
GeneralRe: where is the source code Pin
fredatcodeproject27-Dec-13 13:33
professionalfredatcodeproject27-Dec-13 13:33 
GeneralMy vote of 5 Pin
fredatcodeproject9-Sep-12 2:43
professionalfredatcodeproject9-Sep-12 2:43 
QuestionAlternative button used? Pin
Wendelius6-Sep-12 7:13
mentorWendelius6-Sep-12 7:13 
SuggestionNot an article Pin
Shahin Khorshidnia25-May-12 22:34
professionalShahin Khorshidnia25-May-12 22:34 
GeneralRe: Not an article Pin
immortalus10-Jun-12 21:59
immortalus10-Jun-12 21:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.