0
votes

enter code hereI have a ScrollViewer in Silverlight that is not scrolling vertically whenever I call the ScrollToVerticalOffset method from the code behind.

Basically, I have my View (UserControl) that contains the ScrollViewer. I invoke an action from my ViewModel that triggers an event in the View's code-behind that sets the VerticalOffset to a specific value.

First of all, I know this is very ugly. Ideally I wish that I could have an attachable property that I could bind to a property in my ViewModel, that, when set, would cause the VerticalOffset property (which I know is read-only) to be updated, and the ScrollViewer to scroll.

The ScrollViewer contains dynamic content. So, if the user is viewing content in the ScrollViewer, and scrolls half-way down, and then clicks on a button, new content is loaded into the ScrollViewer. The problem is that the ScrollViewer's vertical offset doesn't get reset, so the user has to scroll up to read the content. So, my solution was to be able to control the vertical offset from the ViewModel, and I have racked my brain and can't come up with a viable solution, so I am looking for someone to help, please.

By the way - I have included code from a class I put together for an attachable property. This property binds to a property in my ViewModel. When I set the property in the ViewModel, it correctly triggers the PropertyChanged callback method in this class, which then calls the ScrollToVerticalOffset method for the ScrollViewer, but the ScrollViewer still doesn't scroll.

public class ScrollViewerHelper
{
    public static readonly DependencyProperty BindableOffsetProperty =
    DependencyProperty.RegisterAttached("BindableOffset", typeof(double), typeof(ScrollViewerHelper),
    new PropertyMetadata(OnBindableOffsetChanged));

    public static double GetBindableOffset(DependencyObject d)
    {
        return (double)d.GetValue(BindableOffsetProperty);
    }

    public static void SetBindableOffset(DependencyObject d, double value)
    {
        d.SetValue(BindableOffsetProperty, value);
    }

    private static void OnBindableOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ScrollViewer scrollViewer = d as ScrollViewer;

        if (scrollViewer != null)
        {
            scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
        }
    }
}
1
I don't see why this solution would not work... are you just trying to pin the viewport to the top or bottom of the panel of elements anytime something new is added? Are you sure OnBindableOffsetChanged is getting called every time you add a new element? If you are just setting the view model property to 0.0 every time (to scroll to the top) I could see why it might only get called the first time and never again.Dan Auclair

1 Answers

0
votes

This approach is a little bit funky in my opinion, as I think of both a ScrollViewer and a VerticalScrollOffset as "View" entities that should have very little (or nothing) to do with a ViewModel. It seems like this might be forcing MVVM a little too much, and creating a lot of extra work in creating an attached dependency property and basically trying to keep a bound Offset ViewModel property in sync with the readonly VerticalScrollOffset of the ScrollViewer.

I am not exactly sure of what you are trying to achieve, but it sounds like you are trying to scroll to a specified offset when some dynamic element is added to the underlying panel of your ScrollViewer. Personally, I would just want to handle this behavior with a little bit of code in my View and forget about tying it to the ViewModel.

One really nice way to do this type of thing in Silverlight 3 is with Blend behaviors. You write a little bit of behavior code in C# and then can attach it declaratively to an element in XAML. This keeps it reusable and out of your code-behind. Your project will have to reference the System.Windows.Interactivity DLL which is part of the Blend SKD.

Here's a simple example of a simple Blend behavior you could add to a ScrollViewer which scrolls to a specified offset whenever the size of the underlying content of the ScrollViewer changes:

public class ScrollToOffsetBehavior : Behavior<ScrollViewer>
{
    private FrameworkElement contentElement = null;

    public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register(
        "Offset",
        typeof(double),
        typeof(ScrollToOffsetBehavior),
        new PropertyMetadata(0.0));

    public double Offset
    {
        get { return (double)GetValue(OffsetProperty); }
        set { SetValue(OffsetProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();

        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (this.contentElement != null)
        {
            this.contentElement.SizeChanged -= contentElement_SizeChanged;
        }

        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
        }
    }

    void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        this.contentElement = this.AssociatedObject.Content as FrameworkElement;

        if (this.contentElement != null)
        {
            this.contentElement.SizeChanged += new SizeChangedEventHandler(contentElement_SizeChanged);
        }
    }

    void contentElement_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        this.AssociatedObject.ScrollToVerticalOffset(this.Offset);
    }
}

You could then apply this behavior to the ScrollViewer in XAML (and specify an offset of 0 to scroll back to the top):

   <ScrollViewer>
        <i:Interaction.Behaviors>
            <local:ScrollToOffsetBehavior Offset="0"/>
        </i:Interaction.Behaviors>
        ...Scroll Viewer Content...
    </ScrollViewer>

This would be assuming that you always want to scroll to the offset whenever the content size changes. This may not be exactly what you are looking for, but it is one example of how something like this can be done in the view using a behavior.