9
votes

I have a View and a ViewModel in Silverlight 3.0.

The view contains a standard ScrollViewer, which contains dynamic content.

Depending on the content within the ScrollViewer, the user could have scrolled half way down the content, and then performed an action that causes the ScrollViewer to load new content, but the ScrollViewer does not automatically scroll to the top.

I want to be able to bind to the VerticalOffset property, but it is read-only. Any ideas on attachable behavior? Any ideas?

Thanks.

4
You want to expose a Property on the ViewModel which indicates where the ScrollViewer should be? Its not clear what you would want to bind the VerticalOffset to?AnthonyWJones

4 Answers

5
votes

The following blog post provides an attached behaviour that exposes the vertical / horizontal offsets of a scrollviewer so that you can bind to them, or set them in code:

http://blog.scottlogic.com/2010/07/21/exposing-and-binding-to-a-silverlight-scrollviewers-scrollbars.html

This allows the following markup:

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

Since you are using a ViewModel I take it the "action that causes ScrollViewer to load new content" is a result of changes made inside or to the ViewModel. That being the case I would add an event to the ViewModel that gets fired every time such a change occurs.

Your View can the add a handler on this event and call ScrollToVerticalPosition on the ScrollViewer when its fired.

3
votes

I've simplified the @ColinE's solution. Instead of hooking to the ScrollBar.ValueChanged event, I hook to the ScrollViewer.ScrollChanged event. So, 1. it is not necessary to find the ScrollBar in the visual tree and 2. ScrollBar.ValueChanged is called in some transition states when the content of the ScrollViewer changes and I do not want to catch these states.

I post my code for the VerticalOffset, the HorizontalOffset is similar:

/// <summary>
/// VerticalOffset attached property
/// </summary>
public static readonly DependencyProperty VerticalOffsetProperty =
    DependencyProperty.RegisterAttached("VerticalOffset", typeof(double),
    typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN,
        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
        OnVerticalOffsetPropertyChanged));

/// <summary>
/// Just a flag that the binding has been applied.
/// </summary>
private static readonly DependencyProperty VerticalScrollBindingProperty =
    DependencyProperty.RegisterAttached("VerticalScrollBinding", typeof(bool?), typeof(ScrollViewerBinding));

public static double GetVerticalOffset(DependencyObject depObj)
{
    return (double)depObj.GetValue(VerticalOffsetProperty);
}

public static void SetVerticalOffset(DependencyObject depObj, double value)
{
    depObj.SetValue(VerticalOffsetProperty, value);
}

private static void OnVerticalOffsetPropertyChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    ScrollViewer scrollViewer = d as ScrollViewer;
    if (scrollViewer == null)
        return;

    BindVerticalOffset(scrollViewer);
    scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}

public static void BindVerticalOffset(ScrollViewer scrollViewer)
{
    if (scrollViewer.GetValue(VerticalScrollBindingProperty) != null)
        return;

    scrollViewer.SetValue(VerticalScrollBindingProperty, true);
    scrollViewer.ScrollChanged += (s, se) =>
    {
        if (se.VerticalChange == 0)
            return;
        SetVerticalOffset(scrollViewer, se.VerticalOffset);
    };
}

And use it in the XAML:

<ScrollViewer local:ScrollViewerBinding.VerticalOffset="{Binding ScrollVertical}">
    <!-- content ... -->
</ScrollViewer>
1
votes

I started with this but noticed there isn't a cleanup phase so here goes the complete implementation with both horizontal and vertical offsets bindable:

using System.Windows;
using System.Windows.Controls;

namespace Test
{
    public static class ScrollPositionBehavior
    {
        public static readonly DependencyProperty HorizontalOffsetProperty =
            DependencyProperty.RegisterAttached(
                "HorizontalOffset",
                typeof(double),
                typeof(ScrollPositionBehavior),
                new FrameworkPropertyMetadata(
                    double.NaN,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnHorizontalOffsetPropertyChanged));

        public static readonly DependencyProperty VerticalOffsetProperty =
            DependencyProperty.RegisterAttached(
                "VerticalOffset",
                typeof(double),
                typeof(ScrollPositionBehavior),
                new FrameworkPropertyMetadata(
                    double.NaN,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnVerticalOffsetPropertyChanged));

        private static readonly DependencyProperty IsScrollPositionBoundProperty =
            DependencyProperty.RegisterAttached("IsScrollPositionBound", typeof(bool?), typeof(ScrollPositionBehavior));

        public static void BindOffset(ScrollViewer scrollViewer)
        {
            if (scrollViewer.GetValue(IsScrollPositionBoundProperty) is true)
                return;

            scrollViewer.SetValue(IsScrollPositionBoundProperty, true);

            scrollViewer.Loaded += ScrollViewer_Loaded;
            scrollViewer.Unloaded += ScrollViewer_Unloaded;
        }

        public static double GetHorizontalOffset(DependencyObject depObj)
        {
            return (double)depObj.GetValue(HorizontalOffsetProperty);
        }

        public static double GetVerticalOffset(DependencyObject depObj)
        {
            return (double)depObj.GetValue(VerticalOffsetProperty);
        }

        public static void SetHorizontalOffset(DependencyObject depObj, double value)
        {
            depObj.SetValue(HorizontalOffsetProperty, value);
        }

        public static void SetVerticalOffset(DependencyObject depObj, double value)
        {
            depObj.SetValue(VerticalOffsetProperty, value);
        }

        private static void OnHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer scrollViewer = d as ScrollViewer;
            if (scrollViewer == null || double.IsNaN((double)e.NewValue))
                return;

            BindOffset(scrollViewer);
            scrollViewer.ScrollToHorizontalOffset((double)e.NewValue);
        }

        private static void OnVerticalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer scrollViewer = d as ScrollViewer;
            if (scrollViewer == null || double.IsNaN((double)e.NewValue))
                return;

            BindOffset(scrollViewer);
            scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
        }

        private static void ScrollChanged(object s, ScrollChangedEventArgs se)
        {
            if (se.VerticalChange != 0)
                SetVerticalOffset(s as ScrollViewer, se.VerticalOffset);

            if (se.HorizontalChange != 0)
                SetHorizontalOffset(s as ScrollViewer, se.HorizontalOffset);
        }

        private static void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
        {
            var scrollViewer = sender as ScrollViewer;
            scrollViewer.ScrollChanged += ScrollChanged;
        }

        private static void ScrollViewer_Unloaded(object sender, RoutedEventArgs e)
        {
            var scrollViewer = sender as ScrollViewer;
            scrollViewer.SetValue(IsScrollPositionBoundProperty, false);

            scrollViewer.ScrollChanged -= ScrollChanged;
            scrollViewer.Loaded -= ScrollViewer_Loaded;
            scrollViewer.Unloaded -= ScrollViewer_Unloaded;
        }
    }
}