Thursday, November 19, 2020

DataGrid captures mousewheel and stops page from scrolling

One of the frustrating things about WPF is when you have a scrollable data grid inside a scrolling page. If you mouse wheel the page up or down it stops working as soon as the data grid scrolls under the cursor. The data grid's scroll viewer is receiving the mouse wheel events and, even when the data grid can't scroll, it does not pass them up to the page so the page scrolls no more.

Fortunately there is a fairly simple solution. Let's start by demonstrating the problem. Start a new C#, .Net Core Visual Studio project called IntelligentScrolling.

We will start by writing a base WPF class because we're good little programmers not like we used to be. Add a class called BaseWPFWindow and populate it thusly. It contains the boilerplate code for handling PropertyChanged.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
 
namespace IntelligentScrolling
{
    public class BaseWPFWindow:WindowINotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void SetProperty<T>(ref T storageT value, [CallerMemberNamestring name = "")
        {
            if (!Object.Equals(storagevalue))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(thisnew PropertyChangedEventArgs(name));
            }
        }
    }
}

The XAML looks like this.

<local:BaseWPFWindow x:Class="IntelligentScrolling.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:IntelligentScrolling"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Intelligent Scrolling" Height="450" Width="800" MaxHeight="450">
    <ScrollViewer>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="300"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Text="Header Area" FontSize="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <DataGrid Grid.Row="1" ItemsSource="{Binding Items}" Height="300" IsReadOnly="True"/>
        </Grid>
    </ScrollViewer>
</local:BaseWPFWindow>

and the code behind looks like this...

using System.Collections.ObjectModel;
 
namespace IntelligentScrolling
{
    public partial class MainWindow :  BaseWPFWindow
    {
        public class cItem
        {
            public string Code { getset; }
            public string Description { getset; }
        }
 
        private Collection<cItem> _Items;
        public Collection<cItem> Items
        {
            get { return _Items; }
            set { SetProperty(ref _Items, value); }
        }
 
        public MainWindow()
        {
            Items = PopulateItems();
            InitializeComponent();
        }
 
        public Collection<cItemPopulateItems()
        {
            Collection<cItemItems = new Collection<cItem>();
            for (int i = 0; i < 30; i++)
            {
                Items.Add(new cItem() { Code = "Code" + i, Description = "Description" + i });
            }
            return Items;
        }
    }
}

If you run this you will see a static label at the top with a data grid below. The whole page is in a scroll viewer. If you place the cursor as shown below and start scrolling with the mouse wheel you will see that as soon as the data grid moves under the cursor the page can no longer be scrolled. We want to change this behavior so that if the data grid cannot be scrolled in the desired direction the page scrolls instead.


We are going to capture the data grid's PreviewMouseWheel event. If the data grid cannot scroll any further in the desired direction we will scroll the containing scroll viewer instead.

Add the following code to the base class. All the interesting code is in the IntelligentScroll method. The other two methods should already be somewhere in your WPF library. All this code is written generically so it only has to exist in your base class (you DO have a base class, right?).

private void IntelligentScroll(object senderMouseWheelEventArgs e)
{
    if (!(sender is DataGrid)) throw new NotImplementedException("IntelligentScroll is only implemented for DataGrids");
 
    // Is the datagrid at the limit for the direction of scroll?
    ScrollViewer svDataGrid = GetFirstChildOfType<ScrollViewer>(sender as DependencyObject);
    bool IsAtLimit = false;
 
    if (svDataGrid == nullreturn;
    if (e.Delta > 0 && svDataGrid.VerticalOffset == 0) IsAtLimit = true;
    if (e.Delta < 0 && svDataGrid.VerticalOffset == svDataGrid.ScrollableHeight) IsAtLimit = true;
 
    if (IsAtLimit)
    {
        ScrollViewer svParent = GetParentOfType<ScrollViewer>(svDataGridas ScrollViewer;
        if (svParent != nullsvParent.ScrollToVerticalOffset(svParent.VerticalOffset - e.Delta);
    }
}
 
public T GetFirstChildOfType<T>(System.Windows.DependencyObject propwhere T : System.Windows.DependencyObject
{
    for (int i = 0; i <= System.Windows.Media.VisualTreeHelper.GetChildrenCount(prop) - 1; i++)
    {
        System.Windows.DependencyObject child = System.Windows.Media.VisualTreeHelper.GetChild((prop), ias System.Windows.DependencyObject;
        if (child == null)
            continue;
 
        T castedProp = child as T;
        if (castedProp != null)
            return castedProp;
 
        castedProp = GetFirstChildOfType<T>(child);
 
        if (castedProp != null)
            return castedProp;
    }
    return null;
}
 
private T GetParentOfType<T>(DependencyObject controlwhere T:System.Windows.DependencyObject
{
    DependencyObject ParentControl = control;
 
     do
        ParentControl = VisualTreeHelper.GetParent(ParentControl);
    while (ParentControl != null && !(ParentControl is T));
 
    return ParentControl as T;
}

It is interesting to note that the scroll delta is positive when scrolling up and negative when scrolling down.

All we do now is add a PreviewMouseWheel event handler for the data grid.

<DataGrid Grid.Row="1" ItemsSource="{Binding Items}" Height="300" IsReadOnly="True" PreviewMouseWheel="IntelligentScroll"/>

If you run the application now you will see that you can mouse wheel the page until the data grid moves under the cursor. If you continue mouse wheeling the data grid will start to scroll until it cannot scroll anymore and then the page will start scrolling again. Scroll the other way too. This is far more convenient for the users.



No comments:

Post a Comment