Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to animate vertical centering of a Silverlight 4 Toolkit Expander within a ScrollViewer

0.00/5 (No votes)
4 Jun 2010 1  
Cannot use Storyboard because ScrollView.VerticalOffset property is read-only. Must use old fashioned DispatchTimer.
I have extensive product documentation in my Silverlight application. I have a product overview, ten tutorials, plus an "Easter egg" that gives background on how such a product ever came to be developed. Each document is initially presented as a list of Expander elements within a ScrollViewer. The average list has about 8 Expander elements and each Expander expands and collapses on average 5 or 6 paragraphs each of which averages about 8 lines of wrapping text.

I discovered that when the Expander expanded to a vertical size larger than the ScrollViewer viewport, sometimes the Expander header flowed off the top of the viewport and then the user could no longer read the topic covered by the Expander. So I wrote code to center the Expander header within the viewport. But I found that the sudden movement of the header was disconcerting, and so I decided to animate the vertical scroll.

I quickly discovered that the ScrollViewer.VerticalOffset is for some inexplicable reason read-only. (If anyone knows why, please tell me. Googling for the answer just seems to produce sheepish acceptance of what Microsoft has done without questioning this strange decision.) Because VerticalOffset is read-only, the animation Storyboard cannot be used to animate the scroll. Silverlight 4 has also sealed the ScrollViewer class so you can't write extensions to define new Dependency Properties! Microsoft seems to be bending over backwards to thwart scrolling animation within a ScrollViewer! There must be something wrong with it that they don't know how to fix. The only way to programmatically change the vertical offset within a ScrollViewer is to use its ScrollToVerticalOffset() public member.

I really thought this animation was necessary on my Website, so I decided to write a class that would perform the animation for me using a DispatcherTimer, the old-fashioned way of developing animations! Based on the number of lines of code I found it necessary to do this, using this brute force technique is about 6X as hard as it would be if VerticalOffset were a read-write Dependency Property.

My class works fairly well. To make it scroll smoothly, I had to increment my scroll 120 times per second, rather than the 60 that WPF/Silverlight uses by default. Incrementing my scroll merely 60 times per second produced an inexplicable and unacceptable jerkiness. There is some anomalous behavior expanding Expander elements near the end of the ScrollViewer, but it is still easy to see what is going on.

I wanted to make the class reasonably general purpose and so I abstracted the Expander being expanded/collapsed as a "vertical offset to be centered" and then named the class, CenterVerticalOffset. It takes a ScrollViewer reference and an animation duration in seconds as parameters. CenterVerticalOffset is created at the end of the Loaded event in the class instance holding the ScrollViewer.

After the CenterVerticalOffset class instance is created, a class member called Center() is called when the size of the Expander changes (because you've clicked on it). Center() is thus called within the Expander.SizeChanged event. Center() takes the vertical offset that you want to center in the viewport. For my application, the vertical offset is the Y coordinate of the mouse pointer when the user clicks on the Expander. I obtain this value from a MouseMove event from the ScrollViewer.

You can watch the animation in action by bringing up my Website: http://powerphototools.com. Bring up any tutorial or the product overview to watch it in action. Here is the source for my CenterVerticalOffset class:
MSIL
public class CenterVerticalOffset
{
    DispatcherTimer Timer { get; set; }
    double NewVerticalOffset { get; set; }
    double VerticalOffsetIncrement { get; set; }
    double CurrentVerticalOffset { get; set; }
    ScrollViewer ScrollViewer { get; set; }
    double ViewportHeight { get; set; }
    double VerticalOffset { get; set; }
    double Intervals { get; set; }

    // This class animates the vertical scrolling of an offset within
    // the viewport to the vertical center of the viewport. durationInSeconds
    // is the length of time in seconds that the animation takes place.
    public CenterVerticalOffset(ScrollViewer scrollViewer, double durationInSeconds)
    {
        ScrollViewer = scrollViewer;
        Intervals = durationInSeconds * 120; // number of timer Intervals
        Timer = new DispatcherTimer();
        Timer.Interval = TimeSpan.FromSeconds(1.0 / 120.0);
        Timer.Tick += new EventHandler(Timer_Tick);
    }

    // verticalOffsetToBeCentered is a vertical pixel offset from the top
    // of the viewport. It needs to be centered within the viewport.
    public void Center(double verticalOffsetToBeCentered)
    {
        // The user can change this between expansions/collapses.
        ViewportHeight = ScrollViewer.ViewportHeight;

        // The user can change this by moving the thumb control.
        // Equivalent to the data type Animation.From property.
        VerticalOffset = ScrollViewer.VerticalOffset;

        // Equivalent to the data type Animation.To property.
        NewVerticalOffset =
            VerticalOffset - ViewportHeight / 2 + verticalOffsetToBeCentered;

        // We don't want to try to scroll the verticalOffsetToBeCentered out of
        // the viewport.If we're at one of the limits, we just do the best we can.
        if (NewVerticalOffset < 0)
        {
            NewVerticalOffset = 0;
        }
        VerticalOffsetIncrement = (NewVerticalOffset - VerticalOffset) / Intervals;
        if (VerticalOffsetIncrement == 0.0)
        {
            return;
        }
        CurrentVerticalOffset = VerticalOffset;
        Timer.Start();
    }

    void Timer_Tick(object sender, EventArgs e)
    {
        double extentHeight = ScrollViewer.ExtentHeight;
        CurrentVerticalOffset += VerticalOffsetIncrement;
        if (VerticalOffsetIncrement > 0 && CurrentVerticalOffset > NewVerticalOffset ||
            VerticalOffsetIncrement < 0 && NewVerticalOffset > CurrentVerticalOffset)
        {
            Timer.Stop();
        }
        else
        {
            ScrollViewer.ScrollToVerticalOffset(CurrentVerticalOffset);
        }
    }
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here