Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bubbling scroll events from a ListView to its parent

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?

like image 424
sourcenouveau Avatar asked Oct 18 '09 17:10

sourcenouveau


4 Answers

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.

like image 80
keyle Avatar answered Nov 18 '22 00:11

keyle


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

like image 31
makc Avatar answered Nov 17 '22 23:11

makc


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>
like image 13
DLeh Avatar answered Nov 17 '22 22:11

DLeh


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);
}
like image 4
Josh F. Avatar answered Nov 17 '22 23:11

Josh F.