views:

5352

answers:

5

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.

+6  A: 

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.

Kiril
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.
Craig Shearer
A: 

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.

Craig Shearer
A: 

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
        }
Brian
A: 

Refer to the below blog post for a sample project containing the solution for handling scroll event for a scroll viewer. http://dotplusnet.blogspot.com/2010/05/scrollviewer-scroll-change-event-in.html

KayKay
+1  A: 

I'm impressed. I spent some hours to make it work - scrolling canvas is a bit frustraiting until I read your post KayKay. Great job!

Michal