dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.08k stars 1.17k forks source link

Touch bug/issue with ShowDialog and TextBox #2491

Open vsfeedback opened 4 years ago

vsfeedback commented 4 years ago

This issue has been moved from a ticket on Developer Community.


Hello I'm developing a WPF application to be used with touch devices (Running Win 10 full).

There is one scenario where the touch stops responding as expected.
If I have a TextBox and open a dialog (ShowDialog()) then the dialog won't handle any touch and will ignore the user:

A) 1 touch ignored - if using single touch driver.

B) 10 touches ignored - if using multi touch driver.

And after that it starts responding normally again.

I have attached a video show casing this issue and also a test project so you can easily replicate (using Windows Simulator w/ touch or a touch monitor)
In the video you can see that when opening the dialog through a button it responds as expected but if opened through a TextBox it will then have a touch issue.

textbox-showdialog-touch-issue.mp4

ms-wpf-testtouchdialogs.zip

The code being used is just this:

        private void TextBox_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            // This cause touch issues within the dialog.
            // (1 touch ignored w/ single touch monitor/driver - 10 touches ignored w/ multi touch monitor/driver)
            new DialogWindow(). ShowDialog();
        }

private void Button_Click(object sender, RoutedEventArgs e)
        {
            new DialogWindow(). ShowDialog();
        }

Windows 10
VS 2019
.NET 4.6.1 (but it was also tested with 4.8, exactly same issue)


Original Comments

Visual Studio Feedback System on 1/20/2020, 06:41 PM:

We have directed your feedback to the appropriate engineering team for further evaluation. The team will review the feedback and notify you about the next steps.

Visual Studio Feedback System on 1/30/2020, 01:12 PM:

This issue is currently being investigated. Our team will get back to you if either more information is needed, a workaround is available, or the issue is resolved.


Original Solutions

(no solutions)

qtbui159 commented 4 years ago

This is an irritated and old bug which exists from .net framework 3.5. All first touch of non-button popup window(modal)would be ignored

wcfCode commented 4 years ago

Hope the problem gets attention.

wcfCode commented 4 years ago

Is there a temporary solution?

qtbui159 commented 4 years ago

Is there a temporary solution?

你所有需要弹窗的地方都用button来做就行

MitSeven commented 4 years ago

I have the same error. I wonder why it takes 10 touches. I am currently doing a temporary fix using Dispatcher.BeginInvoke or DispatcherInvokeAsync

Mzazvor commented 3 years ago

Hi, @MitSeven Could you elaborate on your temporary fix? I had spend few hours looking for progress in this problem and there seems to be none. We do unfortunately have those same 10touches on our showDialog window.

Thanx for any advice Martin

Mzazvor commented 3 years ago

For anyone after me... I managed to get it somewhat working with the use of StylusDownEvent.

private void Button_StylusDown(object sender, System.Windows.Input.StylusDownEventArgs e) { var vm = (DressingPopupViewModel)this.DataContext; Button _myButton = (Button)sender; string value = _myButton.CommandParameter.ToString(); if (vm.CanDoCommand(value)) { vm.DoCommand(value); } e.Handled = true; }

This method handles touches, where Binding to command of VM handles mouseClicks with no problem. It is probably not a clean solution but it is a few lines worth trying in your problem.

GuangSunshine commented 3 years ago

I had similar issue on a Button in my code, and the fix is rather simple for me -- do NOT handle the Touch and PreviewMouse events, just let the touch be promoted to Click event and do things there: <Button Click="btnClick">. If you requirement allows you to do that in your case, that should be the solution. If you can't wait till the Click event, please read on:

My analysis of this problem:

  1. When a Button is being touched, WPF use one instance of some handler (let's call it touchHandler1 of type FingerTouchEventHandler) to handle the whole tunneling and bubbling events from PreviewTouchDown until the final TouchUp. If in the PreviewTouchUp or even TouchUp event handler, you show a new dialog or a MessageBox, the focus is switched to the new dialog, the touchHandler1 on the Button gets stuck, it does not get handled completely.

  2. If now you touch again, the WPF sees there is already one active FingerTouchEventHandler -- touchHandler1 and it is still active, so it wrongly thinks the user is putting a second finger on the screen, WPF proudly thinks "I can handle multi-touch!", so it creates touchHandler2, but since now there are two fingers on the screen, somehow the "second finger" is also stuck, so nothing happens on the screen;

  3. Same thing happens when the third touch occurs, WPF creates touchHandler3. This goes on and on until the 11th touch happens. WPF only supports 10 multi-touches (because human beings have 10 fingers), now it treat this 11th touch as part of the tunneling and bubbling sequence of touchHandler1, and completes touchHandler1. Subsequent touches complete the touchHandler2 etc, now everything is back to normal again.

The above points 1, 2, 3 are my guess, they matches my observation well, then the solution could now also be obvious: in your TextBox_PreviewMouseDown(), BEFORE calling new DialogWindow(). ShowDialog(); just say e.Handled = true; I tried this solution in my code, it works for me. Please give it a try and let us know the result. Good luck!

lasharn commented 2 years ago

BEFORE calling new DialogWindow(). ShowDialog(); just say e.Handled = true;

This didn't work for me although your explanation for the underlying issue does make sense to me.

I did try what @MitSeven suggested and call ShowDialog() within a Dispatcher.BeginInvoke call and that did work for me.

This makes sense with your explanation since queuing the dialog call won't let the button get stuck during the event handler and therefore no extra touch handlers will be created.

marcauto commented 2 years ago

Is this a problem only with Windows 10 or is a problem also in the other Windows (7, p.e)?

GuangSunshine commented 2 years ago

Only tried on Windows 10, do not know about other Windows.

marcauto commented 2 years ago

for the ones that don't need the multi touch functionality, is possible to deactivate the multi touch driver?

heathleger commented 2 years ago

Is there a temporary solution?

你所有需要弹窗的地方都用button来做就行 不懂你说的意思, new Button(). ShowDialog()??

heathleger commented 2 years ago

I have the same error. I wonder why it takes 10 touches. I am currently doing a temporary fix using Dispatcher.BeginInvoke or DispatcherInvokeAsync

could you provide a snipshot?

Mzazvor commented 2 years ago

My solution to this problem is currently to call this method on startup.

public static void DisableWPFTabletSupport(){
    // Get a collection of the tablet devices for this window.
    TabletDeviceCollection devices = Tablet.TabletDevices;
    if (devices.Count > 0)
    {
        // Get the Type of InputManager.  
        Type inputManagerType = typeof(InputManager);

        // Call the StylusLogic method on the InputManager.Current instance.  
        object stylusLogic = inputManagerType.InvokeMember("StylusLogic",
                                BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                                null, InputManager.Current, null);

        if (stylusLogic != null)
        {
            //  Get the type of the stylusLogic returned from the call to StylusLogic.  
            Type stylusLogicType = stylusLogic.GetType();

            // Loop until there are no more devices to remove.  
            while (devices.Count > 0)
            {
                // Remove the first tablet device in the devices collection.  
                stylusLogicType.InvokeMember("OnTabletRemoved",
                                BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic,
                                null, stylusLogic, new object[] { (uint)0 });
            }
        }
    }
}
MitSeven commented 2 years ago

My application is the touch keyboard. This is my temporary solution, I only use Dispatcher.begininvoke Code:

  private void button_Click(object sender, InputEventArgs e)
        {
            if (handled || !(sender is Button bt)) return;
            handled = true;
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,new Action(async () =>
            {
                AddKey(bt); //Processing click action
                await Task.Delay(1);
                handled = false;
            }));
            e.Handled = true;
        }

Including attributes:

 private static readonly object _locker = new object();
        private static bool _handled;
        public static bool handled
        {
            get
            {
                lock (_locker)  { return _handled;}
            }
            set
            {
                lock (_locker) {  _handled = value;}
            }
        }

XAML: <Button Grid.Column="1" PreviewMouseDown="button_Click" Content="a" />

madsrs commented 2 years ago

My solution to this problem is currently to call this method on startup.

public static void DisableWPFTabletSupport(){
  // Get a collection of the tablet devices for this window.
  TabletDeviceCollection devices = Tablet.TabletDevices;
  if (devices.Count > 0)
  {
      // Get the Type of InputManager.  
      Type inputManagerType = typeof(InputManager);

      // Call the StylusLogic method on the InputManager.Current instance.  
      object stylusLogic = inputManagerType.InvokeMember("StylusLogic",
                              BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                              null, InputManager.Current, null);

      if (stylusLogic != null)
      {
          //  Get the type of the stylusLogic returned from the call to StylusLogic.  
          Type stylusLogicType = stylusLogic.GetType();

          // Loop until there are no more devices to remove.  
          while (devices.Count > 0)
          {
              // Remove the first tablet device in the devices collection.  
              stylusLogicType.InvokeMember("OnTabletRemoved",
                              BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic,
                              null, stylusLogic, new object[] { (uint)0 });
          }
      }
  }
}

