Click here to Skip to main content
Click here to Skip to main content

Scroll Synchronization

By , 14 Sep 2012
 

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

About the Author

immortalus
Unknown
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberfredatcodeproject9 Sep '12 - 2:43 
good idea
QuestionAlternative button used?mvpMika Wendelius6 Sep '12 - 7:13 
Did you use the "Add your own alternative version" button in the article Scroll Synchronization[^]?
 
At the moment this isn't showing as an alternative but an independent article.
The need to optimize rises from a bad design.My articles[^]

SuggestionNot an articlememberShahin Khorshidnia25 May '12 - 22:34 
Just code dumping.
If we don't know that we don't know, we'll stay double ignorant for ever.

GeneralRe: Not an articlememberimmortalus10 Jun '12 - 21:59 
(Not an article - subject) - There was no need for it to be... It's alternative code to the original article...
 
If you read the original article and obtain an understanding of how it works then all you need is a code dump to understand how this version works... (as it is only an extension with some modifications...) The core concept is still the same...
 
I wasn't interested in writing a comprehensive point by point explaining the differences but making the code available for others to use and do with as they please.
 
If i get around to it i'll expand the article and generate an example project - time permitting...

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 14 Sep 2012
Article Copyright 2012 by immortalus
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid