Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WPF RepeatButton fires only once on TouchUp when hosted in ListBox #8223

Open
cmjthomas opened this issue Sep 15, 2023 · 2 comments
Open

WPF RepeatButton fires only once on TouchUp when hosted in ListBox #8223

cmjthomas opened this issue Sep 15, 2023 · 2 comments
Labels
Investigate Requires further investigation by the WPF team.

Comments

@cmjthomas
Copy link

Description

Using a touchscreen, the RepeatButton fails to raise repeated Click events when the button is hosted inside a ListBox or ListView.

Similarly, the grip of a Slider (when hosted in a ListBox) cannot be grabbed by touching the screen.

Both Slider and RepeatButton work as expected when interacted with a mouse.

Reproduction Steps

ListBoxRepeatButtonBug.zip

Expected behavior

RepeatButton raises repeated click events when touched/depressed.
Should be able to grab/slide a Slider control

Actual behavior

RepeatButton raises a Click event only once on TouchUp.
Cannot slide a Slider

Regression?

No. Repro'd on other .NET versions

Known Workarounds

None known

Impact

Affects any WPF application where the user interacts with a ListBox using a touchscreen

Configuration

  • .NET 6.0
  • Win10 Pro 10.0.19045 Build 19045
  • Arch: x86/64
  • Does the bug repro in Framework 4.8: Yes
  • Is this bug related specifically to tooling in Visual Studio (e.g. XAML Designer, Code editing, etc...)? No
  • Specific to .NET 6: No. Also repro'd on Framework 4.8, Core 3.1, NET 7.

Other information

The attached repro was inspired by a similar bug report #4512

That particular bug was observed in Core 3.1 but is no longer a problem on NET6.
I have tested that and can confirm that I also see that as fixed since NET6.

During my investigation for this new bug, I tried disabling the Touch API using two methods:

This makes the touchscreen behave instead like a mouse touchpad.
The repeat Click event of the RepeatButton will be raised as expected in this scenario, but to initiate the ButtonDown/depressed state the user must double-tap the screen so this is not a viable solution.

On further investigation, I started monitoring the TouchDown/Up/Move/Enter/Leave events of the RepeatButton.
When the button works I see the following event sequence:

  • TouchEnter
  • TouchDown
  • Click (n times)

Then when I release

  • TouchUp
  • TouchLeave

When the RepeatButton doesn't work (ie is inside a ListBox), I see the following sequence:

  • TouchEnter
  • TouchDown
  • TouchLeave

When I release

  • Click (once)
  • TouchEnter
  • TouchLeave

There is a spurious TouchLeave event while the button is depressed which seems to inhibit any further Click events.

@singhashish-wpf singhashish-wpf added the Investigate Requires further investigation by the WPF team. label Sep 15, 2023
lindexi added a commit to lindexi/lindexi_gd that referenced this issue Jan 30, 2024
@lindexi
Copy link
Contributor

lindexi commented Jan 30, 2024

@cmjthomas Sorry for delay. From the call stack, it’s evident that touch behavior is influenced by Manipulation. Due to the complexity of handling Manipulation logic for touch, unlike a mouse, WPF can only intercept touch in the ManipulationDevice to prevent touch from escalating to mouse and affecting normal logic.

