In my WPF application I have a ListView
whose ScrollViewer.VerticalScrollBarVisibility
is set to Disabled
. It is contained within a
You can also achieve the same thing using an attached behaviour. This has the advantage of not needing the System.Windows.Interactivity library. The logic has been taken from the other answers, only the implementation is different.
public static class IgnoreScrollBehaviour
{
public static readonly DependencyProperty IgnoreScrollProperty = DependencyProperty.RegisterAttached("IgnoreScroll", typeof(bool), typeof(IgnoreScrollBehaviour), new PropertyMetadata(OnIgnoreScollChanged));
public static void SetIgnoreScroll(DependencyObject o, string value)
{
o.SetValue(IgnoreScrollProperty, value);
}
public static string GetIgnoreScroll(DependencyObject o)
{
return (string)o.GetValue(IgnoreScrollProperty);
}
private static void OnIgnoreScollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool ignoreScoll = (bool)e.NewValue;
UIElement element = d as UIElement;
if (element == null)
return;
if (ignoreScoll)
{
element.PreviewMouseWheel += Element_PreviewMouseWheel;
}
else
{
element.PreviewMouseWheel -= Element_PreviewMouseWheel;
}
}
private static void Element_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
UIElement element = sender as UIElement;
if (element != null)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
element.RaiseEvent(e2);
}
}
}
And then in the XAML:
<DataGrid ItemsSource="{Binding Items}">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Results}"
behaviours:IgnoreScrollBehaviour.IgnoreScroll="True">
<ListView.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid.Columns>
...
</DataGrid.Columns>
</DataGrid>
There are different approaches depending on your exact situation, but I found this to work nicely. Assuming your basic situation is this:
<Window Height="200" Width="200">
<Grid>
<ScrollViewer Name="sViewer">
<StackPanel>
<Label Content="Scroll works here" Margin="10" />
<ListView Name="listTest" Margin="10"
PreviewMouseWheel="listTest_PreviewMouseWheel"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListView.ItemsSource>
<Int32Collection>
1,2,3,4,5,6,7,8,9,10
</Int32Collection>
</ListView.ItemsSource>
<ListView.View>
<GridView>
<GridViewColumn Header="Column 1" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
Raising MouseWheelEvent yourself during PreviewMouseWheel seems to force the ScrollViewer to work. I wish I knew why, it seems very counterintuitive.
private void listTest_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
MouseWheelEventArgs e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
listTest.RaiseEvent(e2);
}
Ok been a while since I have been on SO but I had to comment on this. Any Preview event tunnels, so why are we bubbling it up? Stop the tunnel in the parent and be done with it. in the parent add a PreviewMouseWheel event.
private void UIElement_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollViewer = FindName("LeftPanelScrollViwer"); // name your parent mine is a scrollViewer
((ScrollViewer) scrollViewer)?.ScrollToVerticalOffset(e.Delta);
e.Handled = true;
}
Thanks Keyle
I adapted your answer as an RX extension method
public static IDisposable ScrollsParent(this ItemsControl itemsControl)
{
return Observable.FromEventPattern<MouseWheelEventHandler, MouseWheelEventArgs>(
x => itemsControl.PreviewMouseWheel += x,
x => itemsControl.PreviewMouseWheel -= x)
.Subscribe(e =>
{
if(!e.EventArgs.Handled)
{
e.EventArgs.Handled = true;
var eventArg = new MouseWheelEventArgs(e.EventArgs.MouseDevice, e.EventArgs.Timestamp, e.EventArgs.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = e.Sender
};
var parent = ((Control)e.Sender).Parent as UIElement;
parent.RaiseEvent(eventArg);
}
});
}
Usage:
myList.ScrollsParent().DisposeWith(disposables);
If you're coming here looking for a solution to bubble the event ONLY if the child is at the top and scrolling up or the bottom and scrolling down, here's a solution. I only tested this with DataGrid, but it should work with other controls as well.
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
protected override void OnDetaching()
{
this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
var scrollPos = scrollViewer.ContentVerticalOffset;
if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
|| (scrollPos == 0 && e.Delta > 0))
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}
To attach this behavior, add the following XMLNS and XAML to your element:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<i:Interaction.Behaviors>
<shared:ScrollParentWhenAtMax />
</i:Interaction.Behaviors>
My use case was slightly different. I have a very big scrollviewer and at the bottom another scrollviewer which has a maxheight of 600. I want to scroll the whole page to the bottom until I pass scrollevents to the inner scrollviewer. This ensures you see the whole scrollviewer first, before you start scrolling.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace CleverScroller.Helper
{
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
protected override void OnDetaching()
{
this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta < 0)
{
var outerscroller = GetVisualParent<ScrollViewer>(this.AssociatedObject);
if (outerscroller.ContentVerticalOffset < outerscroller.ScrollableHeight)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
else
{
var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
var scrollPos = scrollViewer.ContentVerticalOffset;
if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
|| (scrollPos == 0 && e.Delta > 0))
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
private static T GetVisualParent<T>(DependencyObject parent) where T : Visual
{
T obj = default(T);
Visual v = (Visual)VisualTreeHelper.GetParent(parent);
do
{
v = (Visual)VisualTreeHelper.GetParent(v);
obj = v as T;
} while (obj == null);
return obj;
}
}
}