From 6f4e872c032604cf64a4043c71f479b4fe01b66f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 28 Oct 2026 10:37:47 -0400 Subject: [PATCH] fix(ui): stop a grid's keyboard tooltip before the grid is disposed Browsing nests in Database mode crashed with ObjectDisposedException on DataGridView: selecting another nest drops the embedded version history, and WinForms leaves its grid's keyboard-tooltip auto-pop timer running. When the timer fired it hid the tooltip through the disposed grid. GridStyle now hides that tooltip when a styled grid's window is destroyed, which stops the timer for every grid in the app. Co-Authored-By: Claude Opus 5.5 --- .../Controls/GridStyleTests.cs | 28 +++++++++++++++++++ OpenNest/Controls/GridStyle.cs | 22 ++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/OpenNest.WinForms.Tests/Controls/GridStyleTests.cs b/OpenNest.WinForms.Tests/Controls/GridStyleTests.cs index 53b3c0c9..b60f94b2 100644 --- a/OpenNest.WinForms.Tests/Controls/GridStyleTests.cs +++ b/OpenNest.WinForms.Tests/Controls/GridStyleTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Windows.Forms; using OpenNest.Controls; @@ -79,6 +80,33 @@ public class GridStyleTests Assert.Equal(column.Width, firstPaintWidth()); }); + [Fact] + public void DisposingAGridStopsItsShowingKeyboardToolTip() => ArchUnitsTests.RunSta(() => + { + // WinForms leaves the keyboard tooltip's auto-pop timer running after the grid is + // disposed; when it fires it hides the tooltip through the disposed grid and throws. + const BindingFlags members = BindingFlags.Instance | BindingFlags.NonPublic; + using var form = new Form { Width = 600, Height = 300, ShowInTaskbar = false }; + var grid = GridStyle.Apply(new DataGridView { Dock = DockStyle.Fill, AllowUserToAddRows = false }); + grid.Columns.Add("Version", "Version"); + grid.Rows.Add("2"); + form.Controls.Add(grid); + form.Show(); + Pump(grid); + + // As when the keyboard moves to a cell in the embedded version history. + var toolTip = (ToolTip)typeof(DataGridView).GetProperty("KeyboardToolTip", members).GetValue(grid); + typeof(ToolTip).GetMethod("ShowKeyboardToolTip", members) + .Invoke(toolTip, new object[] { "2", grid.Rows[0].Cells[0], 3000 }); + var timer = typeof(ToolTip).GetField("_timer", members); + Assert.NotNull(timer.GetValue(toolTip)); + + form.Controls.Remove(grid); + grid.Dispose(); + + Assert.Null(timer.GetValue(toolTip)); + }); + private static Func WidthAtFirstPaint(DataGridView grid, DataGridViewColumn column) { var width = -1; diff --git a/OpenNest/Controls/GridStyle.cs b/OpenNest/Controls/GridStyle.cs index 9ea58a84..9a11ca36 100644 --- a/OpenNest/Controls/GridStyle.cs +++ b/OpenNest/Controls/GridStyle.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Drawing; +using System.Reflection; using System.Windows.Forms; namespace OpenNest.Controls; @@ -49,9 +50,28 @@ public static class GridStyle grid.RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; if (sizeColumns) SizeColumnsOnceToContent(grid); + grid.HandleDestroyed += HideToolTipsOnDestroy; return grid; } + private static readonly PropertyInfo KeyboardToolTipProperty = typeof(DataGridView).GetProperty( + "KeyboardToolTip", BindingFlags.Instance | BindingFlags.NonPublic); + + /// + /// Hides the grid's keyboard tooltip while the grid's window still exists. WinForms does not + /// stop that tooltip's auto-pop timer when the grid is disposed, and the timer later hides the + /// tooltip through the disposed grid, which throws (as when the database browser drops its + /// embedded version history). Hiding it stops the timer. The tooltip is internal to WinForms, + /// so a runtime without it is left as it is. + /// + private static void HideToolTipsOnDestroy(object sender, EventArgs e) + { + var grid = (DataGridView)sender; + if (grid.RecreatingHandle || KeyboardToolTipProperty?.GetValue(grid) is not ToolTip toolTip) + return; + toolTip.Hide(grid); + } + /// /// Widens each column to fit its header and its first rows, once, then leaves the widths /// to the user. Columns never stretch to fill the grid.