This fixed my issue completly, and touch input has never been better in my application - thank you so much.

qtbui159 commented 2 years ago

Is there a temporary solution?

你所有需要弹窗的地方都用button来做就行 不懂你说的意思, new Button(). ShowDialog()??

在button的command中或click事件中执行ShowDialog。

isasin commented 2 years ago

public static void DisableWPFTabletSupport(){ // Get a collection of the tablet devices for this window. TabletDeviceCollection devices = Tablet.TabletDevices; if (devices.Count > 0) { // Get the Type of InputManager.
Type inputManagerType = typeof(InputManager);

    // Call the StylusLogic method on the InputManager.Current instance.  
    object stylusLogic = inputManagerType.InvokeMember("StylusLogic",
                            BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                            null, InputManager.Current, null);

    if (stylusLogic != null)
    {
        //  Get the type of the stylusLogic returned from the call to StylusLogic.  
        Type stylusLogicType = stylusLogic.GetType();

        // Loop until there are no more devices to remove.  
        while (devices.Count > 0)
        {
            // Remove the first tablet device in the devices collection.  
            stylusLogicType.InvokeMember("OnTabletRemoved",
                            BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic,
                            null, stylusLogic, new object[] { (uint)0 });
        }
    }
}

}

Hello guys, I have the same problem too that is after touching 10 times it is doing its job. It is really very bad problem and I want to figure this out. My question is that when do we call this DisableWPFTabletSupport method and what it is doing exactly? I just do not want to break some other thing while trying to figure this issue out.

isasin commented 2 years ago

Is there any better ways, they are always welcome by the way

Mzazvor commented 2 years ago

@isasin We are using this solution for little over a year and as far as my knowledge go it will not break anything. How this works is that it adjusts the way events are propagated in WPF. Everything starts as a stylus, then goes to be a touch and ends as a mouse event. Im not aware of any other solution, but since this solved it for me i stoped looking.

isasin commented 2 years ago

I see thanks for the answer I will try. so in that case, lets say I am using mouse and touch at the same time, then which handler should I use after I call this DisableWPFTabletSupport method? If I use mousedown event handler for touch too, does this matter? What do you say?

Mzazvor commented 2 years ago

I see thanks for the answer I will try. so in that case, lets say I am using mouse and touch at the same time, then which handler should I use after I call this DisableWPFTabletSupport method? If I use mousedown event handler for touch too, does this matter? What do you say?

I just call it once on app startup. For clicks i just use regular command binding with mouse event and MVVM structure.

You will probably have to try that. I dont have any device to test it at the moment.

heathleger commented 1 year ago

My solution to this problem is currently to call this method on startup.

public static void DisableWPFTabletSupport(){
  // Get a collection of the tablet devices for this window.
  TabletDeviceCollection devices = Tablet.TabletDevices;
  if (devices.Count > 0)
  {
      // Get the Type of InputManager.  
      Type inputManagerType = typeof(InputManager);

      // Call the StylusLogic method on the InputManager.Current instance.  
      object stylusLogic = inputManagerType.InvokeMember("StylusLogic",
                              BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                              null, InputManager.Current, null);

      if (stylusLogic != null)
      {
          //  Get the type of the stylusLogic returned from the call to StylusLogic.  
          Type stylusLogicType = stylusLogic.GetType();

          // Loop until there are no more devices to remove.  
          while (devices.Count > 0)
          {
              // Remove the first tablet device in the devices collection.  
              stylusLogicType.InvokeMember("OnTabletRemoved",
                              BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic,
                              null, stylusLogic, new object[] { (uint)0 });
          }
      }
  }
}

Thanks, this sample resolve my problem, but i still need the "StylusLogic". I use "Manipulation" to zoom ui element. There is a native smoth animation during user operating, The animation not work afther I remove the "StylusLogic". Please let me know how to fix this.