Wpf – Stop WPF ScrollViewer automatically scrolling to perceived content

scrollviewerwpfwpf-controls

The Application

I am building an application which includes a range selector. This consists of two custom drawn Slider controls contained within one UserControl derived class. The range selector control is then contained inside a ScrollViewer which has the HorizonalScrollBar visible most of the time.

Sample Application Code: ( appologies for the wall of text )

Window.xaml ( the Window file ):

<Grid>
    <ScrollViewer x:Name="ScrollViewer" HorizontalScrollBarVisibility="Visible"  VerticalScrollBarVisibility="Disabled">
            <local:SliderTest x:Name="slider"                                                                         
                           LowerValue="0"
                           UpperValue="10"
                           Minimum="0"
                           Maximum="100" Width="900" Height="165" Padding="15,0,15,0" HorizontalAlignment="Left">
            </local:SliderTest>
    </ScrollViewer>
</Grid>

SliderTest.xaml:

<UserControl x:Class="scrollviewerDemoProblem.SliderTest"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             x:Name="root"
             xmlns:local="clr-namespace:scrollviewerDemoProblem"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <ControlTemplate x:Key="simpleSlider" TargetType="{x:Type Slider}">
            <Border SnapsToDevicePixels="true" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Track x:Name="PART_Track" Grid.Row="1">
                        <Track.Thumb>
                            <Thumb x:Name="Thumb" FlowDirection="LeftToRight" Width="15">
                                <Thumb.Template>
                                    <ControlTemplate TargetType="Thumb">
                                        <Canvas>
                                            <Path x:Name="test1" StrokeThickness="0" Fill="DarkGreen">
                                                <Path.Data>
                                                    <GeometryGroup FillRule="NonZero">
                                                        <PathGeometry>
                                                            <PathGeometry.Figures>
                                                                <PathFigure IsClosed="True" StartPoint="0,150" IsFilled="True">
                                                                    <PathFigure.Segments>
                                                                        <PathSegmentCollection>
                                                                            <LineSegment Point="-15,150" />
                                                                            <LineSegment Point="-15,0" />
                                                                            <LineSegment Point="0,0" />
                                                                        </PathSegmentCollection>
                                                                    </PathFigure.Segments>
                                                                </PathFigure>
                                                            </PathGeometry.Figures>
                                                        </PathGeometry>
                                                    </GeometryGroup>
                                                </Path.Data>
                                            </Path>
                                        </Canvas>
                                    </ControlTemplate>
                                </Thumb.Template>
                            </Thumb>
                        </Track.Thumb>
                    </Track>
                </Grid>
            </Border>
        </ControlTemplate>

        <ControlTemplate x:Key="simpleSliderRight" TargetType="{x:Type Slider}">
            <Border SnapsToDevicePixels="true" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Track x:Name="PART_Track" Grid.Row="1">
                        <Track.Thumb>
                            <Thumb x:Name="Thumb" HorizontalAlignment="Center" HorizontalContentAlignment="Center" Width="15">
                                <Thumb.Template>
                                    <ControlTemplate TargetType="Thumb">
                                        <Canvas>
                                            <Path Stroke="Black" StrokeThickness="0" Fill="DarkCyan">
                                                <Path.Data>
                                                    <GeometryGroup FillRule="NonZero">
                                                        <PathGeometry>
                                                            <PathGeometry.Figures>
                                                                <PathFigure IsClosed="True" StartPoint="0,150">
                                                                    <PathFigure.Segments>
                                                                        <PathSegmentCollection>
                                                                            <LineSegment Point="15,150" />
                                                                            <LineSegment Point="15,0" />
                                                                            <LineSegment Point="0,0" />
                                                                        </PathSegmentCollection>
                                                                    </PathFigure.Segments>
                                                                </PathFigure>
                                                            </PathGeometry.Figures>
                                                        </PathGeometry>
                                                    </GeometryGroup>
                                                </Path.Data>
                                            </Path>
                                        </Canvas>
                                    </ControlTemplate>
                                </Thumb.Template>
                            </Thumb>
                        </Track.Thumb>
                    </Track>
                </Grid>
            </Border>
        </ControlTemplate>
    </UserControl.Resources>

    <Grid x:Name="Gridd" VerticalAlignment="Top" Height="165" >
        <Border x:Name="timeScaleBorder" Width="auto" Height="15" VerticalAlignment="Top" Background="Black">
            <Canvas x:Name="timeCanvas" Width="auto" Height="15">
            </Canvas>
        </Border>
        <Border x:Name="background" BorderThickness="1,1,1,1" BorderBrush="Black" VerticalAlignment="Center" Height="150"
                Margin="0,15,0,0" Background="White" />
        <Slider  x:Name="LowerSlider"
                Minimum="{Binding ElementName=root, Path=Minimum}"
                Maximum="{Binding ElementName=root, Path=Maximum}"
                Value="{Binding ElementName=root, Path=LowerValue, Mode=TwoWay}"
                Template="{StaticResource simpleSlider}"
                Margin="0,15,0,0" />
        <Slider  x:Name="UpperSlider"
                Minimum="{Binding ElementName=root, Path=Minimum}"
                Maximum="{Binding ElementName=root, Path=Maximum}"
                Value="{Binding ElementName=root, Path=UpperValue, Mode=TwoWay}"
                Template="{StaticResource simpleSliderRight}"
                Margin="0,15,0,0" />
    </Grid>
