44

I'm having a problem getting mouse wheel scrolling to work in the following XAML, which I have simplified for clarity:

<ScrollViewer
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
CanContentScroll="False"
>
    <Grid
    MouseDown="Editor_MouseDown"
    MouseUp="Editor_MouseUp"
    MouseMove="Editor_MouseMove"
    Focusable="False"
    >
        <Grid.Resources>
            <DataTemplate
            DataType="{x:Type local:DataFieldModel}"
            >
                <Grid
                Margin="0,2,2,2"
                >
                    <TextBox
                    Cursor="IBeam"
                    MouseDown="TextBox_MouseDown"
                    MouseUp="TextBox_MouseUp"
                    MouseMove="TextBox_MouseMove"
                    />
                </Grid>
            </DataTemplate>
        </Grid.Resources>
        <ListBox
        x:Name="DataFieldListBox"
        ItemsSource="{Binding GetDataFields}"
        SelectionMode="Extended"
        Background="Transparent"
        Focusable="False"
        >
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <ListBox.ItemContainerStyle>
                <Style
                TargetType="ListBoxItem"
                >
                    <Setter
                    Property="Canvas.Left"
                    Value="{Binding dfX}"
                    />
                    <Setter
                    Property="Canvas.Top"
                    Value="{Binding dfY}"
                    />
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Grid>
</ScrollViewer>

Visually, the result is an area of some known size where DataFields read from a collection can be represented with TextBoxes which have arbitrary position, size, et cetera. In cases where the ListBox's styled "area" is too large to display all at once, horizontal and vertical scrolling is possible, but only with the scroll bars.

For better ergonomics and sanity, mouse wheel scrolling should be possible, and normally ScrollViewer would handle it automatically, but the ListBox appears to be handing those events such that the parent ScrollViewer never sees them. So far I have only been able to get wheel scrolling working be setting IsHitTestVisible=False for either the ListBox or the parent Grid, but of course none of the child element's mouse events work after that.

What can I do to ensure the ScrollViewer sees mouse wheel events while preserving others for child elements?

Edit: I just learned that ListBox has a built-in ScrollViewer which is probably stealing wheel events from the parent ScrollViewer and that specifying a control template can disable it. I'll update this question if that resolves the problem.

Tom
  • 627
  • 1
  • 5
  • 11

5 Answers5

64

You can also create a behavior and attach it to the parent control (in which the scroll events should bubble through).

// Used on sub-controls of an expander to bubble the mouse wheel scroll event up 
public sealed class BubbleScrollEvent : 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);
    }
}

<SomePanel>
            <i:Interaction.Behaviors>
                <viewsCommon:BubbleScrollEvent />
            </i:Interaction.Behaviors>
</SomePanel>
JoeB
  • 2,743
  • 6
  • 38
  • 51
  • 6
    It works !!! Thank you very much (do not forget to add the namespace xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity") – Gerard Walace Jun 16 '14 at 15:18
  • 7
    Just a note to anyone else trying to use this solution. You will need to install the Expression Blend SDK in order to get access to `System.Windows.Interactivity`. The NuGet command `Install-Package Expression.Blend.Sdk` will install it for you. – Bradley Uffner Aug 05 '14 at 22:11
  • 1
    @JoeB This is the better answer by far. Works easily with themes and other styles/templates! – Darkhydro Sep 29 '14 at 23:12
  • 3
    @BradleyUffner FYI, worked for me on VS2013 Ultimate without the Expression Blend SDK. Interestingly, ReSharper added the namespace `xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"` though, so not sure if it's just included now. – Stajs Mar 31 '15 at 09:43
  • 1
    +1 It works, but I don't understand why it works :( In all my WPF experience setting `e.Handled = true` is enough, but in this occasion I have to raise the event again!? Anyway, thanks for a workaround! – Samuel Aug 09 '15 at 07:57
  • A note for the future readers. I was trying to implement this in a `ListBox` where my `ListBoxItem`s were stealing wheel event. This solution didn't work for me initially until I realized that I needed to attach this behavior to the items, not the parent ListBox. Maybe it will help someone down the road. – dotNET Jun 01 '17 at 00:08
  • Seriously, this is the only answer that worked properly for me. Works perfectly with our MVVM solution. – Fred Aug 14 '17 at 12:45
  • I got design-time error `XamlObjectWriterException: Collection property 'System.Windows.Controls.StackPanel'.'Behaviors' is null.` within parent UserControl – Val Sep 09 '19 at 09:31
  • Is it working with panning events using a touchscreen ? – Morgane Dec 11 '19 at 16:32
14

Specifying a ControlTemplate for the Listbox which doesn't include a ScrollViewer solves the problem. See this answer and these two MSDN pages for more information:

ControlTemplate

ListBox Styles and Templates

Community
  • 1
  • 1
Tom
  • 627
  • 1
  • 5
  • 11
5

Another way of implementing this, is by creating you own ScrollViewer like this:

public class MyScrollViewer : ScrollViewer
{
    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
        var parentElement = Parent as UIElement;
        if (parentElement != null)
        {
            if ((e.Delta > 0 && VerticalOffset == 0) ||
                (e.Delta < 0 && VerticalOffset == ScrollableHeight))
            {
                e.Handled = true;

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

        base.OnMouseWheel(e);
    }
}
kahr
  • 115
  • 2
  • 8
2

I know it's a little late but I have another solution that worked for me. I switched out my stackpanel/listbox for an itemscontrol/grid. Not sure why the scroll events work properly but they do in my case.

<ScrollViewer VerticalScrollBarVisibility="Auto" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
                <StackPanel Orientation="Vertical">
                    <ListBox ItemsSource="{Binding DrillingConfigs}" Margin="0,5,0,0">
                        <ListBox.ItemTemplate>
                            <DataTemplate>

became

<ScrollViewer VerticalScrollBarVisibility="Auto" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ItemsControl ItemsSource="{Binding DrillingConfigs}" Margin="0,5,0,0" Grid.Row="0">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
sean.net
  • 735
  • 8
  • 25
  • This is because is more "dumb" control (actually, it's the ancestor for ListBox) and it's have no support for scrolling at all. So you got a solution that _potentially_ is slower than previous version (ListBox-based). – Yury Schkatula Apr 05 '16 at 15:16
0

isHitTestVisible=False in the child works great for me

Edit This isnt a good way to do it

Gabagoool
  • 1
  • 2