Bubbling scroll events from a ListView to its parent

WpfListviewScrollEvent Bubbling

Wpf Problem Overview


In my WPF application I have a ListView whose ScrollViewer.VerticalScrollBarVisibility is set to Disabled. It is contained within a ScrollViewer. When I attempt to use the mouse wheel over the ListView, the outer ScrollViewer does not scroll because the ListView is capturing the scroll events.

How can I force the ListView to allow the scroll events to bubble up to the ScrollViewer?

Wpf Solutions


Solution 1 - Wpf

You need to capture the preview mouse wheel event in the inner listview

MyListView.PreviewMouseWheel += HandlePreviewMouseWheel;

Or in the XAML

<ListView ... PreviewMouseWheel="HandlePreviewMouseWheel">

then stop the event from scrolling the listview and raise the event in the parent listview.

private void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e) {
    if (!e.Handled) {
        e.Handled = true;
        var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
        eventArg.RoutedEvent = UIElement.MouseWheelEvent;
        eventArg.Source = sender;
        var parent = ((Control)sender).Parent as UIElement;
        parent.RaiseEvent(eventArg);
    }
}

Creds go to @robert-wagner who solved this for me a few months ago.

Solution 2 - Wpf

Another nice solution using attached behavior. I like it because it decoples the solution from the Control.

Create a no scroling behavior which will catch the PreviewMouseWheel(Tunneling) event and raise a new MouseWheelEvent(Bubbling)

public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{

  protected override void OnAttached( )
  {
    base.OnAttached( );
    AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel ;
  }

protected override void OnDetaching( )
{
    AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
    base.OnDetaching( );
}

void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{

    e.Handled = true;

    var e2 = new MouseWheelEventArgs(e.MouseDevice,e.Timestamp,e.Delta);
    e2.RoutedEvent = UIElement.MouseWheelEvent;
        AssociatedObject.RaiseEvent(e2);

    }
}

Then attach the behavior to any UIElement with nested ScrollViewers case

 <ListBox Name="ForwardScrolling">
    <i:Interaction.Behaviors>
        <local:IgnoreMouseWheelBehavior />
    </i:Interaction.Behaviors>
</ListBox>

all credit to Josh Einstein Blog

Solution 3 - Wpf

If you're coming here looking for a solution to bubble the event ONLY if the child is at the top and scrolling up or the bottom and scrolling down, here's a solution. I only tested this with DataGrid, but it should work with other controls as well.

public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
        base.OnDetaching();
    }

    private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
        var scrollPos = scrollViewer.ContentVerticalOffset;
        if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
            || (scrollPos == 0 && e.Delta > 0))
        {
            e.Handled = true;
            var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
            e2.RoutedEvent = UIElement.MouseWheelEvent;
            AssociatedObject.RaiseEvent(e2);
        }
    }

    private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T child = default(T);

        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T;
            if (child == null)
            {
                child = GetVisualChild<T>(v);
            }
            if (child != null)
            {
                break;
            }
        }
        return child;
    }
}

To attach this behavior, add the following XMLNS and XAML to your element:

    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

    <i:Interaction.Behaviors>
        <shared:ScrollParentWhenAtMax />
    </i:Interaction.Behaviors>

Solution 4 - Wpf

There are different approaches depending on your exact situation, but I found this to work nicely. Assuming your basic situation is this:

<Window Height="200" Width="200">
<Grid>
    <ScrollViewer Name="sViewer">
        <StackPanel>
            <Label Content="Scroll works here" Margin="10" />
            <ListView Name="listTest" Margin="10" 
                      PreviewMouseWheel="listTest_PreviewMouseWheel" 
                      ScrollViewer.VerticalScrollBarVisibility="Disabled">
                <ListView.ItemsSource>
                    <Int32Collection>
                        1,2,3,4,5,6,7,8,9,10
                    </Int32Collection>
                </ListView.ItemsSource>
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Column 1" />
                    </GridView>
                </ListView.View>
            </ListView>
        </StackPanel>
    </ScrollViewer>
</Grid>
</Window>

Raising MouseWheelEvent yourself during PreviewMouseWheel seems to force the ScrollViewer to work. I wish I knew why, it seems very counterintuitive.

private void listTest_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
    e.Handled = true;
    MouseWheelEventArgs e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
    e2.RoutedEvent = UIElement.MouseWheelEvent;
    listTest.RaiseEvent(e2);
}

Solution 5 - Wpf

You can also achieve the same thing using an attached behaviour. This has the advantage of not needing the System.Windows.Interactivity library. The logic has been taken from the other answers, only the implementation is different.

public static class IgnoreScrollBehaviour
{
    public static readonly DependencyProperty IgnoreScrollProperty = DependencyProperty.RegisterAttached("IgnoreScroll", typeof(bool), typeof(IgnoreScrollBehaviour), new PropertyMetadata(OnIgnoreScollChanged));

    public static void SetIgnoreScroll(DependencyObject o, string value)
    {
        o.SetValue(IgnoreScrollProperty, value);
    }

