dotnet / winforms

Windows Forms is a .NET UI framework for building Windows desktop applications.
MIT License
4.41k stars 982 forks source link

KeyboardToolTip created for a DataGridView with ShowCellTooltips enabled can crash in HideAllToolTips #11837

Closed AndrewDavidLees1974 closed 2 months ago

AndrewDavidLees1974 commented 3 months ago

.NET version

.NET 8

Did it work in .NET Framework?

Yes

Did it work in any of the earlier releases of .NET Core or .NET 5+?

.NET 6 had other cell tooltip issues which resulted in us disabling cell tooltips. We found the behaviour had improved in .NET8 so we enabled them again.

.NET Framework did not have this issue, but .NET 8 has added these lines to OnCurrentCellChanged:

if (CurrentCell is not null && (ShowCellToolTips || (ShowCellErrors && !string.IsNullOrEmpty(CurrentCell?.ErrorText)))) { ActivateToolTip(false /activate/, string.Empty, CurrentCell.ColumnIndex, CurrentCell.RowIndex); KeyboardToolTipStateMachine.Instance.NotifyAboutGotFocus(CurrentCell); }

Which always shows a cell tooltip now when navigating the grid using arrow keys (not really sure why that is necessary though as cell tool tips before were only shown for truncated content?)

Issue description

When ShowCellToopTips is true for a DataGridView and the user navigates with arrow keys, the grid creates a KeyboardToolTip in the base Control PropertiesStore. This Tooltip is never disposed which leads to it remaining attached to the TopLevelForm. If the data grid is disposed while the cell tooltip is showing (e.g. removing a tab page that contains a data grid) and the top level form receives a WmActivate at the same time then the call to BaseFormDeactivate will crash in HideAllTooltips because it can't get the handle of the disposed grid.

The following in Tooltip Dispose code is also slightly odd:

            // Unhook the DeactivateEvent. Find the Form for associated Control and hook
            // up to the Deactivated event to Hide the Shown tooltip
            if (TopLevelControl is Form baseFrom)
            {
                baseFrom.Deactivate -= BaseFormDeactivate;
            }

Since RemoveAll called further up will set _topLevelControl to null and clear _tools so the call to TopLevelControl will always return null at this point. TopLevelControl is correct before the call to RemoveAll so perhaps its value should be cached or Tooltip detached from Deactivate earlier in the Dispose method?

Steps to reproduce

Dispose a data grid view that has an active keyboard cell tooltip in such a way as to force a WmActivate while the tooltip is still showing.

Disposing of the KeyboardToolTip in the data grid dispose method fixes this issue. I achieve this by calling the following code in my (sub classed) data grid before calling the base Dispose method:

private static readonly PropertyInfo KeyboardToolTipPropertyInfo = typeof(DataGridView).GetProperty("KeyboardToolTip", BindingFlags.Instance | BindingFlags.NonPublic);

    if (KeyboardToolTipPropertyInfo.GetValue(dataGridView) is ToolTip keyboardToolTip)
    {
        keyboardToolTip.Dispose();
    }
JeremyKuhne commented 3 months ago

@AndrewDavidLees1974 can you include the call stack so we can make sure we're fully aligned here?

AndrewDavidLees1974 commented 3 months ago

When the crash occurs it is after a sequence of rows have been deleted in the data grid view using the arrow keys to navigate to the next row after pressing Delete.

We found two different examples of crashes:

The first crash we found was:

at System.ThrowHelper.ThrowObjectDisposedException(Object instance) at System.Windows.Forms.Control.CreateHandle() at System.Windows.Forms.Control.get_Handle() at System.Windows.Forms.Control.IHandle.get_Handle() at HandleRef1..ctor(IHandle1 handle) at System.Windows.Forms.Control.GetSafeHandle(IWin32Window window) at System.Windows.Forms.ToolTip.Hide(IWin32Window win) at System.Windows.Forms.ToolTip.HideAllToolTips() at System.Windows.Forms.ToolTip.BaseFormDeactivate(Object sender, EventArgs e) at DevComponents.DotNetBar.RibbonForm.OnDeactivate(EventArgs e) at System.Windows.Forms.Form.WmActivate(Message& m) at System.Windows.Forms.Form.WndProc(Message& m) at DevComponents.DotNetBar.RibbonForm.WndProc(Message& m) at Emb.PricingSuite.RateAssessor.MainForm.WndProc(Message& m) in C:\Work\Radar2\EMB.PricingSuite.RateAssessor\src\implementation\MainForm.cs:line 5782

