Search code examples
c#xamluwpscrollviewer

UWP ScrollViewer unexpected behavior


When developing an app for the UWP platform I came across a case of unexpected behavior. When I had too much content to present on one page I took advantage of the ScrollViewer control, which should fix this problem.

The control allows the scrolling of the content, but the way it handles input is unnatural. When the control gets Clicked, so that no focusable control within the ScrollViewer is clicked, the Focus is passed to the first focusable control in it's content. This effectively means that the ScrollViewer scrolls back to the top, which is not the behavior that I would not expect.

I'd like the ScrollViewer retain it's current position, just as a webpage does. A webpage does not scroll back to the first focusable control if one were to click on an empty place. Another similar example is that a Grid or Stackpanel does not pass the focus to one of it's children when clicked.

Below is some xaml code that demonstrates said behavior, just create a new UWP project and place this inside the grid of the generated MainPage.xaml:

<ScrollViewer Height="400"> <!-- Unexpected and unnatrual behavior -->
    <StackPanel Width="250" Background="DarkGray" Padding="10"> <!-- With Content Height > ScrollViewer Height -->
        <TextBlock Text="Unnatural scroll behavior" FontWeight="Bold"/>
        <TextBox Text="Some focusable control"/>
        <TextBlock Height="800" Text="On focus => pass focus to first available => scroll to the top" TextWrapping="Wrap"/>
        <TextBox Text="Some focusable control"/>
    </StackPanel>
</ScrollViewer>

I did come up with a solution, however it seems quite weird that this would be the correct way to solve the current problem:

<ScrollViewer Height="400">
    <!-- Corrected behaviour, but extra ContentControl. Easily forgotten -->
    <ContentControl>
        <StackPanel Width="250" Background="Gray" Padding="10">
            <!-- With Content Height > ScrollViewer Height -->
            <TextBlock Text="Corrected scroll behavior" FontWeight="Bold"/>
            <TextBox Text="Some focusable control"/>
            <TextBlock Height="800" Text="On focus => no scroll" TextWrapping="Wrap"/>
            <TextBox Text="Some focusable control"/>
        </StackPanel>
    </ContentControl>
</ScrollViewer>

This works, however it's quite annoying to always remember to add the ContentControl, which is why I will post an answer that implements my solution in a better way.

The question remains: Is there a better way to solve this?


Solution

  • My solution implemented in a more developer friendly way by creating a custom UserControl that handles the ContentControl.

    Scroller.xaml:

    <UserControl
        x:Class="Controls.Scroller"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        d:DesignHeight="300"
        d:DesignWidth="400">
    
        <ScrollViewer Name="ScrollerControl" Height="{x:Bind Height, Mode=TwoWay}" Width="{x:Bind Width, Mode=TwoWay}">
            <ContentControl Content="{x:Bind ScrollContent}"/>
        </ScrollViewer>
    
    </UserControl>
    

    Scroller.xaml.cs:

    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Markup;
    
    namespace Controls
    {
        [ContentProperty(Name = "ScrollContent")]
        public sealed partial class Scroller : UserControl
        {
            public static readonly DependencyProperty ScrollContentProperty = DependencyProperty.Register("ScrollContent", typeof(object), typeof(object), new PropertyMetadata(new Grid()));
    
            public ScrollViewer ScrollViewer { get { return ScrollerControl; } }
            public object ScrollContent { get { return GetValue(ScrollContentProperty); } set { SetValue(ScrollContentProperty, value); } }
    
            public Scroller()
            {
                DataContext = this;
                this.InitializeComponent();
            }
        }
    }
    

    Usage:

    First add the following line to a Page's xaml:

    xmlns:cont="using:Controls"
    

    Then use the control in the following way:

    <cont:Scroller Height="400" x:Name="ScrollControl">
        <!-- Place any content in here -->
        <!-- The content below is just for example -->
        <StackPanel Width="250" Background="DarkGray" Padding="10">
            <TextBlock Text="Corrected scroll behaviour" FontWeight="Bold"/>
            <TextBox Text="Some focusable control"/>
            <TextBlock Height="800" Text="On focus => no scroll" TextWrapping="Wrap"/>
            <TextBox Text="Some focusable control"/>
        </StackPanel>
    </cont:Scroller>
    

    Note that x:Name must be used to refer to the control in code. Also if you want to access the ScrollViewer, then use

    ScrollControl.ScrollViewer