2

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>
halfer
  • 19,824
  • 17
  • 99
  • 186
wwarby
  • 1,873
  • 1
  • 21
  • 37
  • I think this may (at least partially) answer your question : http://stackoverflow.com/questions/10793717/how-to-find-that-scrollviewer-is-scrolled-to-the-end-in-wpf – thomasb Feb 23 '15 at 16:48

2 Answers2

2

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.

Wyatt Earp
  • 1,783
  • 13
  • 23
  • This code really helped me out - however I had to swap Visible and Collapsed in the converter code as the behavior was the opposite of what I wanted in combination with my Grid element I manipulated with it. – Christian Ivicevic Apr 04 '16 at 11:01
0

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));
}
Bizhan
  • 16,157
  • 9
  • 63
  • 101
  • Much appreciated and I'm sure this would have worked - I went with the alternative suggestion since it didn't require any code behind. – wwarby Feb 25 '15 at 10:05