>	ListBoxRepeatButtonBug.dll!ListBoxRepeatButtonBug.MainWindow.ListBoxRepeatButtonClick(object sender = {System.Windows.Controls.Primitives.RepeatButton}, System.Windows.RoutedEventArgs e = {System.Windows.RoutedEventArgs}) 
 	PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs)
 	PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source = {System.Windows.Controls.Primitives.RepeatButton}, System.Windows.RoutedEventArgs args = {System.Windows.RoutedEventArgs}, bool reRaised = false)
 	PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender = {System.Windows.Controls.Primitives.RepeatButton}, System.Windows.RoutedEventArgs args = {System.Windows.RoutedEventArgs})
 	PresentationCore.dll!System.Windows.UIElement.RaiseEvent(System.Windows.RoutedEventArgs e)
 	PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnClick()
 	PresentationFramework.dll!System.Windows.Controls.Primitives.RepeatButton.OnClick()
 	PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e = {System.Windows.Input.MouseButtonEventArgs})
 	PresentationFramework.dll!System.Windows.Controls.Primitives.RepeatButton.OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e)
 	PresentationCore.dll!System.Windows.UIElement.OnMouseLeftButtonDownThunk(object sender, System.Windows.Input.MouseButtonEventArgs e)
 	PresentationCore.dll!System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(System.Delegate genericHandler, object genericTarget)
 	PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target)
 	PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs)
 	PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source = {System.Windows.Controls.Primitives.RepeatButton}, System.Windows.RoutedEventArgs args = {System.Windows.Input.MouseButtonEventArgs}, bool reRaised = true)
 	PresentationCore.dll!System.Windows.UIElement.ReRaiseEventAs(System.Windows.DependencyObject sender = {System.Windows.Controls.Primitives.RepeatButton}, System.Windows.RoutedEventArgs args = {System.Windows.Input.MouseButtonEventArgs}, System.Windows.RoutedEvent newEvent)
 	PresentationCore.dll!System.Windows.UIElement.OnMouseDownThunk(object sender, System.Windows.Input.MouseButtonEventArgs e)
 	PresentationCore.dll!System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(System.Delegate genericHandler, object genericTarget)
 	PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target)
 	PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs)
 	PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source = {System.Windows.Controls.Border}, System.Windows.RoutedEventArgs args = {System.Windows.Input.MouseButtonEventArgs}, bool reRaised = false)
 	PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender = {System.Windows.Controls.Border}, System.Windows.RoutedEventArgs args = {System.Windows.Input.MouseButtonEventArgs})
 	PresentationCore.dll!System.Windows.UIElement.RaiseTrustedEvent(System.Windows.RoutedEventArgs args = {System.Windows.Input.MouseButtonEventArgs})
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispStylusDevice.PlayBackCachedDownInputReport(int timestamp)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PromoteStoredItemsToMouse(System.Windows.Input.StylusWisp.WispStylusTouchDevice touchDevice = {System.Windows.Input.StylusWisp.WispStylusTouchDevice})
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispStylusTouchDevice.OnManipulationEnded(bool cancel)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.System.Windows.Input.IManipulator.ManipulationEnded(bool cancel)
 	PresentationCore.dll!System.Windows.Input.ManipulationDevice.OnManipulationCancel()
 	PresentationCore.dll!System.Windows.Input.ManipulationDevice.PostProcessInput(object sender, System.Windows.Input.ProcessInputEventArgs e)
 	PresentationCore.dll!System.Windows.Input.InputManager.RaiseProcessInputEventHandlers(System.Tuple<System.Windows.Input.ProcessInputEventHandler, System.Delegate[]> postProcessInput, System.Windows.Input.ProcessInputEventArgs processInputEventArgs = {System.Windows.Input.ProcessInputEventArgs})
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input)
 	PresentationCore.dll!System.Windows.Input.ManipulationLogic.ReportFrame(System.Collections.Generic.ICollection<System.Windows.Input.IManipulator> manipulators)
 	PresentationCore.dll!System.Windows.Input.ManipulationDevice.ReportFrame()
 	PresentationCore.dll!System.Windows.Input.ManipulationDevice.RemoveManipulator(System.Windows.Input.IManipulator manipulator)
 	PresentationCore.dll!System.Windows.Input.Manipulation.TryRemoveManipulator(System.Windows.UIElement element, System.Windows.Input.IManipulator manipulator)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.PromoteMainToManipulation(System.Windows.UIElement manipulatableElement, System.Windows.Input.TouchEventArgs touchEventArgs)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.PostProcessInput(object sender, System.Windows.Input.ProcessInputEventArgs e)
 	PresentationCore.dll!System.Windows.Input.InputManager.RaiseProcessInputEventHandlers(System.Tuple<System.Windows.Input.ProcessInputEventHandler, System.Delegate[]> postProcessInput, System.Windows.Input.ProcessInputEventArgs processInputEventArgs = {System.Windows.Input.ProcessInputEventArgs})
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.RaiseLostCapture(System.Windows.IInputElement oldCapture)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.Capture(System.Windows.IInputElement element = null, System.Windows.Input.CaptureMode captureMode = None)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.PromoteMainToManipulation(System.Windows.UIElement manipulatableElement, System.Windows.Input.TouchEventArgs touchEventArgs)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.PostProcessInput(object sender, System.Windows.Input.ProcessInputEventArgs e)
 	PresentationCore.dll!System.Windows.Input.InputManager.RaiseProcessInputEventHandlers(System.Tuple<System.Windows.Input.ProcessInputEventHandler, System.Delegate[]> postProcessInput, System.Windows.Input.ProcessInputEventArgs processInputEventArgs = {System.Windows.Input.ProcessInputEventArgs})
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input)
 	PresentationCore.dll!System.Windows.Input.TouchDevice.RaiseTouchUp()
 	PresentationCore.dll!System.Windows.Input.TouchDevice.ReportUp()
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PromoteMainUpToTouch(System.Windows.Input.StylusWisp.WispStylusDevice stylusDevice, System.Windows.Input.StagingAreaInputItem stagingItem = {System.Windows.Input.StagingAreaInputItem})
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PromoteMainToTouch(System.Windows.Input.ProcessInputEventArgs e, System.Windows.Input.StylusEventArgs stylusEventArgs)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PromoteMainToOther(System.Windows.Input.ProcessInputEventArgs e)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PostProcessInput(object sender, System.Windows.Input.ProcessInputEventArgs e = {System.Windows.Input.ProcessInputEventArgs})
 	PresentationCore.dll!System.Windows.Input.InputManager.RaiseProcessInputEventHandlers(System.Tuple<System.Windows.Input.ProcessInputEventHandler, System.Delegate[]> postProcessInput, System.Windows.Input.ProcessInputEventArgs processInputEventArgs = {System.Windows.Input.ProcessInputEventArgs})
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.InputManagerProcessInput(object oInput)
 	WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs)
 	WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(object source = {System.Windows.Threading.Dispatcher}, System.Delegate callback, object args, int numArgs, System.Delegate catchHandler = null)
 	WindowsBase.dll!System.Windows.Threading.DispatcherOperation.InvokeImpl()
 	WindowsBase.dll!System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(object state)
 	WindowsBase.dll!MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(object obj)
 	System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
 	System.Private.CoreLib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
 	WindowsBase.dll!MS.Internal.CulturePreservingExecutionContext.Run(MS.Internal.CulturePreservingExecutionContext executionContext = {MS.Internal.CulturePreservingExecutionContext}, System.Threading.ContextCallback callback, object state)
 	WindowsBase.dll!System.Windows.Threading.DispatcherOperation.Invoke()
 	WindowsBase.dll!System.Windows.Threading.Dispatcher.ProcessQueue()
 	WindowsBase.dll!System.Windows.Threading.Dispatcher.WndProcHook(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled)
 	WindowsBase.dll!MS.Win32.HwndWrapper.WndProc(System.IntPtr hwnd = 0x000000002431305e, int msg, System.IntPtr wParam = 0x0000000000000001, System.IntPtr lParam = 0x0000000000000000, ref bool handled = false)
 	WindowsBase.dll!MS.Win32.HwndSubclass.DispatcherCallbackOperation(object o)
 	WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs)
 	WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(object source = {System.Windows.Threading.Dispatcher}, System.Delegate callback, object args, int numArgs, System.Delegate catchHandler = null)
 	WindowsBase.dll!System.Windows.Threading.Dispatcher.LegacyInvokeImpl(System.Windows.Threading.DispatcherPriority priority, System.TimeSpan timeout, System.Delegate method, object args, int numArgs)
 	WindowsBase.dll!MS.Win32.HwndSubclass.SubclassWndProc(System.IntPtr hwnd = 0x000000002431305e, int msg, System.IntPtr wParam = 0x0000000000000001, System.IntPtr lParam = 0x0000000000000000)
 	WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame frame = {System.Windows.Threading.DispatcherFrame})
 	WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame frame)
 	WindowsBase.dll!System.Windows.Threading.Dispatcher.Run()
 	PresentationFramework.dll!System.Windows.Application.RunDispatcher(object ignore)
 	PresentationFramework.dll!System.Windows.Application.RunInternal(System.Windows.Window window)
 	PresentationFramework.dll!System.Windows.Application.Run()
 	ListBoxRepeatButtonBug.dll!ListBoxRepeatButtonBug.App.Main()

In the demo, the RepeatButton is placed inside a ListBox, and the default style of ListBox includes a ScrollViewer object. This ScrollViewer object sets the IsManipulationEnabled property to true. This results in the touch received by the RepeatButton inside the ListBox being consumed by Manipulation, preventing the continuous triggering of Click events. How to prove this? Try setting the IsManipulationEnabled property of the ListBox’s ScrollViewer to false, and you will find that the RepeatButton can continuously trigger Click events under touch. The specific implementation code can be found in my demo application. Additionally, I have read the source code of WPF and found that this issue may be difficult to fix. I apologize that I can only tell you the cause of this issue, and I may not be able to help you fix it. If there is anything you don’t understand, feel free to discuss.

@lindexi
Copy link
Contributor

lindexi commented Jan 30, 2024

And you will find that after setting the IsManipulationEnabled property of RepeatButton to true, no matter where the RepeatButton is placed, it can reproduce the behavior you mentioned of placing the RepeatButton inside a ListBox.

<RepeatButton IsManipulationEnabled="True" Height="100" Click="StackPanelRepeatButtonClick" Content="Long touch 'repeat' works on this RepeatButton"/>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Investigate Requires further investigation by the WPF team.
Projects
None yet
Development

No branches or pull requests

3 participants