mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 21:52:11 -04:00
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:
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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user