Question

I've templated a "minimal" ScrollViewer with everything but the arrows removed:

Templated ScrollViewer

I'm looking for a way to hide the arrow for any given direction, when it's not possible to scroll further (so for example the "up" and "left" arrows should be hidden on load).

I thought I'd subclass ScrollViewer or ScrollBar, but both of those classes are (unlike in WPF) sealed. So how would it be possible to add this behavior?

Was it helpful?

Solution

I created a Behavior<ScrollBar> with a dependency property for the scroll amount. I applied the behavior to the ScrollBars inside the ScrollViewer control template, and bound the HorizontalOffset / VerticalOffset properties to the behavior's DPs. Then, I added some visual states to the ScrollBar's control template. The behavior is then responsible for updating the visual state when the scroll offset changes.

This is the behavior (actually need 2 extra dependency properties to complete the calculation: size of the viewport, and extent of the scroll bar):

public enum ScrollBarDirection { Vertical, Horizontal } ;

public class HideScrollButtonsBehavior : Behavior<ScrollBar>
{
    public ScrollBarDirection Direction { get; set; }

    public double VerticalScrollAmount
    {
        get { return (double)GetValue(VerticalScrollAmountProperty); }
        set { SetValue(VerticalScrollAmountProperty, value); }
    }
    public static readonly DependencyProperty VerticalScrollAmountProperty =
        DependencyProperty.Register("VerticalScrollAmount", typeof(double), typeof(HideScrollButtonsBehavior), new PropertyMetadata(ScrollChanged));

    public double ScrollExtent
    {
        get { return (double)GetValue(ScrollExtentProperty); }
        set { SetValue(ScrollExtentProperty, value); }
    }
    public static readonly DependencyProperty ScrollExtentProperty =
        DependencyProperty.Register("ScrollExtent", typeof(double), typeof(HideScrollButtonsBehavior), new PropertyMetadata(null));

    public double ViewportExtent
    {
        get { return (double)GetValue(ViewportExtentProperty); }
        set { SetValue(ViewportExtentProperty, value); }
    }
    public static readonly DependencyProperty ViewportExtentProperty =
        DependencyProperty.Register("ViewportExtent", typeof(double), typeof(HideScrollButtonsBehavior), new PropertyMetadata(null));

    public static void ScrollChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
        if (args.NewValue as double? == null)
            return;

        var owner = (HideScrollButtonsBehavior)sender;
        ScrollBar scrollBar = owner.AssociatedObject;
        double scrollPosition = (double)args.NewValue;

        if (scrollPosition <= 0)
            VisualStateManager.GoToState(scrollBar, owner.Direction + "DownOnly", true);
        else if (scrollPosition >= owner.ScrollExtent - owner.ViewportExtent)
            VisualStateManager.GoToState(scrollBar, owner.Direction + "UpOnly", true);
        else
            VisualStateManager.GoToState(scrollBar, owner.Direction + "Both", true);
    }
}

These are applied to the ScrollBar instances inside the ScrollViewer control template like this (DPs set using a RelativeSource binding):

<ScrollBar x:Name="VerticalScrollBar" 
            IsTabStop="False"
            Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
            Orientation="Vertical"
            ViewportSize="{TemplateBinding ViewportHeight}"
            Maximum="{TemplateBinding ScrollableHeight}"
            Minimum="0" 
            Value="{TemplateBinding VerticalOffset}">
    <i:Interaction.Behaviors>
        <behavior:HideScrollButtonsBehavior 
            VerticalScrollAmount="{Binding RelativeSource={RelativeSource AncestorType=ScrollViewer},Path=VerticalOffset}" 
            ScrollExtent="{Binding RelativeSource={RelativeSource AncestorType=ScrollViewer},Path=ExtentHeight}"
            ViewportExtent="{Binding RelativeSource={RelativeSource AncestorType=ScrollViewer},Path=ViewportHeight}"
            Direction="Vertical"
            />
    </i:Interaction.Behaviors>
</ScrollBar>

And finally, the visual states in the ScrollBar control template just show/hide the increase/decrease buttons as needed:

<VisualStateGroup x:Name="VerticalScrollButtonVisibilityStates">
    <VisualState x:Name="VerticalDownOnly">
        <Storyboard Duration="0:0:0.1">
            <DoubleAnimation Storyboard.TargetName="VerticalSmallDecrease" Storyboard.TargetProperty="Opacity"
                To="0" Duration="0:0:0.1" />
            <DoubleAnimation Storyboard.TargetName="VerticalSmallIncrease" Storyboard.TargetProperty="Opacity"
                To="1" Duration="0:0:0.1" />
        </Storyboard>
    </VisualState>
    <VisualState x:Name="VerticalUpOnly">
        <Storyboard Duration="0:0:0.1">
            <DoubleAnimation Storyboard.TargetName="VerticalSmallDecrease" Storyboard.TargetProperty="Opacity"
                To="1" Duration="0:0:0.1" />
            <DoubleAnimation Storyboard.TargetName="VerticalSmallIncrease" Storyboard.TargetProperty="Opacity"
                To="0" Duration="0:0:0.1" />
        </Storyboard>
    </VisualState>
    <VisualState x:Name="VerticalBoth">
        <Storyboard Duration="0:0:0.1">
            <DoubleAnimation Storyboard.TargetName="VerticalSmallDecrease" Storyboard.TargetProperty="Opacity"
                To="1" Duration="0:0:0.1" />
            <DoubleAnimation Storyboard.TargetName="VerticalSmallIncrease" Storyboard.TargetProperty="Opacity"
                To="1" Duration="0:0:0.1" />
        </Storyboard>
    </VisualState>
</VisualStateGroup>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top