wwarby
wwarby

Reputation: 2051

WPF - Set image visibility when ScrollViewer is scrolled

I have a ScrollViewer on a WPF user control and what I'd like to achieve is to show a shadow image at the top and bottom of the ScrollViewer, but hide the top shadow when the scrollbar is at the top, and hide the bottom shadow when the scrollbar is at the bottom.

In other words, I need to bind the Visibility property of the image to the offset of the ScrollViewer somehow. The following code is clearly not right but should illustrate what I'm trying to do.

<Grid>
    <Image Source="Shadow.png" VerticalAlignment="Top">
        <Image.Resources>
            <Style TargetType="Image">
                <Style.Triggers>
                    <Trigger SourceName="Scroller" Property="VerticalOffset" Value="GREATER THAN ZERO OR LESS THAN MAX">
                        <Setter Property="Visibility" Value="Collapsed" />
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Image.Resources>
    </Image>
    <ScrollViewer Height="200" x:Name="Scroller">
        <ContentControl />
    </ScrollViewer>
</Grid>

Upvotes: 2

Views: 673

Answers (2)

Wyatt Earp
Wyatt Earp

Reputation: 1823

Here is what I would do:

First, you'll need an IMultiValueConverter:

public class ScrollOffsetToVisibilityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values == null)
            throw new ArgumentException("Values cannot be null.");
        if (values.Count() != 2)
            throw new ArgumentException("Incorrect number of bindings (" + values.Count() + ")");
        if (parameter == null)
            throw new ArgumentException("Parameter cannot be null.");

        var top = parameter.ToString().ToUpper() == "TOP";

        var offset = Double.Parse(values[0].ToString());
        var maxHeight = Double.Parse(values[1].ToString());

        return (top && offset == 0) || (!top && offset == maxHeight) ? Visibility.Visible : Visibility.Collapsed;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Then, you can use this converter to apply a Setter for the Visibility property of your image.

<Image.Style Source="Shadow.png" VerticalAlignment="Top">
    <Style>
        <Setter Property="Image.Visibility">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource ScrollOffsetToVisibilityConverter}" ConverterParameter="Top">
                    <Binding ElementName="Scroller" Path="VerticalOffset"/>
                    <Binding ElementName="Scroller" Path="ScrollableHeight"/>
                </MultiBinding>
            </Setter.Value>
        </Setter>
    </Style>
</Image.Style>

Just pass in "top" or "bottom" (or more accurately, not "top") for the ConverterParameter to return "Visible" if the scroll bar is at the top or bottom.

Upvotes: 2

Bizhan
Bizhan

Reputation: 17085

You have to use ScrollBar instead of ScrollViewer because ScrollViewer can't tell when it reaches the end of its content. (or at least I haven't seen a reliable method for doing so). On the other hand ScrollBar is more suitable for these kinds of operations.

I used a ListBox with several items to test this behavior. you can change it to whatever you need.

Xaml :

<Grid>
    <ListBox ScrollBar.Scroll="ListBox_Scroll">
        <!-- add many items here -->
        <TextBlock Text="something"/>
    </ListBox>

    <Image Source="Shadow.png" VerticalAlignment="Top">
        <Image.Style>
            <Style TargetType="Image">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding ScrollerState}" 
                            Value="{x:Static enum:ScrollState.AtTheTop}">
                        <Setter Property="Visibility" Value="Collapsed" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Image.Style>
    </Image>
    <Image Source="Shadow.png" VerticalAlignment="Bottom">
        <Image.Style>
            <Style TargetType="Image">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding ScrollerState}" 
                          Value="{x:Static enum:ScrollState.AtTheBottom}">
                        <Setter Property="Visibility" Value="Collapsed" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Image.Style>
    </Image>
</Grid>

As you can see I changed the trigger to DataTrigger and used a bindable property in DataContext (named ScrollerState with type of ScrollState which is a simple enum) to bind to.

enum:

public enum ScrollState
{
    AtTheTop, AtTheBottom, InBetween
}

Now in Code behind we implement the Scroll event from where we change the value of ScrollerState:

public MainWindow()
{
    InitializeComponent();
    DataContext = new VM();
}

private void ListBox_Scroll(object sender, ScrollEventArgs e)
{
    ScrollBar sb = e.OriginalSource as ScrollBar;

    if (sb.Orientation == Orientation.Horizontal)
        return;

    if (sb.Value == 0)
        (DataContext as VM).ScrollerState = ScrollState.AtTheTop;
    else if (sb.Value == sb.Maximum)
        (DataContext as VM).ScrollerState = ScrollState.AtTheBottom;
    else
        (DataContext as VM).ScrollerState = ScrollState.InBetween;
}

VM (an instance of this is set as DataContext of the window):

public class VM : DependencyObject
{
    public ScrollState ScrollerState
    {
        get { return (ScrollState)GetValue(ScrollerStateProperty); }
        set { SetValue(ScrollerStateProperty, value); }
    }
    public static readonly DependencyProperty ScrollerStateProperty =
        DependencyProperty.Register("ScrollerState", typeof(ScrollState), typeof(VM), 
        new PropertyMetadata(ScrollState.AtTheTop));
}

Upvotes: 0

Related Questions