fix(ui): post cutting-plan progress and results to the dialog thread

The dialog relied on Progress<T> and an await continuation capturing
SynchronizationContext.Current when planning started. WinForms
uninstalls its ambient context when the outermost DoEvents loop ends,
so a plan started outside a message loop reported progress from a
thread-pool thread and crashed the test host with "Error creating
window handle" (windows-desktop job of run 37398775280,
CuttingPlanFormTests.ApplyAfterALiveEdit_ChangesNothingAndReplanRecovers).

Capture the dialog's context once when it is built and post progress
and the finished plan to it explicitly. The busy-editor test now
starts planning after a DoEvents loop to cover that path.
This commit is contained in:
aj committed 2026-10-05 21:27:42 -04:00
1 parent 187234f459
commit 1769b8907d
2 files changed
+41 -25

No files matched your search

@@ -138,6 +138,9 @@ public class CuttingPlanFormTests
Assert.Null(form.Proposal);
Assert.Contains("Wait for the current nesting or plate action", Control<TextBox>(form, "summaryBox").Text);
busy = false;
// Ending a DoEvents loop uninstalls WinForms' ambient context; planning started outside a
// message loop must still report progress and results on the dialog's thread.
Application.DoEvents();
Invoke(form, "StartPlanning");
PumpUntil(() => form.Proposal != null, "the plan");
Assert.True(Control<Button>(form, "applyButton").Enabled);
+38 -25
View File
@@ -25,6 +25,7 @@ public partial class CuttingPlanForm : Form
private readonly Func<bool> isOperationBusy;
private readonly string unit;
private readonly PlateView preview;
private readonly SynchronizationContext uiContext;
private CuttingParameters parameters;
private CancellationTokenSource planning;
private CuttingPlanProposal proposal;
@@ -53,6 +54,10 @@ public partial class CuttingPlanForm : Form
this.isOperationBusy = isOperationBusy;
unit = UnitsHelper.GetShortString(nest.Units);
InitializeComponent();
// Captured once: when Application.DoEvents ends the outermost message loop, WinForms
// uninstalls its ambient context, so progress and results must not depend on whichever
// context is current when planning starts.
uiContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
preview = new PlateView
{
@@ -160,51 +165,53 @@ public partial class CuttingPlanForm : Form
}
var source = new CancellationTokenSource();
var token = source.Token;
planning = source;
var run = ++generation;
SetPlanning(true);
summaryBox.Text = string.Empty;
statusLabel.Text = "Planning…";
var progress = new Progress<CuttingPlanProgress>(value =>
var progress = new PostingProgress(uiContext, value =>
{
if (run == generation && ReferenceEquals(planning, source) && !IsDisposed)
statusLabel.Text = ProgressText(value);
});
PlanningTask = RunPlanningAsync(batch, progress, source, run);
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
PlanningTask = completion.Task;
Task.Run(() => batch.Plan(progress, token)).ContinueWith(
work => uiContext.Post(_ => FinishPlanning(work, source, run, completion), null),
CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
}
private async Task RunPlanningAsync(CuttingPlanBatch batch, IProgress<CuttingPlanProgress> progress,
CancellationTokenSource source, int run)
// Runs on the dialog's thread once the worker has stopped.
private void FinishPlanning(Task<CuttingPlanProposal> work, CancellationTokenSource source, int run,
TaskCompletionSource completion)
{
CuttingPlanProposal result = null;
Exception error = null;
try
{
result = await Task.Run(() => batch.Plan(progress, source.Token));
}
catch (Exception ex)
{
error = ex;
}
finally
{
if (ReferenceEquals(planning, source))
planning = null;
source.Dispose();
}
if (run != generation || IsDisposed)
return;
SetPlanning(false);
if (closeRequested)
if (run != generation || IsDisposed)
return;
SetPlanning(false);
if (closeRequested)
{
Close();
return;
}
if (work.IsFaulted)
ShowMessage($"Planning failed: {work.Exception.GetBaseException().Message}");
else if (work.IsCanceled)
ShowMessage("Planning was cancelled. Nothing has changed.");
else
ShowProposal(work.Result);
}
finally
{
Close();
return;
completion.TrySetResult();
}
if (error != null)
ShowMessage($"Planning failed: {error.Message}");
else
ShowProposal(result);
}
private void ShowProposal(CuttingPlanProposal result)
@@ -287,6 +294,12 @@ public partial class CuttingPlanForm : Form
private void PlanButton_Click(object sender, EventArgs e) => StartPlanning();
private sealed class PostingProgress(SynchronizationContext context, Action<CuttingPlanProgress> handler)
: IProgress<CuttingPlanProgress>
{
public void Report(CuttingPlanProgress value) => context.Post(_ => handler(value), null);
}
private void ApplyButton_Click(object sender, EventArgs e)
{
if (planning != null || proposal?.CanApply != true)