</UserControl>

SliderText.xaml.cs:

public partial class SliderTest : UserControl
{
    public SliderTest()
    {
        InitializeComponent();
    }

    #region Dependency properties, values etc.

    public static readonly DependencyProperty MinimumProperty =
        DependencyProperty.Register("Minimum", typeof(double), typeof(SliderTest), new UIPropertyMetadata(0d));

    public double LowerValue
    {
        get { return (double)GetValue(LowerValueProperty); }
        set { SetValue(LowerValueProperty, value); }
    }

    public static readonly DependencyProperty LowerValueProperty =
        DependencyProperty.Register("LowerValue", typeof(double), typeof(SliderTest), new UIPropertyMetadata(0d));

    public double UpperValue
    {
        get { return (double)GetValue(UpperValueProperty); }
        set { SetValue(UpperValueProperty, value); }
    }

    public static readonly DependencyProperty UpperValueProperty =
        DependencyProperty.Register("UpperValue", typeof(double), typeof(SliderTest), new UIPropertyMetadata(0d));

    public double Maximum
    {
        get { return (double)GetValue(MaximumProperty); }
        set { SetValue(MaximumProperty, value); }
    }

    public static readonly DependencyProperty MaximumProperty =
        DependencyProperty.Register("Maximum", typeof(double), typeof(SliderTest), new UIPropertyMetadata(1d));

    public double Minimum
    {
        get { return (double)GetValue(MinimumProperty); }
        set { SetValue(MinimumProperty, value); }
    }
    #endregion        
}

The Problem
Most of the sample code provided is boring and the mechanics of it works pretty good. The problem I am having is a visual problem specifically with the ScrollViewer control that I have in the main Window. The ScrollViewer seems to be automatically adjusting the horizontal offset of the ScrollViewer when either of the Slider's gains focus ( from a mouse click for example ).

Reproducing the behaviour

  1. Run the application, you will see that the horizontal scroll bar of the ScrollViewer is visible.
  2. Click on the Green ( far left ) Slider, you will notice that the ScrollViewer automatically adjusts to shift the horizontal offset to where the perceived 'content' starts.

These symptoms occur at either end of the scroll pane.

Screenshot of application when it is run ( Application is Zoomed in 200% for detail clarity ):

Screenshot1

Screenshot of the behavior when the left slider is clicked:

enter image description here

What I want to happen:

When I click on either slider item ( at either end ) when a slider looks to be beyond end of the slider ( slider range is denoted by the black bar at the top ) I don't want the ScrollViewer to automatically adjust it's horizontal offset.

Suspected problem:

I suspect that the problem is that the ScrollViewer perceives the actual 'content' of it's childen starts 15 pixels ( the drawn width of both of my sliders ) in from where the actual drawn content does start. The Canvas only draws because I included a padding of 15 pixels inside of the SliderTest control on the main window, if this padding is removed the ScrollViewer does not show any of the Slider's Canvas.

EDIT : it appears the padding is not the problem, read the comments as to why.

Things I have tried

I have tried looking into overriding the OnPreviewMouseDown event of the main Window. The problem here is that I still want both Slider's to behave normally, setting the event to Handled causes the Slider to stop working completely.

Notes:

The Slider's within the range selector control ( Called SliderTest in this example ) must both have a width of 1 pixel. The slider's must be able to extend 15 pixels past the end of the time selection range ( see the black bar at the top for a reference ).

Thank you for reading this novel-lengthed problem.

Best Answer

By default when a control receives the logical focus, FrameworkElement calls its own BringIntoView method (from within its OnGotFocus method if it has keyboard focus). That results in a RequestBringIntoView event being generated that bubbles up the element tree to allow ancestor elements to bring that portion of the element into view. The ScrollViewer listens for this event and eventually will call MakeVisible on the associated IScrollInfo/ScrollContentPresenter which leaves it up to the panel to bring that portion into view (since the panel would know how it arranges its children). It then takes that returned rect it receives back and asks for that portion of itself to be brought into view (in case you had nested elements that would require some action to ensure the original element was brought into view). So one way to suppress this behavior would be to handle the RequestBringIntoView event on the sliders and mark the event handled.

Related Topic