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:Window, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "") { if (!Object.Equals(storage, value)) { storage = value; if (PropertyChanged != null) PropertyChanged(this, new 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 { get; set; } public string Description { get; set; } } private Collection<cItem> _Items; public Collection<cItem> Items { get { return _Items; } set { SetProperty(ref _Items, value); } } public MainWindow() { Items = PopulateItems(); InitializeComponent(); } public Collection<cItem> PopulateItems() { Collection<cItem> Items = 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 sender, MouseWheelEventArgs 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 == null) return; 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>(svDataGrid) as ScrollViewer; if (svParent != null) svParent.ScrollToVerticalOffset(svParent.VerticalOffset - e.Delta); } }
public T GetFirstChildOfType<T>(System.Windows.DependencyObject prop) where 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), i) as 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 control) where 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