Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

Exposing and Binding to a Silverlight ScrollViewer’s Scrollbars

, 22 Jul 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
An attached behaviour that exposes the ScrollViewer's horizontal / vertical offset as dependency properties that permit binding

The Silverlight ScrollViewer exposes readonly properties which indicate the current vertical and horizontal scroll offset, and methods for setting the current offset. In this blog post, I demonstrate a simple attached behaviour that exposes these offsets as read / write dependency properties allowing them to be bound to.

The Silverlight ScrollViewer is a very useful control, if you have some content that is larger than the space available in your application, just sit it inside a ScrollViewer and it will automatically add vertical or horizontal scrollbars as required. Simple.

The ScrollViewer exposes readonly properties which indicate the current vertical and horizontal scroll offset. I have used a ScrollViewer on many occasions without finding this to be an issue, however, recently I was creating an MVVM application which contained a list of items within a ScrollViewer. When users click on an item, they are taken to its ‘details’ page, they then have the option to return to the list. I wanted the list to have the same state when the user returned to it, i.e. the same scroll location. Naturally, with this being an MVVM application, the scroll position belongs on the ViewModel and should be relayed to the View via a binding. However, with the offset properties on the ScrollViewer being readonly, this was not possible. Time to get creative!

The template of the ScrollViewer contains two ScrollBar instances, one horizontal and one vertical. The basic idea behind my solution is to add an attached behaviour (i.e. an attached property, that on attachment performs some logic on the target element) which walks the visual tree to find these scrollbars exposing their offset values.

Firstly, we’ll start by defining an attached property called VerticalOffset, which will be used to expose the position of the vertical scrollbar. Dependency property boiler plate code follows:

public static class ScrollViewerBinding
{
  #region VerticalOffset attached property
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// Gets the vertical offset value
  /// <span class="code-SummaryComment"></summary>
</span>  public static double GetVerticalOffset(DependencyObject depObj)
  {
    return (double)depObj.GetValue(VerticalOffsetProperty);
  }
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// Sets the vertical offset value
  /// <span class="code-SummaryComment"></summary>
</span>  public static void SetVerticalOffset(DependencyObject depObj, double value)
  {
    depObj.SetValue(VerticalOffsetProperty, value);
  }
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// VerticalOffset attached property
  /// <span class="code-SummaryComment"></summary>
</span>  public static readonly DependencyProperty VerticalOffsetProperty =
      DependencyProperty.RegisterAttached("VerticalOffset", typeof(double),
      typeof(ScrollViewerBinding), 
	new PropertyMetadata(0.0, OnVerticalOffsetPropertyChanged));
 
  #endregion
}

We’ll also define a private attached property of type ScrollBar, which will be used to store a reference to the scrollbar once it has been located within the ScrollViewers visual tree:

/// <span class="code-SummaryComment"><summary>
</span>/// An attached property which holds a reference to the vertical scrollbar which
/// is extracted from the visual tree of a ScrollViewer
/// <span class="code-SummaryComment"></summary>
</span>private static readonly DependencyProperty VerticalScrollBarProperty =
    DependencyProperty.RegisterAttached("VerticalScrollBar", typeof(ScrollBar),
    typeof(ScrollViewerBinding), new PropertyMetadata(null));

Because this attached property is private, I haven’t bothered to create CLR property accessors which are only used for convenience.

The VerticalOffset attached property has a changed event handler. When the property is first attached to a ScrollViewer, this allows us to walk the visual tree constructed from the ScrollViewer’s template and extract the scrollbars. It is this extra logic that adds new functionality to the ScrollViewer which is what makes this an attached behaviour, as opposed to a regular attached property which merely holds state.

When an attached property is first associated with its target, the visual tree described by the target’s template has not yet been constructed. With WPF, you would typically register for the Loaded event on the target, and on handling this event walk the visual tree that will have been constructed. However, in Silverlight there is a very subtle difference, apparently the Loaded event is not guaranteed to occur after the template is applied. Oh dear. The only other alternative is to handle the LayoutUpdated which is fired whenever changes are made to the visual tree. However, there is another complication! The LayoutUpdated event always has a null sender, this might look like a bug, but it is actually by design. This means that if we handle the LayoutUpdated event on the ScrollViewer element which the property is attached to, the method invoked as the event handler no longer has a reference to the ScrollViewer!

In order to get around this problem, the attached property changed handler is as follows:

