fix(ui): refresh the drawing list on the UI thread, never from the timer thread

EditNestForm's debounce timer is a System.Timers.Timer whose Elapsed ran
on a pool thread, checked drawingListBox1.IsHandleCreated, then called
drawingListBox1.Invoke. Invoke reads Handle, so a handle destroyed
between the check and the call was re-created on the pool thread (the
cross-thread check is off outside the debugger) while the UI thread was
disposing the window, which threw "Dispose() cannot be called while
doing CreateHandle()". The timer starts enabled at 50 ms, so short
WinForms tests such as LedgerCodeRecheckTests.NoLedgerOrNoCodes_ChecksNothing
hit it intermittently on CI.

Elapsed now posts to the window's UI synchronization context and the
refresh checks IsDisposed there; the timer is disposed with the form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-05 12:55:14 -05:00
1 parent fbe82d198a
commit 46adbfd6f6
2 files changed
+17 -10

No files matched your search

+2
View File
@@ -19,6 +19,8 @@
// The simulator's lock disables the main window until it closes.
if (disposing)
cutSimulatorForm?.Close();
if (disposing)
updateDrawingListTimer?.Dispose();
if (disposing && (components != null))
{
components.Dispose();
+15 -10
View File
@@ -49,6 +49,12 @@ namespace OpenNest.Forms
public readonly NestHoldStatus HoldStatus;
private readonly Timer updateDrawingListTimer;
// The window's UI thread. updateDrawingListTimer fires on a pool thread, which must
// never touch a control: Control.Invoke reads Handle, and a handle destroyed between
// the check and the call is then re-created on the pool thread while this window is
// being disposed ("Dispose() cannot be called while doing CreateHandle()").
private readonly SynchronizationContext uiContext;
private bool updatingPlateList;
private bool addingToSet;
private CutSimulatorForm cutSimulatorForm;
@@ -457,6 +463,7 @@ namespace OpenNest.Forms
Interval = 50,
};
updateDrawingListTimer.Elapsed += drawingListUpdateTimer_Elapsed;
uiContext = SynchronizationContext.Current;
Document = new Document { Nest = nest };
HoldStatus = new NestHoldStatus(Document);
@@ -1790,14 +1797,13 @@ namespace OpenNest.Forms
updateDrawingListTimer.Start();
}
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
if (!drawingListBox1.IsHandleCreated)
return;
drawingListBox1.Invoke(
new MethodInvoker(() =>
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) =>
uiContext?.Post(_ =>
{
// Runs on the UI thread, so this check cannot race the window's disposal.
if (IsDisposed || Disposing || !drawingListBox1.IsHandleCreated)
return;
if (hideNestedButton.Checked)
{
drawingListBox1.BeginUpdate();
@@ -1827,9 +1833,8 @@ namespace OpenNest.Forms
// part-count and utilization columns stay current. The debounce
// timer collapses a fill's many per-part events into one refresh.
UpdatePlateList();
})
);
}
},
null);
private void drawingListBox1_DoubleClick(object sender, EventArgs e)
{