I am trying to figure out how to get the mouse scroll working on a wpf window with a scrollviewer and a datagrid within it. The WPF and C# code is below
To enable touch support you might also want to set ScrollViewer.PanningMode
to None
on your DataGrid
and set the same property to VerticalFirst
or other value on your top level ScrollViewer
Example
<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="5" PanningMode="VerticalFirst">
<DataGrid ScrollViewer.PanningMode="None" ItemsSource="{Binding Items}" />
</ScrollViewer>
Of course, also use the PreviewMouseWheel
event as indicated by Don B's answers to fix the original mouse scrolling issue.
private static void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
var scrollViewer = (ScrollViewer)sender;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta);
e.Handled = true;
}
Or, you can simply set the following Attached Property to your ScrollViewer
public class TopMouseScrollPriorityBehavior
{
public static bool GetTopMouseScrollPriority(ScrollViewer obj)
{
return (bool)obj.GetValue(TopMouseScrollPriorityProperty);
}
public static void SetTopMouseScrollPriority(ScrollViewer obj, bool value)
{
obj.SetValue(TopMouseScrollPriorityProperty, value);
}
public static readonly DependencyProperty TopMouseScrollPriorityProperty =
DependencyProperty.RegisterAttached("TopMouseScrollPriority", typeof(bool), typeof(TopMouseScrollPriorityBehavior), new PropertyMetadata(false, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null)
throw new InvalidOperationException($"{nameof(TopMouseScrollPriorityBehavior)}.{nameof(TopMouseScrollPriorityProperty)} can only be applied to controls of type {nameof(ScrollViewer)}");
if (e.NewValue == e.OldValue)
return;
if ((bool)e.NewValue)
scrollViewer.PreviewMouseWheel += ScrollViewer_PreviewMouseWheel;
else
scrollViewer.PreviewMouseWheel -= ScrollViewer_PreviewMouseWheel;
}
private static void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
var scrollViewer = (ScrollViewer)sender;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta);
e.Handled = true;
}
}
Usage
<ScrollViewer b:TopMouseScrollPriorityBehavior.TopMouseScrollPriority="True" VerticalScrollBarVisibility="Auto" Margin="5" PanningMode="VerticalFirst">
<DataGrid ScrollViewer.PanningMode="None" ItemsSource="{Binding Items}" />
</ScrollViewer>
Where b: is the namespace that contains this behavior
This way your no code-behind is needed and your app is purely MVVM
I think Dave's solution is a good one. However, one recommendation I'd make is to catch the PreviewMouseWheel event on the scrollviewer instead of on the datagrid. If you don't, you might notice some minor differences based on whether you're scrolling over the datagrid or the scroll bar itself. The reasoning is that the scrollviewer will be handling scrolling when the mouse is hovered over the scrollbar, and the datagrid event will handle the scrolling when over the datagrid. For instance, one mouse wheel scroll over the datagrid might bring you farther down your list then it would when over the scroll bar. If you catch it on scrollviewer preview event, all will use the same measurement when scrolling. Also, if you catch it this way, you won't need to name the scrollviewer element, as you won't need a reference to the object since you can just type cast the sender object passed into the scrollviewer PreviewMouseWheel event. Lastly, I'd recommend marking the event handled at the end of the event, unless you need to catch it in an element further down the heirarchy for some reason. Example below:
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
I tried Don B's solution and it's solves the issue with scrolling over a data grid pretty well for the cases when you don't have other inner scrollable controls.
For the case when the scroll viewer has as children other scrollable controls and a data grid, then that requires that the event shouldn't be marked as handled at the end of the event handler for the main scroll viewer so it could be catched also in the inner scrollable controls, however that has the side effect that when the scroll should occur only on the inner scrollable control, it will also occur on the main scroll viewer.
So I've updated Dave's solution with the difference for how the scroll viewer is found so it won't be necessary to know the name of the scroll viewer:
private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scrollViewer = (((DependencyObject)sender).GetVisualParent<ScrollViewer>());
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta);
}
The Grid has a built in ScrollPanel so wrapping it with a ScrollPanel didn't work at all for me (making the Grid of fixed height was out of the question because I wanted it to resize nicely with the rest of the application).
What I did was a combination of a few of the higher rated solutions in here, but essentially the idea is to get rid of the ScrollPanel and just pipe the PreviewMouseEvent of the DataGrid back to the parent control that is actually handling the scrolling (in my case it was a Grid).
private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
MouseWheelEventArgs eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = MouseWheelEvent, Source = e.Source
};
DependencyObject parent = VisualTreeHelper.GetParent((DependencyObject) sender);
while (parent != null && !(parent is Grid))
{
parent = VisualTreeHelper.GetParent(parent);
}
if (parent != null)
{
Grid grid = (Grid) parent;
grid.RaiseEvent(eventArg);
}
e.Handled = true;
}