This appears to happen if the data grid is showing a tooltip for the last deleted row before you dispose the data grid view.

This is a different code path that appears to happen if the data grid row is still showing the tooltip from the penultimate row deleted before disposing the data grid view.

Exception of type System.ObjectDisposedException Cannot access a disposed object. Object name: 'Emb.Components.UI.Utilities.TriSortModelElementView'.

System.Private.CoreLib.dll!System.ThrowHelper.ThrowObjectDisposedException(object instance) Line 403 at //src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs(403) System.Windows.Forms.dll!System.Windows.Forms.Control.CreateHandle() Line 4843 at //src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(4843) System.Windows.Forms.dll!System.Windows.Forms.Control.Handle.get() Line 2433 at //src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(2433) System.Windows.Forms.dll!System.Windows.Forms.Control.IHandle.Handle.get() Line 13591 at //src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(13591) System.Windows.Forms.Primitives.dll!HandleRef.HandleRef(IHandle handle) Line 23 at -\HandleRef.cs(23) System.Windows.Forms.dll!System.Windows.Forms.Control.GetSafeHandle(System.Windows.Forms.IWin32Window window) Line 6125 at //src/System.Windows.Forms/src/System/Windows/Forms/Control.cs(6125) System.Windows.Forms.dll!System.Windows.Forms.ToolTip.Hide(System.Windows.Forms.IWin32Window win) Line 1778 at //src/System.Windows.Forms/src/System/Windows/Forms/ToolTip.cs(1778) System.Windows.Forms.dll!System.Windows.Forms.Timer.TimerNativeWindow.WndProc(ref System.Windows.Forms.Message m) Line 346 at //src/System.Windows.Forms/src/System/Windows/Forms/Timer.cs(346) System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Callback(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.MessageId msg, Windows.Win32.Foundation.WPARAM wparam, Windows.Win32.Foundation.LPARAM lparam) Line 372 at //src/System.Windows.Forms/src/System/Windows/Forms/NativeWindow.cs(372) [Native to Managed Transition] [Managed to Native Transition] System.Windows.Forms.Primitives.dll!Windows.Win32.PInvoke.DispatchMessage(Windows.Win32.UI.WindowsAndMessaging.MSG lpMsg) System.Windows.Forms.dll!System.Windows.Forms.Application.ComponentManager.Microsoft.Office.IMsoComponentManager.FPushMessageLoop(nuint dwComponentID, Microsoft.Office.msoloop uReason, void pvLoopData) Line 290 at //src/System.Windows.Forms/src/System/Windows/Forms/Application.ComponentManager.cs(290) System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Microsoft.Office.msoloop reason, System.Windows.Forms.ApplicationContext context) Line 1063 at //src/System.Windows.Forms/src/System/Windows/Forms/Application.ThreadContext.cs(1063) System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Microsoft.Office.msoloop reason, System.Windows.Forms.ApplicationContext context) Line 929 at /_/src/System.Windows.Forms/src/System/Windows/Forms/Application.ThreadContext.cs(929) Microsoft.VisualBasic.Forms.dll!Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.OnRun() Microsoft.VisualBasic.Forms.dll!Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.DoApplicationModel() Microsoft.VisualBasic.Forms.dll!Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.Run(string[] commandLine) Radar.dll!Emb.PricingSuite.RateAssessor.Program.Main(string[] args) Line 34 at C:\Work\Radar8\EMB.PricingSuite.RateAssessor\src\implementation\Program.cs(34)

(TriSortModelElementGrid is a sub class of DataGridView)

MelonWang1 commented 2 months ago

Hi, @AndrewDavidLees1974 could you please provide a sample application to repro this issue?