    public static string GetIgnoreScroll(DependencyObject o)
    {
        return (string)o.GetValue(IgnoreScrollProperty);
    }

    private static void OnIgnoreScollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        bool ignoreScoll = (bool)e.NewValue;
        UIElement element = d as UIElement;

        if (element == null)
            return;

        if (ignoreScoll)
        {
            element.PreviewMouseWheel += Element_PreviewMouseWheel;
        }
        else
        {
            element.PreviewMouseWheel -= Element_PreviewMouseWheel;
        }
    }

    private static void Element_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        UIElement element = sender as UIElement;

        if (element != null)
        {
            e.Handled = true;

            var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
            e2.RoutedEvent = UIElement.MouseWheelEvent;
            element.RaiseEvent(e2);
        }
    }
}

And then in the XAML:

<DataGrid ItemsSource="{Binding Items}">

<DataGrid.RowDetailsTemplate>
    <DataTemplate>

        <ListView ItemsSource="{Binding Results}"
                  behaviours:IgnoreScrollBehaviour.IgnoreScroll="True">
            <ListView.ItemTemplate>
                <DataTemplate>
                    ...
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </DataTemplate>
</DataGrid.RowDetailsTemplate>

<DataGrid.Columns>
   ...
</DataGrid.Columns>

</DataGrid>

Solution 6 - Wpf

My use case was slightly different. I have a very big scrollviewer and at the bottom another scrollviewer which has a maxheight of 600. I want to scroll the whole page to the bottom until I pass scrollevents to the inner scrollviewer. This ensures you see the whole scrollviewer first, before you start scrolling.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace CleverScroller.Helper
{
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
        base.OnDetaching();
    }

    private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (e.Delta < 0)
        {
            var outerscroller = GetVisualParent<ScrollViewer>(this.AssociatedObject);
            if (outerscroller.ContentVerticalOffset < outerscroller.ScrollableHeight)
            {
                e.Handled = true;
                var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
                e2.RoutedEvent = UIElement.MouseWheelEvent;
                AssociatedObject.RaiseEvent(e2);
            }
        }
        else
        {
            var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
            var scrollPos = scrollViewer.ContentVerticalOffset;
            if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
                || (scrollPos == 0 && e.Delta > 0))
            {
                e.Handled = true;
                var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
                e2.RoutedEvent = UIElement.MouseWheelEvent;
                AssociatedObject.RaiseEvent(e2);
            }
        }
    }

    private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T child = default(T);

        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T;
            if (child == null)
            {
                child = GetVisualChild<T>(v);
            }
            if (child != null)
            {
                break;
            }
        }
        return child;
    }

    private static T GetVisualParent<T>(DependencyObject parent) where T : Visual
    {
        T obj = default(T);
        Visual v = (Visual)VisualTreeHelper.GetParent(parent);
        do
        {
            v = (Visual)VisualTreeHelper.GetParent(v);
            obj = v as T;
        } while (obj == null);
        
        return obj;
    }
}
}

Solution 7 - Wpf

Thanks Keyle

I adapted your answer as an RX extension method

    public static IDisposable ScrollsParent(this ItemsControl itemsControl)
    {
        return Observable.FromEventPattern<MouseWheelEventHandler, MouseWheelEventArgs>(
           x => itemsControl.PreviewMouseWheel += x,
           x => itemsControl.PreviewMouseWheel -= x)
           .Subscribe(e =>
           {
               if(!e.EventArgs.Handled)
               {
                   e.EventArgs.Handled = true;
                   var eventArg = new MouseWheelEventArgs(e.EventArgs.MouseDevice, e.EventArgs.Timestamp, e.EventArgs.Delta)
                   {
                       RoutedEvent = UIElement.MouseWheelEvent,
                       Source = e.Sender
                   };
                   var parent = ((Control)e.Sender).Parent as UIElement;
                   parent.RaiseEvent(eventArg);
               }
           });
    }

Usage:

 myList.ScrollsParent().DisposeWith(disposables);

Solution 8 - Wpf

Ok been a while since I have been on SO but I had to comment on this. Any Preview event tunnels, so why are we bubbling it up? Stop the tunnel in the parent and be done with it. in the parent add a PreviewMouseWheel event.

     private void UIElement_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
    var scrollViewer = FindName("LeftPanelScrollViwer"); // name your parent mine is a scrollViewer
    ((ScrollViewer) scrollViewer)?.ScrollToVerticalOffset(e.Delta);
    e.Handled = true;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsourcenouveauView Question on Stackoverflow
Solution 1 - WpfkeyleView Answer on Stackoverflow
Solution 2 - WpfmakcView Answer on Stackoverflow
Solution 3 - WpfDLehView Answer on Stackoverflow
Solution 4 - WpfJosh F.View Answer on Stackoverflow
Solution 5 - WpfSimon StanfordView Answer on Stackoverflow
Solution 6 - WpfManuView Answer on Stackoverflow
Solution 7 - WpfBilly Jake O'ConnorView Answer on Stackoverflow
Solution 8 - WpfLawrence ThurmanView Answer on Stackoverflow