31

I have a scrollviewer with a couple listboxes in it. The problem is if a user uses the middle mouse roller to scroll the scrollviewer while their mouse is over a listview. The listview scrolls its internal scrollviewer to the bottom and then continues to capture the mouse, preventing the containing scrollviewer from scrolling.

Any ideas on how to handle this?

ConditionRacer
  • 4,418
  • 6
  • 45
  • 67
  • I have the same problem. There's a discussion here: http://social.msdn.microsoft.com/Forums/en-IE/wpf/thread/b440b2cb-26e0-4115-9858-5679c4e45e0a I'll let you know if I resolve the problem. – Kos Jul 12 '12 at 12:01

5 Answers5

69

That happens because the ListView's (ListBox's, actually) content template wraps its items with a ScrollViewer by itself.

The simplest way is to disable it by dropping your own Template for the inside ListView, one that doesn't create a ScrollViewer:

    <ListView>
      <ListView.Template>
        <ControlTemplate>
          <ItemsPresenter></ItemsPresenter>
        </ControlTemplate>
      </ListView.Template>
      ...
    </ListView>

BTW the same happens if you have a ListView inside a ListView (this was my case).

Kos
  • 70,399
  • 25
  • 169
  • 233
  • This solution worked nicely for me and it is easy to implement. I tried other suggested solutions, but only this worked for me (ListViewer inside a ScrollViewer) – Peter Jun 23 '20 at 08:59
  • I was looking for exactly this. Been a long time windows 10 dev and this issue caught me by surprise in WPF. Thanks +1 :) – iam.Carrot Jul 14 '21 at 20:43
  • Haha thanks! I haven't touched any C# since 2013, happy to see that this answer is still relevant! – Kos Jul 20 '21 at 14:28
7

IMO, the best way to handle this scenario is to create a custom control :

     class MyScrollViewer : ScrollViewer
     {
         protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
         {
            base.OnPreviewMouseWheel(e);
            if (!e.Handled)
            {
                e.Handled = true;
                this.RaiseEvent(new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
                {
                    RoutedEvent = UIElement.MouseWheelEvent,
                    Source = this
                });
            }
        }
    }
Poppyto
  • 554
  • 8
  • 17
  • 2
    Your answer is the best, because other answers, will remove the header, and to solve this you will need to implement the ScrollViewer from the ground see [ref1](https://stackoverflow.com/questions/52719006/wpf-listview-custom-scrollviewer-causes-column-header-to-disappear) [help](https://stackoverflow.com/questions/20784207/custom-wpf-listview-with-style-using-datatemplate-how-do-i-add-headers) – Al Banna Techno logy Apr 25 '21 at 11:56
6

Did you try disabling the ListView's ScrollBars?

<ListView ScrollViewer.HorizontalScrollBarVisibility="Disabled"
          ScrollViewer.VerticalScrollBarVisibility="Disabled" />
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • I can't do that because the listviews may have more items than are visible. – ConditionRacer Jan 19 '12 at 22:17
  • 1
    @Justin984 Aren't they in another `ScrollViewer` though? – Rachel Jan 20 '12 at 04:35
  • Bah, I was going to draw a little ascii pic, but it's too much of a pain. The listboxes each have multiple entries. For example, list box 1 may have 10 entries, which require scrolling the listbox in order to view. But there are multiple listboxes which, together, are too large for the screen, so i have an outer scrollviewer to scroll the listboxes into view. Does that make sense? – ConditionRacer Jan 23 '12 at 14:50
  • @Justin984 Perhaps you can do something where if the ListView is already scrolled to the bottom, it stops responding to scroll down events? Or instead passes them to the outer `ScrollViewer`? – Rachel Jan 23 '12 at 15:26
5

Inspired by some helpful answers, I have an implementation that scrolls ancestor ScrollViewers when inner ones (including from ListView, ListBox, DataGrid) scroll past their top/bottom.

I apply an attached property to all ScrollViewers in App.xaml:

<Style TargetType="ScrollViewer" BasedOn="{StaticResource {x:Type ScrollViewer}}">
    <Setter Property="local:ScrollViewerHelper.FixMouseWheel" Value="True" />
</Style>

The attached property detects scrolling past top/bottom, and when that happens raises a mouse wheel event on the ScrollViewer's parent. Event routing gets it to the outer ScrollViewer:

public static class ScrollViewerHelper
{
    // Attached property boilerplate
    public static bool GetFixMouseWheel(ScrollViewer scrollViewer) => (bool)scrollViewer?.GetValue(FixMouseWheelProperty);
    public static void SetFixMouseWheel(ScrollViewer scrollViewer, bool value) => scrollViewer?.SetValue(FixMouseWheelProperty, value);
    public static readonly DependencyProperty FixMouseWheelProperty =
        DependencyProperty.RegisterAttached("FixMouseWheel", typeof(bool), typeof(ScrollViewerHelper),
            new PropertyMetadata(OnFixMouseWheelChanged));
    // End attached property boilerplate

    static void OnFixMouseWheelChanged(DependencyObject d,
                                       DependencyPropertyChangedEventArgs e)
    {
        var scrollViewer = d as ScrollViewer;
        if (scrollViewer == null) return;

        scrollViewer.PreviewMouseWheel += (s2, e2) =>
        {
            var parent = scrollViewer.Parent as UIElement;
            bool hitTopOrBottom = HitTopOrBottom(e2.Delta, scrollViewer);
            if (parent is null || !hitTopOrBottom) return;

            var argsCopy = Copy(e2);
            parent.RaiseEvent(argsCopy);
        };
    }

    static bool HitTopOrBottom(double delta, ScrollViewer scrollViewer)
    {
        var contentVerticalOffset = scrollViewer.ContentVerticalOffset;

        var atTop = contentVerticalOffset == 0;
        var movedUp = delta > 0;
        var hitTop = atTop && movedUp;

        var atBottom =
            contentVerticalOffset == scrollViewer.ScrollableHeight;
        var movedDown = delta < 0;
        var hitBottom = atBottom && movedDown;

        return hitTop || hitBottom;
    }

    static MouseWheelEventArgs Copy(MouseWheelEventArgs e)
        => new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
        {
            RoutedEvent = UIElement.MouseWheelEvent,
            Source = e.Source,
        };
}
Vimes
  • 10,577
  • 17
  • 66
  • 86
  • This worked for me, except I had to use TargetType="{x:Type ScrollViewer}" instead of TargetType="ScrollViewer" – cnhe Sep 08 '22 at 06:58
0

If you wrap the inner listview in a scrollviewer then the scrolling will work.

<ListView ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
                <ListView>
                    <ListView.ItemTemplate>
                        <DataTemplate>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </ScrollViewer>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
aquamoon
  • 15
  • 6