refactor(ui): extract MainForm startup and settings services

Moves startup and settings code out of MainForm:
- PostProcessorLoader discovers posts and registers their materials.
  Each DLL now loads in isolation; a failing one is named in a startup
  warning instead of throwing out of the MainForm constructor.
- MachineSettings owns the clamp-settings and nest-defaults entry points
  and their once-per-session warnings; BomImportForm calls it directly.
- NewNestFactory creates File > New nests and their daily names;
  GetNestName is now a pure static the characterization test calls.
- MainWindowSettings restores/saves window placement and migrates the
  legacy nest template.
- UpdaterStatus owns the updater and its status-bar item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-17 21:11:37 -04:00
1 parent 3cae257fb1
commit 9b46db4891
13 files changed
+377 -282

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ public sealed class NestDefaults
/// <summary>
/// The built-in defaults used when no file exists and for every field
/// that is missing or invalid. Matches the historical
/// MainForm.CreateDefaultNest values (units default to Inches; callers
/// File > New values (units default to Inches; callers
/// may override from their own settings).
/// </summary>
public static NestDefaults Fallback => new();
@@ -18,15 +18,7 @@ public class MainFormCharacterizationTests
[InlineData(900, "N26-322")]
[InlineData(27000, "N26-3222")]
public void NestNamesUseTwoDigitYearAndPaddedBase30Sequence(int id, string expected) =>
StaTestThread.Run(() =>
{
using var host = new Host();
var name = (string)typeof(MainForm)
.GetMethod("GetNestName", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(host, [new DateTime(2026, 10, 17), id])!;
Assert.Equal(expected, name);
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
Assert.Equal(expected, NewNestFactory.GetNestName(new DateTime(2026, 10, 17), id));
[Fact]
public void BusyGuardsDifferInWhichOperationsTheyCount() => StaTestThread.Run(() =>
+2 -2
View File
@@ -32,7 +32,7 @@ namespace OpenNest.Forms
dgvParts.AutoGenerateColumns = false;
dgvParts.DataSource = _parts;
_groupSettings = new Dictionary<string, BomGroupPlateSettings>();
_defaults = MainForm.LoadSavedNestDefaults(out _);
_defaults = MachineSettings.LoadSavedNestDefaults(out _);
ApplyDefaults();
}
@@ -414,7 +414,7 @@ namespace OpenNest.Forms
var nestsCreated = 0;
var importErrors = new List<string>();
// Read when the nests are created, warning (once per session) like New does.
var clamps = MainForm.LoadClampSettings(this);
var clamps = MachineSettings.LoadClampSettings(this);
foreach (var group in groups)
{
+83
View File
@@ -0,0 +1,83 @@
using System.Windows.Forms;
using OpenNest.Data;
namespace OpenNest.Forms
{
/// <summary>
/// The desktop entry points for the persisted machine clamp settings and nest defaults.
/// The warning variants report an unusable file once per session.
/// </summary>
internal static class MachineSettings
{
private static bool clampSettingsWarned;
private static bool defaultsWarned;
/// <summary>
/// Loads the machine clamp settings for creating new nests (New, BOM import); an
/// unreadable or partly invalid file warns once per session, because the new nest's
/// clamps may not match the machine.
/// </summary>
public static ClampSettings LoadClampSettings(IWin32Window owner)
{
var settings = LoadSavedClampSettings(out var status);
if (
status is ClampSettingsStatus.Invalid or ClampSettingsStatus.Partial
&& !clampSettingsWarned
)
{
clampSettingsWarned = true;
var problem =
status == ClampSettingsStatus.Invalid
? "could not be read, so new nests have no clamps"
: "has invalid values that were ignored, so new nests may be missing clamps";
MessageBox.Show(
owner,
$"The clamp settings file {problem}:\n{ClampSettings.DefaultPath}\n\nCheck the clamps in Nest > Clamps and use Save as Machine Defaults to rewrite the file.",
"Clamps",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
return settings;
}
/// <summary>The single desktop entry point for the persisted machine clamp settings.</summary>
public static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) =>
ClampSettings.Load(ClampSettings.DefaultPath, out status);
/// <summary>
/// Loads the persisted nest defaults (see <see cref="LoadSavedNestDefaults"/>);
/// a corrupt or unreadable file warns once per session.
/// </summary>
public static NestDefaults LoadNestDefaults()
{
var defaults = LoadSavedNestDefaults(out var status);
if (status == NestDefaultsStatus.Invalid && !defaultsWarned)
{
defaultsWarned = true;
MessageBox.Show(
$"The nest defaults file could not be read:\n{NestDefaults.DefaultPath}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.",
"Nest Defaults",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
return defaults;
}
/// <summary>
/// The single desktop entry point for persisted nest defaults. Units
/// fall back to the legacy DefaultUnit setting whenever the file is
/// missing or unusable or has no valid unit, so New, BOM import and
/// the defaults dialog always agree.
/// </summary>
public static NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) =>
NestDefaults.Load(
NestDefaults.DefaultPath,
Properties.Settings.Default.DefaultUnit,
out status
);
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ namespace OpenNest.Forms
components.Dispose();
}
if (disposing)
updater?.Dispose();
updaterStatus?.Dispose();
base.Dispose(disposing);
}
+1 -118
View File
@@ -6,116 +6,11 @@ using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.Forms.Commands;
using OpenNest.IO;
using OpenNest.Properties;
namespace OpenNest.Forms
{
public partial class MainForm
{
private Nest CreateDefaultNest()
{
var nest = new Nest();
LoadNestDefaults().ApplyTo(nest);
// After the defaults so the rail is converted to the new nest's units.
LoadClampSettings(this).ApplyToNewNest(nest);
return nest;
}
/// <summary>
/// Loads the machine clamp settings for creating new nests (New, BOM import); an
/// unreadable or partly invalid file warns once per session, because the new nest's
/// clamps may not match the machine.
/// </summary>
internal static ClampSettings LoadClampSettings(IWin32Window owner)
{
var settings = LoadSavedClampSettings(out var status);
if (
status is ClampSettingsStatus.Invalid or ClampSettingsStatus.Partial
&& !clampSettingsWarned
)
{
clampSettingsWarned = true;
var problem =
status == ClampSettingsStatus.Invalid
? "could not be read, so new nests have no clamps"
: "has invalid values that were ignored, so new nests may be missing clamps";
MessageBox.Show(
owner,
$"The clamp settings file {problem}:\n{ClampSettings.DefaultPath}\n\nCheck the clamps in Nest > Clamps and use Save as Machine Defaults to rewrite the file.",
"Clamps",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
return settings;
}
private static bool clampSettingsWarned;
/// <summary>The single desktop entry point for the persisted machine clamp settings.</summary>
internal static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) =>
ClampSettings.Load(ClampSettings.DefaultPath, out status);
/// <summary>
/// Loads the persisted nest defaults (see <see cref="LoadSavedNestDefaults"/>);
/// a corrupt or unreadable file warns once per session.
/// </summary>
private NestDefaults LoadNestDefaults()
{
var defaults = LoadSavedNestDefaults(out var status);
if (status == NestDefaultsStatus.Invalid && !defaultsWarned)
{
defaultsWarned = true;
MessageBox.Show(
$"The nest defaults file could not be read:\n{NestDefaults.DefaultPath}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.",
"Nest Defaults",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
return defaults;
}
private bool defaultsWarned;
/// <summary>
/// The single desktop entry point for persisted nest defaults. Units
/// fall back to the legacy DefaultUnit setting whenever the file is
/// missing or unusable or has no valid unit, so New, BOM import and
/// the defaults dialog always agree.
/// </summary>
internal static NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) =>
NestDefaults.Load(
NestDefaults.DefaultPath,
Properties.Settings.Default.DefaultUnit,
out status
);
private string GetNestName(DateTime date, int id)
{
var year = (date.Year % 100).ToString("D2");
var seq = ToBase36(id).PadLeft(3, '2');
return $"N{year}-{seq}";
}
private static string ToBase36(int value)
{
const string chars = "2345679ACDEFGHJKLMNPQRSTUVWXYZ";
if (value == 0)
return chars[0].ToString();
var result = "";
while (value > 0)
{
result = chars[value % chars.Length] + result;
value /= chars.Length;
}
return result;
}
private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized,
Guid remoteId = default, string serverUrl = null, int remoteRevision = 0)
{
@@ -142,19 +37,7 @@ namespace OpenNest.Forms
var windowState =
ActiveMdiChild != null ? ActiveMdiChild.WindowState : FormWindowState.Maximized;
var nest = CreateDefaultNest();
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
if (DateTime.Now.Date != Settings.Default.LastNestCreatedDate.Date)
{
Settings.Default.NestNumber = 1;
Settings.Default.LastNestCreatedDate = DateTime.Now.Date;
}
nest.Name = GetNestName(DateTime.Now, Settings.Default.NestNumber++);
LoadNest(nest, windowState);
LoadNest(NewNestFactory.Create(this), windowState);
}
private async void Open_Click(object sender, EventArgs e)
+1 -1
View File
@@ -77,7 +77,7 @@ namespace OpenNest.Forms
return;
}
var defaults = LoadSavedClampSettings(out _);
var defaults = MachineSettings.LoadSavedClampSettings(out _);
using var form = new ClampsForm(nest.PlateDefaults.ClampRail, nest.Units, defaults);
if (form.ShowDialog(this) != DialogResult.OK)
return;
+1 -1
View File
@@ -177,7 +177,7 @@ namespace OpenNest.Forms
private void NestDefaults_Click(object sender, EventArgs e)
{
using (var form = new NestDefaultsForm(LoadSavedNestDefaults(out _)))
using (var form = new NestDefaultsForm(MachineSettings.LoadSavedNestDefaults(out _)))
{
if (form.ShowDialog(this) == DialogResult.OK)
form.GetDefaults().Save(NestDefaults.DefaultPath);
+21 -149
View File
@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs;
using OpenNest.Forms.Commands;
@@ -110,7 +108,7 @@ namespace OpenNest.Forms
mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExport), exportNestMenu);
mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExit), storageModeMenu);
UpdateOverlapMenu();
LoadSettings();
MainWindowSettings.Load(this);
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
menuStrip1.Renderer = renderer;
@@ -124,7 +122,7 @@ namespace OpenNest.Forms
this.SetBevel(false);
AllowDrop = true;
LoadPosts();
LoadPosts(PostProcessorLoader.Load(PostProcessorLoader.DefaultDirectory));
EnableCheck();
UpdateStatus();
@@ -146,84 +144,11 @@ namespace OpenNest.Forms
engineComboBox.SelectedItem = EngineSelection.EngineName;
engineComboBox.SelectedIndexChanged += EngineComboBox_SelectedIndexChanged;
InitializeUpdater();
updaterStatus = new UpdaterStatus(this, statusStrip1, statusLabel1.Font);
updaterStatus.Start();
}
private global::OpenNest.UpdaterService updater;
private ToolStripStatusLabel updateStatusLabel;
/// <summary>
/// Starts background update checks against the per-machine feed URL
/// (Options dialog; blank disables checks). Never restarts on its
/// own -- shows a clickable status item once an update is ready and
/// restarts only when the user clicks it.
/// </summary>
private void InitializeUpdater()
{
updateStatusLabel = new ToolStripStatusLabel
{
Visible = false,
ForeColor = Color.DarkGreen,
Font = new Font(statusLabel1.Font, FontStyle.Bold),
};
updateStatusLabel.Click += (_, _) =>
{
if (!updater.HasPendingUpdate)
return;
var result = MessageBox.Show(
this,
"An OpenNest update has been downloaded. Restart now to apply it? Any unsaved nest will be lost.",
"Update ready",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (result == DialogResult.Yes)
updater.ApplyAndRestart();
};
statusStrip1.Items.Add(updateStatusLabel);
updater = new global::OpenNest.UpdaterService();
updater.UpdateReady += version =>
{
if (IsHandleCreated)
BeginInvoke(new System.Action(() => ShowUpdateReady(version)));
};
updater.Start();
}
private void ShowUpdateReady(string version)
{
updateStatusLabel.Text = $"Update {version} ready - click to restart";
updateStatusLabel.Visible = true;
}
private void LoadSettings()
{
var screen = Screen.PrimaryScreen.Bounds;
if (screen.Contains(Settings.Default.MainWindowLocation))
Location = Settings.Default.MainWindowLocation;
if (
Settings.Default.MainWindowSize.Width <= screen.Width
&& Settings.Default.MainWindowSize.Height <= screen.Height
)
{
Size = Settings.Default.MainWindowSize;
}
WindowState = Settings.Default.MainWindowState;
}
private void SaveSettings()
{
Settings.Default.MainWindowLocation = Location;
Settings.Default.MainWindowSize = Size;
Settings.Default.MainWindowState = WindowState;
Settings.Default.Save();
}
private UpdaterStatus updaterStatus;
private static ToolStripButton CreateActionButton(string name, string text, Image image,
EventHandler click)
@@ -242,40 +167,27 @@ namespace OpenNest.Forms
return button;
}
private void LoadPosts()
private void LoadPosts(PostProcessorLoader.Result posts)
{
var exepath = Assembly.GetEntryAssembly().Location;
var postprocessordir = Path.Combine(Path.GetDirectoryName(exepath), "Posts");
if (Directory.Exists(postprocessordir))
foreach (var postProcessor in posts.PostProcessors)
{
foreach (var file in Directory.GetFiles(postprocessordir, "*.dll"))
{
var types = Assembly.LoadFile(file).GetTypes();
var postProcessorMenuItem = new ToolStripMenuItem(postProcessor.Name);
postProcessorMenuItem.Tag = postProcessor;
postProcessorMenuItem.Click += PostProcessor_Click;
mnuNestPost.DropDownItems.Add(postProcessorMenuItem);
foreach (var type in types)
{
if (type.GetInterfaces().Contains(typeof(IPostProcessor)) == false)
continue;
var postProcessor = Activator.CreateInstance(type) as IPostProcessor;
var postProcessorMenuItem = new ToolStripMenuItem(postProcessor.Name);
postProcessorMenuItem.Tag = postProcessor;
postProcessorMenuItem.Click += PostProcessor_Click;
mnuNestPost.DropDownItems.Add(postProcessorMenuItem);
var toolbarPost = new ToolStripMenuItem(postProcessor.Name) { Tag = postProcessor };
toolbarPost.Click += PostProcessor_Click;
postButton.DropDownItems.Add(toolbarPost);
if (postProcessor is IMaterialProvidingPostProcessor materialProvider)
PostProcessorMaterials.AddFrom(materialProvider);
}
}
var toolbarPost = new ToolStripMenuItem(postProcessor.Name) { Tag = postProcessor };
toolbarPost.Click += PostProcessor_Click;
postButton.DropDownItems.Add(toolbarPost);
}
mnuNestPost.Visible = mnuNestPost.DropDownItems.Count > 0;
postButton.Visible = postButton.DropDownItems.Count > 0;
if (posts.Failures.Count > 0)
Shown += (_, _) => MessageBox.Show(this,
"These post processors could not be loaded:\n\n" + string.Join("\n", posts.Failures),
"Post Processors", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
#region Overrides
@@ -344,52 +256,12 @@ namespace OpenNest.Forms
{
base.OnLoad(e);
MigrateNestTemplate();
MainWindowSettings.MigrateNestTemplate();
if (Settings.Default.CreateNewNestOnOpen)
New_Click(this, new EventArgs());
}
/// <summary>
/// One-time upgrade: converts a legacy .nstdot nest template
/// (NestTemplatePath setting) into defaults.json, then clears the
/// setting so the template mechanism is never consulted again.
/// </summary>
private void MigrateNestTemplate()
{
var templatePath = Settings.Default.NestTemplatePath;
if (string.IsNullOrWhiteSpace(templatePath))
return;
// A failed conversion keeps the setting populated so the user
// can still find their template file.
var converted = true;
if (File.Exists(templatePath) && !File.Exists(NestDefaults.DefaultPath))
{
try
{
var nest = new NestReader(templatePath) { BestFitStore = BestFitCacheStore.Shared }.Read();
NestDefaults.FromNest(nest).Save(NestDefaults.DefaultPath);
}
catch (Exception ex)
{
converted = false;
MessageBox.Show(
$"The nest template could not be converted to the new defaults file:\n{templatePath}\n\n{ex.Message}\n\nIt will no longer be loaded automatically. Set defaults under Tools > Nest Defaults.",
"Nest Template",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
}
if (!converted)
return;
Settings.Default.NestTemplatePath = "";
Settings.Default.Save();
}
protected override void OnDragEnter(DragEventArgs e)
{
base.OnDragEnter(e);
@@ -430,7 +302,7 @@ namespace OpenNest.Forms
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
base.OnClosing(e);
SaveSettings();
MainWindowSettings.Save(this);
}
#endregion
+81
View File
@@ -0,0 +1,81 @@
using System;
using System.IO;
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.IO;
using OpenNest.Properties;
namespace OpenNest.Forms
{
/// <summary>The main window's persisted placement and its one-time settings upgrades.</summary>
internal static class MainWindowSettings
{
/// <summary>Restores the saved placement when it still fits the primary screen.</summary>
public static void Load(Form form)
{
var screen = Screen.PrimaryScreen.Bounds;
if (screen.Contains(Settings.Default.MainWindowLocation))
form.Location = Settings.Default.MainWindowLocation;
if (
Settings.Default.MainWindowSize.Width <= screen.Width
&& Settings.Default.MainWindowSize.Height <= screen.Height
)
{
form.Size = Settings.Default.MainWindowSize;
}
form.WindowState = Settings.Default.MainWindowState;
}
public static void Save(Form form)
{
Settings.Default.MainWindowLocation = form.Location;
Settings.Default.MainWindowSize = form.Size;
Settings.Default.MainWindowState = form.WindowState;
Settings.Default.Save();
}
/// <summary>
/// One-time upgrade: converts a legacy .nstdot nest template
/// (NestTemplatePath setting) into defaults.json, then clears the
/// setting so the template mechanism is never consulted again.
/// </summary>
public static void MigrateNestTemplate()
{
var templatePath = Settings.Default.NestTemplatePath;
if (string.IsNullOrWhiteSpace(templatePath))
return;
// A failed conversion keeps the setting populated so the user
// can still find their template file.
var converted = true;
if (File.Exists(templatePath) && !File.Exists(NestDefaults.DefaultPath))
{
try
{
var nest = new NestReader(templatePath) { BestFitStore = BestFitCacheStore.Shared }.Read();
NestDefaults.FromNest(nest).Save(NestDefaults.DefaultPath);
}
catch (Exception ex)
{
converted = false;
MessageBox.Show(
$"The nest template could not be converted to the new defaults file:\n{templatePath}\n\n{ex.Message}\n\nIt will no longer be loaded automatically. Set defaults under Tools > Nest Defaults.",
"Nest Template",
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
}
if (!converted)
return;
Settings.Default.NestTemplatePath = "";
Settings.Default.Save();
}
}
}
+68
View File
@@ -0,0 +1,68 @@
using System;
using System.Windows.Forms;
using OpenNest.Properties;
namespace OpenNest.Forms
{
/// <summary>
/// Creates File > New nests: machine defaults and clamps, then the next name in the
/// day's sequence (<see cref="Settings.NestNumber"/> restarts at 1 each day).
/// </summary>
internal static class NewNestFactory
{
private const string Base30Digits = "2345679ACDEFGHJKLMNPQRSTUVWXYZ";
public static Nest Create(IWin32Window owner)
{
var nest = CreateDefaultNest(owner);
var now = DateTime.Now;
nest.DateCreated = now;
nest.DateLastModified = now;
if (now.Date != Settings.Default.LastNestCreatedDate.Date)
{
Settings.Default.NestNumber = 1;
Settings.Default.LastNestCreatedDate = now.Date;
}
nest.Name = GetNestName(now, Settings.Default.NestNumber++);
return nest;
}
private static Nest CreateDefaultNest(IWin32Window owner)
{
var nest = new Nest();
MachineSettings.LoadNestDefaults().ApplyTo(nest);
// After the defaults so the rail is converted to the new nest's units.
MachineSettings.LoadClampSettings(owner).ApplyToNewNest(nest);
return nest;
}
/// <summary>
/// "N" + two-digit year + the sequence in an unambiguous base-30 alphabet, padded to
/// three digits with its zero digit ('2').
/// </summary>
public static string GetNestName(DateTime date, int id)
{
var year = (date.Year % 100).ToString("D2");
var seq = ToBase30(id).PadLeft(3, Base30Digits[0]);
return $"N{year}-{seq}";
}
private static string ToBase30(int value)
{
if (value == 0)
return Base30Digits[0].ToString();
var result = "";
while (value > 0)
{
result = Base30Digits[value % Base30Digits.Length] + result;
value /= Base30Digits.Length;
}
return result;
}
}
}
+63
View File
@@ -0,0 +1,63 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace OpenNest.Forms
{
/// <summary>
/// Background update checks against the per-machine feed URL (Options dialog; blank
/// disables checks) and the status-bar item that announces a downloaded update. Never
/// restarts on its own: it restarts only when the user clicks the item and confirms.
/// </summary>
internal sealed class UpdaterStatus : IDisposable
{
private readonly Form owner;
private readonly UpdaterService updater = new();
private readonly ToolStripStatusLabel label;
public UpdaterStatus(Form owner, StatusStrip statusStrip, Font baseFont)
{
this.owner = owner;
label = new ToolStripStatusLabel
{
Visible = false,
ForeColor = Color.DarkGreen,
Font = new Font(baseFont, FontStyle.Bold),
};
label.Click += Label_Click;
statusStrip.Items.Add(label);
updater.UpdateReady += version =>
{
if (owner.IsHandleCreated)
owner.BeginInvoke(new Action(() => ShowUpdateReady(version)));
};
}
public void Start() => updater.Start();
public void Dispose() => updater.Dispose();
private void Label_Click(object sender, EventArgs e)
{
if (!updater.HasPendingUpdate)
return;
var result = MessageBox.Show(
owner,
"An OpenNest update has been downloaded. Restart now to apply it? Any unsaved nest will be lost.",
"Update ready",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (result == DialogResult.Yes)
updater.ApplyAndRestart();
}
private void ShowUpdateReady(string version)
{
label.Text = $"Update {version} ready - click to restart";
label.Visible = true;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace OpenNest
{
/// <summary>
/// Discovers post processors in the desktop's <c>Posts/</c> directory and registers the
/// materials they provide. Each DLL loads in isolation: one that fails is reported in
/// <see cref="Result.Failures"/> instead of stopping the others or startup.
/// </summary>
public static class PostProcessorLoader
{
public sealed record Result(IReadOnlyList<IPostProcessor> PostProcessors, IReadOnlyList<string> Failures);
public static string DefaultDirectory =>
Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Posts");
public static Result Load(string directory)
{
var postProcessors = new List<IPostProcessor>();
var failures = new List<string>();
if (!Directory.Exists(directory))
return new Result(postProcessors, failures);
foreach (var file in Directory.GetFiles(directory, "*.dll"))
{
try
{
// Instantiate the whole DLL before registering any of it.
var loaded = Assembly.LoadFile(file).GetTypes()
.Where(type => type.GetInterfaces().Contains(typeof(IPostProcessor)))
.Select(type => (IPostProcessor)Activator.CreateInstance(type))
.ToList();
foreach (var postProcessor in loaded)
{
postProcessors.Add(postProcessor);
if (postProcessor is IMaterialProvidingPostProcessor materialProvider)
PostProcessorMaterials.AddFrom(materialProvider);
}
}
catch (Exception ex)
{
var reason = ex is TargetInvocationException { InnerException: { } inner } ? inner : ex;
failures.Add($"{Path.GetFileName(file)}: {reason.Message}");
}
}
return new Result(postProcessors, failures);
}
}
}