Silverlight 4 Scrollviewer & Keeping items in view

Just a short note today – mostly so I can look up later what I’ve done to get this working, but also to share the knowledge! 

 

The Problem:

When using Silverlight 4 and a Scrollviewer (not sure if WPF has the same issue, but the fix should be similar), if your tabbing through the form, or have some type of validation summary, which allows clicking the error in the summary to focus the item, the scrollviewer does not inherently bring an item into view.

 

The Solution:

The good news, this is fixed quite easily, using Silverlight 4’s behaviors, which I absolutely love to work with.

 

The Setup:

 

<ScrollViewer Height="300">
        <StackPanel>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
            <Grid Height="Auto">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Label" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,15,0" />
                <TextBox Grid.Column="1" />
            </Grid>
        </StackPanel>
    </ScrollViewer>

This creates a LONG Form – that will scroll if we display it in a page, I know, I probably went overboard on my example.  That’s what happens when you copy and paste a grid over and over, it’s kinda fun.

 

The Code:

Ok, so now we get to where we need to create our behavior.  It’s pretty simple, so I wont go into depth in explaining it.  It’s actually borrowed from an internet example, and modified for my use:  If you created it, let me know and I’ll credit you.

    public class ScrollIntoView : Behavior<ScrollViewer>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.Loaded -= new RoutedEventHandler(AssociatedObject_Loaded);
            var controls = this.AssociatedObject.Descendants();
            foreach (FrameworkElement control in controls)
            {
                control.GotFocus -= new RoutedEventHandler(control_GotFocus);
            }
        }

        private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            var controls = this.AssociatedObject.Descendants();
            foreach (FrameworkElement control in controls)
            {
                control.GotFocus += new RoutedEventHandler(control_GotFocus);
            }
        }

        private void control_GotFocus(object sender, RoutedEventArgs e)
        {
            FrameworkElement element = e.OriginalSource as FrameworkElement;
            ScrollViewer sv = this.AssociatedObject;
            GeneralTransform gt = element.TransformToVisual(sv);
            Point offset = gt.Transform(new Point(0, 0));
            Double controlTop = offset.Y;

            if (controlTop < 0 || controlTop + element.ActualHeight > sv.ViewportHeight)
            {
                double newOffset = controlTop + sv.VerticalOffset;
                if (newOffset > (sv.ViewportHeight / 2))
                {
                    newOffset -= (sv.ViewportHeight / 2);
                }
                else
                {
                    newOffset = 0;
                }

                if (sv.VerticalOffset != newOffset)
                {
                    sv.ScrollToVerticalOffset(newOffset);
                }
            }
        }
    }

I should note, that this code requires the LinqToVisualTree library found and explained @ http://www.scottlogic.co.uk/blog/colin/2010/03/linq-to-visual-tree/

If you don’t use the LinqToVisualTree library in your projects, you’re making your life very difficult.

 

Now that we’ve got our behavior written, which allows us to wire it up to a scrollviewer (which is nice!  We don’t have to attach it to every single element inside the scrollviewer), we can simply add it to the scrollviewer as such:

 

Declare the namespace in the page / usercontrol where you intent to use it:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 

Add the behavior to the scrollviewer in question:

<i:Interaction.Behaviors>
         <IQ_Client_Helpers:ScrollIntoView/>
 </i:Interaction.Behaviors>

Good luck!  Have fun with this one. Smile

 

Bryan

1 Comment

  • Not bad Josh, the implementations are very close. What I didnt like about your approach, was the requirment to attach the behavior (or call the code) on every element inside the scrollviewer, so I created a design pattern that allowed my designer to attach it directly to the scrollviewer, and it will work for every single element inside it.

    Thanks for the post!

Comments have been disabled for this content.