From ae14458444a9fba0cbb9348ca511e16dc90aaeee Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 5 Nov 2026 07:21:23 -0500 Subject: [PATCH] fix(ui): open View in CAD through the shell instead of crashing .NET 8 defaults Process.Start to UseShellExecute=false, so handing it the temp DXF tried to run the file as an executable and threw an unhandled Win32Exception that took the app down. Open it through the shell so the .dxf association is used, report write/launch failures in a message box, and replace characters a file name cannot hold in the nest name. Co-Authored-By: Claude Opus 5.5 --- .../Forms/PlateExportTests.cs | 56 +++++++++++++++++++ OpenNest/Forms/EditNestForm.cs | 39 +++++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/OpenNest.WinForms.Tests/Forms/PlateExportTests.cs b/OpenNest.WinForms.Tests/Forms/PlateExportTests.cs index ec4d9adc..da318b6f 100644 --- a/OpenNest.WinForms.Tests/Forms/PlateExportTests.cs +++ b/OpenNest.WinForms.Tests/Forms/PlateExportTests.cs @@ -327,6 +327,50 @@ public class PlateExportTests Assert.Same(form.Nest.Plates[0], form.PlateView.Plate); }); + [Fact] + public void ViewInCadWritesDxfAndOpensItThroughTheShell() => RunSta(() => + { + using var form = ShowJob(); + form.PlateManager.LoadAt(1); + + form.OpenCurrentPlate(); + + var path = Assert.Single(form.Launches); + try + { + Assert.Equal(Path.Combine(Path.GetTempPath(), "export job-P2.dxf"), path); + Assert.Same(form.Nest.Plates[1], Assert.Single(form.WriteRequests).Plate); + Assert.True(new FileInfo(path).Length > 0); + Assert.Empty(form.Failures); + Assert.True(EditNestForm.ExternalCadStartInfo(path).UseShellExecute); + } + finally + { + File.Delete(path); + } + }); + + [Fact] + public void ViewInCadReportsLaunchFailureInsteadOfThrowing() => RunSta(() => + { + using var form = ShowJob(); + var error = new System.ComponentModel.Win32Exception("No application is associated with .dxf."); + form.LaunchAction = _ => throw error; + + form.OpenCurrentPlate(); + + var failure = Assert.Single(form.Failures); + File.Delete(failure.Destination); + Assert.Same(error, failure.Error); + }); + + [Theory] + [InlineData("job 1", 3, "job 1-P3.dxf")] + [InlineData("A/B:C*?", 1, "A_B_C__-P1.dxf")] + [InlineData(" ", 2, "Nest-P2.dxf")] + public void ViewInCadFileNameIsAlwaysUsable(string nestName, int plateNumber, string expected) => + Assert.Equal(expected, EditNestForm.ExternalCadFileName(nestName, plateNumber)); + private static TestEditNestForm ShowJob() { var form = new TestEditNestForm(CreateJob()); @@ -409,6 +453,18 @@ public class PlateExportTests internal override void ReportPlateExportFailure(string destination, Exception error) => Failures.Add((destination, error)); + + public List Launches { get; } = []; + public System.Action? LaunchAction { get; set; } + + internal override void LaunchExternalCad(string path) + { + Launches.Add(path); + LaunchAction?.Invoke(path); + } + + internal override void ReportExternalCadFailure(string path, Exception error) => + Failures.Add((path, error)); } private sealed class FlushFailureStream(string path) : FileStream(path, FileMode.Create) diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 8391b267..b3db88e0 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1057,13 +1057,44 @@ namespace OpenNest.Forms public void OpenCurrentPlate() { var plate = PlateView.Plate; - var name = string.Format("{0}-P{1}.dxf", Nest.Name, PlateManager.CurrentIndex + 1); + var name = ExternalCadFileName(Nest.Name, PlateManager.CurrentIndex + 1); var path = Path.Combine(Path.GetTempPath(), name); - Dxf.ExportPlate(plate, path); - - Process.Start(path); + try + { + WritePlateExport(plate, path, 1); + LaunchExternalCad(path); + } + catch (Exception ex) + { + ReportExternalCadFailure(path, ex); + } } + /// + /// Temp DXF name for View in CAD; characters a file name cannot hold become underscores. + /// + internal static string ExternalCadFileName(string nestName, int plateNumber) + { + var baseName = string.IsNullOrWhiteSpace(nestName) ? "Nest" : nestName.Trim(); + var invalid = Path.GetInvalidFileNameChars(); + var chars = baseName.Select(c => Array.IndexOf(invalid, c) >= 0 ? '_' : c).ToArray(); + return $"{new string(chars)}-P{plateNumber}.dxf"; + } + + // .NET 8 defaults UseShellExecute to false, which tries to run the DXF as an executable; + // the shell opens it in the program associated with .dxf instead. + internal static ProcessStartInfo ExternalCadStartInfo(string path) => + new ProcessStartInfo(path) { UseShellExecute = true }; + + internal virtual void LaunchExternalCad(string path) + { + using var process = Process.Start(ExternalCadStartInfo(path)); + } + + internal virtual void ReportExternalCadFailure(string path, Exception error) => + MessageBox.Show(this, $"Could not open '{path}' in CAD: {error.Message}", + "View in CAD", MessageBoxButtons.OK, MessageBoxIcon.Error); + public void AddPlate() { using var change = BeginChange("Add Plate");