How can I make the Silverlight ScrollViewer scroll to show a child control with focus?

27,848

Solution 1

I tested this using Silverlight 3. I am not sure about SL2.

This is my XAML:

<ScrollViewer Height="200" Width="200" KeyUp="ScrollViewer_KeyUp">
    <StackPanel>
        <Button Content="1" Height="20" />
        <Button Content="2" Height="20" />
        <Button Content="3" Height="20" />
        <Button Content="4" Height="20" />
        <Button Content="5" Height="20" />
        <Button Content="6" Height="20" />
        <Button Content="7" Height="20" />
        <Button Content="8" Height="20" />
        <Button Content="9" Height="20" />
    <Button Content="10" Height="20" />
        <Button Content="11" Height="20" />
        <Button Content="12" Height="20" />
        <Button Content="13" Height="20" />
        <Button Content="14" Height="20" />
        <Button Content="15" Height="20" />
        <Button Content="16" Height="20" />
        <Button Content="17" Height="20" />
        <Button Content="18" Height="20" />
        <Button Content="19" Height="20" />
        <Button Content="20" Height="20" />
    </StackPanel>
</ScrollViewer>

And this is the code-behind:

private void ScrollViewer_KeyUp(object sender, KeyEventArgs e)
{
    ScrollViewer scrollViewer = sender as ScrollViewer;
    FrameworkElement focusedElement = FocusManager.GetFocusedElement() as FrameworkElement;
    GeneralTransform focusedVisualTransform = focusedElement.TransformToVisual(scrollViewer);
    Rect rectangle = focusedVisualTransform.TransformBounds(new Rect(new Point(focusedElement.Margin.Left, focusedElement.Margin.Top), focusedElement.RenderSize));
    double newOffset = scrollViewer.VerticalOffset + (rectangle.Bottom - scrollViewer.ViewportHeight);
    scrollViewer.ScrollToVerticalOffset(newOffset);
}

What I did was to click on Button #1 and tab until I get to Button #20. It worked for me. Give it a try and let me know how it works for you.

Solution 2

The silverlight toolkit contains a method "ScrollIntoView".

Add a reference to System.Windows.Controls.Toolkit.dll ans you should be able to use the code below.

scrollViewer.ScrollIntoView(control);

Solution 3

Just a slight enhancement. Still need to do this for Silverlight 4 by the way. Instead of GotFocus for each control you can handle the GotFocus of the scrollviewer itself and implement it just once.

 private void _ScrollViewer_GotFocus(object sender, RoutedEventArgs e)
        {
            FrameworkElement element = e.OriginalSource as FrameworkElement;

            if (element != null)
            {
                ScrollViewer scrollViewer = sender as ScrollViewer;
                scrollViewer.ScrollToVerticalOffset(GetVerticalOffset(element, scrollViewer));
            }

        }

        private double GetVerticalOffset(FrameworkElement child, ScrollViewer scrollViewer)
        {
            // Ensure the control is scrolled into view in the ScrollViewer. 
            GeneralTransform focusedVisualTransform = child.TransformToVisual(scrollViewer);
            Point topLeft = focusedVisualTransform.Transform(new Point(child.Margin.Left, child.Margin.Top));
            Rect rectangle = new Rect(topLeft, child.RenderSize);
            double newOffset = scrollViewer.VerticalOffset + (rectangle.Bottom - scrollViewer.ViewportHeight);
            return newOffset < 0 ? 0 : newOffset; // no use returning negative offset
        }

Solution 4

I got this to work, with help of Kiril's answer above. The general context of this is that I have user-defineable forms in my application, and this code is used for rendering the controls on a form.

My general strategy was to add my controls to a Grid, then find all the children of the ScrollViewer using VisualTreeHelper, and add a GotFocus event handler to each control.

When the control gets focus, again using VisualTreeHelper, I search up the visual tree to find the control whose parent is the Grid that is being scrolled by the ScrollViewer. Then I scroll the ScrollViewer to make the control visible.

Here's the code (gridRender is the Grid that the controls are added to):

private void AfterFormRendered()
{
    var controls = VisualTreeHelperUtil.FindChildren<Control>(gridRender);
    foreach (var ctrl in controls)
    {
        ctrl.GotFocus += CtrlGotFocus;
    }
}

private void CtrlGotFocus(object sender, RoutedEventArgs e)
{
    var ctrl = sender as Control;
    var gridChildControl = VisualTreeHelperUtil.FindParentWithParent(ctrl, gridRender) as FrameworkElement;

    if (gridChildControl != null)
    {
        // Ensure the control is scrolled into view in the ScrollViewer.
        GeneralTransform focusedVisualTransform = gridChildControl.TransformToVisual(scrollViewer);
        Point topLeft = focusedVisualTransform.Transform(new Point(gridChildControl.Margin.Left, gridChildControl.Margin.Top));
        Rect rectangle = new Rect(topLeft, gridChildControl.RenderSize);
        double newOffset = scrollViewer.VerticalOffset + (rectangle.Bottom - scrollViewer.ViewportHeight);    

        scrollViewer.ScrollToVerticalOffset(newOffset);
    }
}

Note: the VisualTreeHelperUtil class is my own class that adds some useful searching functionality to the VisualTreeHelper class.

Share:
27,848

Related videos on Youtube

Tushee
Author by

Tushee

Full-stack Software Architect and Developer in C#, .NET, Javascript, Typescript, AngularJS, Angular .NET, JADE, related web technologies. SOreadytohelp

Updated on July 09, 2022

Comments

  • Tushee
    Tushee almost 2 years

    I have a ScrollViewer which contains a Grid with multiple controls in it. The user can tab through the controls, but eventually they tab to a control that isn't in view - so they have to manully scroll to make the control visible again.

    Is there any way to make the ScrollViewer scroll automatically so that the focussed control is always visible. Failing that, is there any way I can make this work, short of listening to a GotFocus event on every control and then scrolling the ScrollViewer to make the control visible?

    At present I'm using Silverlight 2.

  • Tushee
    Tushee almost 15 years
    Thanks - with a little modification, which I'll post as an answer, it worked very nicely. The TransformBounds method on GeneralTransform seems to be a SL3 thing though.
  • Jeremy
    Jeremy over 10 years
    I know this is an old post, but I'm trying to do the same thing. I'm using this code but the Margin.Left and Margin.Top values are always 0. The control I want to scroll to is inside a grid in the ScrollViewer, and the control is positioned in the grid by giving it a row and rowspan property value.