/// <span class="code-SummaryComment"><summary>
</span>/// Invoked when the VerticalOffset attached property changes
/// <span class="code-SummaryComment"></summary>
</span>private static void OnVerticalOffsetPropertyChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
  ScrollViewer sv = d as ScrollViewer;
  if (sv != null)
  {
    // check whether we have a reference to the vertical scrollbar
    if (sv.GetValue(VerticalScrollBarProperty) == null)
    {
      // if not, handle LayoutUpdated, which will be invoked after the
      // template is applied and extract the scrollbar
      sv.LayoutUpdated += (s, ev) =>
        {
          if (sv.GetValue(VerticalScrollBarProperty) == null)
          {
            GetScrollBarsForScrollViewer(sv);
          }
        };
    }
    else
    {
      // update the scrollviewer offset
      sv.ScrollToVerticalOffset((double)e.NewValue);
    }
  }
}

Firstly, we check whether we have already got a reference to the scrollbar, if not, we attach a LayoutUpdated event handler. But instead of defining this handler as a method, it is written as an lambda expression (i.e. an anonymous delegate), so that the ScrollViewer reference is captured using closure.

When the LayoutUpdated event is fired, the following method is invoked:

/// <span class="code-SummaryComment"><summary>
</span>/// Attempts to extract the scrollbars that are within the scrollviewers
/// visual tree. When extracted, event handlers are added to their ValueChanged events.
/// <span class="code-SummaryComment"></summary>
</span>private static void GetScrollBarsForScrollViewer(ScrollViewer scrollViewer)
{
  ScrollBar scroll = GetScrollBar(scrollViewer, Orientation.Vertical);
  if (scroll != null)
  {
    // save a reference to this scrollbar on the attached property
    scrollViewer.SetValue(VerticalScrollBarProperty, scroll);
 
    // scroll the scrollviewer
    scrollViewer.ScrollToVerticalOffset(
      ScrollViewerBinding.GetVerticalOffset(scrollViewer));
 
 
    // handle the changed event to update the exposed VerticalOffset
    scroll.ValueChanged += (s, e) =>
      {
        SetVerticalOffset(scrollViewer, e.NewValue);
      };
  }
}
 
/// <span class="code-SummaryComment"><summary>
</span>/// Searches the descendants of the given element, looking for a scrollbar
/// with the given orientation.
/// <span class="code-SummaryComment"></summary>
</span>private static ScrollBar GetScrollBar(FrameworkElement fe, Orientation orientation)
{
  return fe.Descendants()
            .OfType<ScrollBar>()
            .Where(s => s.Orientation == orientation)
            .SingleOrDefault(); 
}

The GetScrollBar method is invoked, which then uses Linq-to-VisualTree to walk the descendants of the ScrollViewer in an attempt to find scrollbars of a given orientation. If one is found, it is stored in the private attached property. We also subscribe to the events which the scrollbar raises when its value changes, in order to update the VerticalOffsetProperty. Again, a lambda expression is used to capture the ScrollViewer which this scrollbar is associated with, so that we do not have to store a relationship from scrollbar to scrollviewer.

As an aside, it would have been a bit more elegant to use binding to connect our VerticalScrollOffset to the scrollbar value as follows:

scroll.SetBinding(ScrollBar.ValueProperty,
  new Binding("(ScrollViewerBinding.VerticalOffset)")
  {
    Source = scrollViewer
  });

However, it appears that there is a bug in Silverlight which makes binding to custom attached properties in code behind fail with a critical error. Until this is fixed, we have to take care of the ‘binding’, by handling change events from both properties, manually.

Using this attached behaviour is as simple as the following:

<ScrollViewer 
    local:ScrollViewerBinding.VerticalOffset="{Binding YPosition, Mode=TwoWay}"
    local:ScrollViewerBinding.HorizontalOffset="{Binding XPosition, Mode=TwoWay}">
    <!--<span class="code-comment"> Big content goes here! --></span>
</ScrollViewer>

The following little demo binds the X & Y offset of the scrollviewer to two CLR properties (with property changed notifications) defined in code behind. These same properties are bound to the text fields below: 

[See this code in action on my blog.]

You can download the complete source code for this blog post here.

Regards, Colin E.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Colin Eberhardt
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.
 
I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.
 
I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.
 
Visit my blog - Colin Eberhardt's Adventures in .NET.
 
Follow me on Twitter - @ColinEberhardt
 
-
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralNot having any luck with the binding Pinmemberkfrosty30-Aug-10 7:01 
GeneralAnimations PinmemberRespaldoMatias2-Aug-10 11:45 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.141015.1 | Last Updated 22 Jul 2010
Article Copyright 2010 by Colin Eberhardt
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid