wpf - Burbujeando eventos de desplazamiento de un ListView a su padre
scroll event-bubbling (7)
En mi aplicación WPF tengo un ListView
cuya ScrollViewer.VerticalScrollBarVisibility
está establecida en Disabled
. Está contenido dentro de un ScrollViewer
. Cuando intento utilizar la rueda del mouse sobre ListView
, el ScrollViewer
externo no se desplaza porque ListView
está capturando los eventos de desplazamiento.
¿Cómo puedo forzar el ListView
para permitir que los eventos de desplazamiento aparezcan en ScrollViewer
?
Debe capturar el evento de rueda de mouse de vista previa en la vista de lista interna
ctl.PreviewMouseWheel += PreviewMouseWheel;
luego detenga el evento para que no se desplace la vista de lista y suba el evento en la vista de lista principal.
private static void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!e.Handled)
{
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
eventArg.RoutedEvent = UIElement.MouseWheelEvent;
eventArg.Source = sender;
var parent = ((Control)sender).Parent as UIElement;
parent.RaiseEvent(eventArg);
}
}
Creds van a @ robert-wagner quien resolvió esto para mí hace unos meses.
Existen diferentes enfoques dependiendo de su situación exacta, pero encontré que esto funciona bien. Asumiendo su situación básica es esta:
<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 usted mismo durante PreviewMouseWheel parece forzar el ScrollViewer para que funcione. Desearía saber por qué, parece muy contrario a la intuición.
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);
}
Mi caso de uso fue ligeramente diferente. Tengo un scrollviewer muy grande y en la parte inferior otro scrollviewer que tiene un maxheight de 600. Quiero desplazar toda la página hacia abajo hasta que pase scrollevents al scrollviewer interno. Esto le asegura que primero verá el scrollviewer completo, antes de comenzar a desplazarse.
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;
}
}
}
Ok, ha pasado un tiempo desde que estuve en SO, pero tuve que comentar sobre esto. Todos los túneles de eventos de vista previa, entonces, ¿por qué estamos burbujeando? Detenga el túnel en el padre y termine con él. en el padre agregue un evento PreviewMouseWheel.
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;
}
Otra buena solución que usa comportamiento adjunto. Me gusta porque decora la solución del Control.
Cree un comportamiento sin desplazamiento que capture el evento PreviewMouseWheel (Tunneling) y levante un nuevo MouseWheelEvent (Bubbling)
public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{
protected override void OnAttached( )
{
base.OnAttached( );
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel ;
}
protected override void OnDetaching( )
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching( );
}
void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice,e.Timestamp,e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
A continuación, adjunte el comportamiento a cualquier UIElement con el caso ScrollViewers anidado
<ListBox Name="ForwardScrolling">
<i:Interaction.Behaviors>
<local:IgnoreMouseWheelBehavior />
</i:Interaction.Behaviors>
</ListBox>
todo el crédito al blog de Josh Einstein
Si vienes aquí buscando una solución para hacer burbujas en el evento SOLAMENTE si el niño está en la parte superior y desplazándose hacia arriba o hacia abajo y desplazándose hacia abajo, aquí hay una solución. Solo probé esto con DataGrid, pero también debería funcionar con otros controles.
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;
}
}
Para adjuntar este comportamiento, agregue los siguientes XMLNS y XAML a su elemento:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<i:Interaction.Behaviors>
<shared:ScrollParentWhenAtMax />
</i:Interaction.Behaviors>
También puede lograr lo mismo utilizando un comportamiento adjunto. Esto tiene la ventaja de no necesitar la biblioteca System.Windows.Interactivity. La lógica se ha tomado de las otras respuestas, solo la implementación es diferente.
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);
}
}
}
Y luego en el 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>