A developer’s view

ScrollViewer always handles the mousewheel

· Read in about 3 min · (519 Words)
WPF ScrollViewer MouseWheel

The ScrollViewer control is a convenient control, when the content doesn’t fit on screen and you need some kind of scrolling to make all content accessible. I tend to use the mousewheel to scroll up and down, but this sometimes doesn’t work in WPF applications. It fails when you nest multiple scrollviewers, because the deepest ScrollViewer will always handle the MouseWheel message. Even if there is no vertical scrollbar visible or when it is disabled.

You can see this behaviour when you paste the following code in XamlPad. When you scroll down using the mousewheel and you hit the white area, then it is impossible to scroll further using the mousewheel.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Height="400">
 <ScrollViewer VerticalScrollBarVisibility="Auto">
  <StackPanel>
    <Border BorderBrush="Black" BorderThickness="1"
            Width="400" Height="200" Background="Red"/>
    <ScrollViewer VerticalScrollBarVisibility="Disabled">
      <Border BorderBrush="Black" BorderThickness="1" 
              Width="400" Height="200" Background="White"/> 
    </ScrollViewer>
    <Border BorderBrush="Black" BorderThickness="1"
            Width="400" Height="200" Background="Blue"/> 
  </StackPanel>
 </ScrollViewer>
</Page>

It’s a bad idea to have two vertical scrollbars that are nested, but sometimes you use a control that has a ScrollViewer inside its template. Because you have wrapped the control in your own ScrollViewer, the scrollbars will never be visible. But because it does handle the MouseWheel messages, the mousewheel will stop scrolling when the mouse hovers over it. This is very annoying to the end-user.

I used .NET Reflector to check how the MouseWheel message was handled. It looks like this:

protected override void OnMouseWheel(MouseWheelEventArgs e)
{
  if (!e.Handled && this.HandlesMouseWheelScrolling)
  {
    if (this.ScrollInfo != null)
    {
      if (e.Delta < 0)
        this.ScrollInfo.MouseWheelDown();
      else
        this.ScrollInfo.MouseWheelUp();
    }
    e.Handled = true;
  }
}

As you can see, it’s quite easy to prevent the MouseWheel message from being processed and that is to set the HandlesMouseWheelScrolling to false, when the vertical scrollbar is hidden or invisible. Unfortunately, Microsoft decided to mark this property as internal, so it isn’t accessible. This property is used exclusively by the TextBox control (it uses a ScrollViewer too), that disables the handling of the MouseWheel message of the internal ScrollViewer. The result is that a TextBox doesn’t block vertical scrolling when using the mousewheel.

The problem can also be solved a different way, but this requires is us to subclass the ScrollViewer:

public class ExtScrollViewer : ScrollViewer
{
  private ScrollBar verticalScrollbar;
 
  public override void OnApplyTemplate()
  {
    // Call base class
    base.OnApplyTemplate();
 
    // Obtain the vertical scrollbar
    this.verticalScrollbar = this.GetTemplateChild("PART_VerticalScrollBar") as ScrollBar;
  }
 
  protected override void OnMouseWheel(MouseWheelEventArgs e)
  {
    // Only handle this message if the vertical scrollbar is in use
    if (this.verticalScrollbar != null && 
        this.verticalScrollbar.Visibility == Visibility.Visible &&
        this.verticalScrollbar.IsEnabled)
    {
      // Perform default handling
      base.OnMouseWheel(e);
    }
  }
}

The new MouseWheel handling doesn’t do anything when the vertical scrollbar is either invisible or disabled. The drawback of this approach is that you need to use a custom ScrollViewer, instead of the default ScrollViewer. You can do this pretty easy for your own controls, but if you have third party controls that you don’t control, then this won’t work.

Sebastien Lambla describes a method on his blog that allows you to change the characteristics of the default ScrollViewer. Use whatever you like best.

I have submitted this bug with Microsoft. You can track it here.

Comments