Extract business logic into dedicated service layer
Create CutListService and DocumentService to separate business logic from UI concerns, following the service layer pattern. Changes: - CutListService: Handles bin packing algorithm orchestration and data transformation from UI models to domain models - DocumentService: Handles document persistence and validation - Document: Removed Save/Load/Validate methods (now pure POCO) - MainForm: Refactored to delegate to service classes instead of containing business logic directly Benefits: - Improved separation of concerns - Business logic can now be unit tested independently - Removed MessageBox dependencies from business logic layer - MainForm is now focused on UI concerns only 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,53 +25,5 @@ namespace CutList.Forms
|
|||||||
public List<BinInputItem> StockBins { get; set; }
|
public List<BinInputItem> StockBins { get; set; }
|
||||||
|
|
||||||
public Tool Tool { get; set; }
|
public Tool Tool { get; set; }
|
||||||
|
|
||||||
public void Save(string filePath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
|
||||||
File.WriteAllText(filePath, json);
|
|
||||||
LastFilePath = filePath;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Failed to save file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Document Load(string filePath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(filePath);
|
|
||||||
var document = JsonConvert.DeserializeObject<Document>(json);
|
|
||||||
document.LastFilePath = filePath;
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"Failed to load file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Validate(out string validationMessage)
|
|
||||||
{
|
|
||||||
if (PartsToNest == null || !PartsToNest.Any())
|
|
||||||
{
|
|
||||||
validationMessage = "No parts to nest.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StockBins == null || !StockBins.Any())
|
|
||||||
{
|
|
||||||
validationMessage = "No stock bins available.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
validationMessage = string.Empty;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using CutList.Models;
|
using CutList.Models;
|
||||||
using Newtonsoft.Json;
|
using CutList.Services;
|
||||||
using SawCut;
|
|
||||||
using SawCut.Nesting;
|
using SawCut.Nesting;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -21,11 +20,16 @@ namespace CutList.Forms
|
|||||||
private BindingList<BinInputItem> bins;
|
private BindingList<BinInputItem> bins;
|
||||||
private Toolbox toolbox;
|
private Toolbox toolbox;
|
||||||
private Document currentDocument;
|
private Document currentDocument;
|
||||||
|
private readonly CutListService cutListService;
|
||||||
|
private readonly DocumentService documentService;
|
||||||
|
|
||||||
public MainForm()
|
public MainForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
cutListService = new CutListService();
|
||||||
|
documentService = new DocumentService();
|
||||||
|
|
||||||
dataGridView1.DrawRowNumbers();
|
dataGridView1.DrawRowNumbers();
|
||||||
dataGridView2.DrawRowNumbers();
|
dataGridView2.DrawRowNumbers();
|
||||||
|
|
||||||
@@ -90,7 +94,7 @@ namespace CutList.Forms
|
|||||||
|
|
||||||
if (openFileDialog.ShowDialog() == DialogResult.OK)
|
if (openFileDialog.ShowDialog() == DialogResult.OK)
|
||||||
{
|
{
|
||||||
currentDocument = Document.Load(openFileDialog.FileName);
|
currentDocument = documentService.Load(openFileDialog.FileName);
|
||||||
|
|
||||||
if (currentDocument == null)
|
if (currentDocument == null)
|
||||||
return;
|
return;
|
||||||
@@ -138,23 +142,30 @@ namespace CutList.Forms
|
|||||||
|
|
||||||
private void Save()
|
private void Save()
|
||||||
{
|
{
|
||||||
SyncDocumentFromUI();
|
try
|
||||||
|
|
||||||
if (!currentDocument.Validate(out string validationMessage))
|
|
||||||
{
|
{
|
||||||
MessageBox.Show(validationMessage, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
SyncDocumentFromUI();
|
||||||
return;
|
|
||||||
|
if (!documentService.Validate(currentDocument, out string validationMessage))
|
||||||
|
{
|
||||||
|
MessageBox.Show(validationMessage, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveFileDialog = new SaveFileDialog
|
||||||
|
{
|
||||||
|
FileName = currentDocument.LastFilePath == null ? "NewDocument.json" : Path.GetFileName(currentDocument.LastFilePath),
|
||||||
|
Filter = "Json File|*.json"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (saveFileDialog.ShowDialog() == DialogResult.OK)
|
||||||
|
{
|
||||||
|
documentService.Save(currentDocument, saveFileDialog.FileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
var saveFileDialog = new SaveFileDialog
|
|
||||||
{
|
{
|
||||||
FileName = currentDocument.LastFilePath == null ? "NewDocument.json" : Path.GetFileName(currentDocument.LastFilePath),
|
MessageBox.Show($"Failed to save file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
Filter = "Json File|*.json"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (saveFileDialog.ShowDialog() == DialogResult.OK)
|
|
||||||
{
|
|
||||||
currentDocument.Save(saveFileDialog.FileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,28 +175,11 @@ namespace CutList.Forms
|
|||||||
dataGridView2.EndEdit();
|
dataGridView2.EndEdit();
|
||||||
|
|
||||||
var cutTool = GetSelectedTool();
|
var cutTool = GetSelectedTool();
|
||||||
var stockBins = new List<MultiBin>();
|
var result = cutListService.Pack(parts.ToList(), bins.ToList(), cutTool);
|
||||||
|
|
||||||
foreach (var item in bins)
|
|
||||||
{
|
|
||||||
stockBins.Add(new MultiBin
|
|
||||||
{
|
|
||||||
Length = item.Length.Value,
|
|
||||||
Quantity = item.Quantity,
|
|
||||||
Priority = item.Priority
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var engine = new MultiBinEngine();
|
|
||||||
engine.Spacing = cutTool.Kerf;
|
|
||||||
engine.Bins = stockBins;
|
|
||||||
|
|
||||||
var items = GetItems();
|
|
||||||
var result = engine.Pack(items);
|
|
||||||
|
|
||||||
var filename = GetResultsSaveName();
|
var filename = GetResultsSaveName();
|
||||||
var form = new ResultsForm(filename);
|
var form = new ResultsForm(filename);
|
||||||
form.Bins = result.Bins;
|
form.Bins = result.Bins.ToList();
|
||||||
form.ShowDialog();
|
form.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,28 +194,6 @@ namespace CutList.Forms
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<BinItem> GetItems()
|
|
||||||
{
|
|
||||||
var items2 = new List<BinItem>();
|
|
||||||
|
|
||||||
foreach (var item in parts)
|
|
||||||
{
|
|
||||||
if (item.Length == null || item.Length == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
for (int i = 0; i < item.Quantity; i++)
|
|
||||||
{
|
|
||||||
items2.Add(new BinItem
|
|
||||||
{
|
|
||||||
Name = item.Name,
|
|
||||||
Length = item.Length.Value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Tool GetSelectedTool()
|
public Tool GetSelectedTool()
|
||||||
{
|
{
|
||||||
return cutMethodComboBox.SelectedItem as Tool;
|
return cutMethodComboBox.SelectedItem as Tool;
|
||||||
|
|||||||
74
CutList/Services/CutListService.cs
Normal file
74
CutList/Services/CutListService.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using CutList.Models;
|
||||||
|
using SawCut;
|
||||||
|
using SawCut.Nesting;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CutList.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service class that handles the core business logic for cut list optimization.
|
||||||
|
/// Separates business logic from UI concerns.
|
||||||
|
/// </summary>
|
||||||
|
public class CutListService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the bin packing algorithm to optimize cut lists.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parts">The parts to be nested</param>
|
||||||
|
/// <param name="stockBins">The available stock bins</param>
|
||||||
|
/// <param name="cuttingTool">The cutting tool to use (determines kerf/spacing)</param>
|
||||||
|
/// <returns>The packing result with optimized bins and unused items</returns>
|
||||||
|
public Result Pack(List<PartInputItem> parts, List<BinInputItem> stockBins, Tool cuttingTool)
|
||||||
|
{
|
||||||
|
var multiBins = ConvertToMultiBins(stockBins);
|
||||||
|
var binItems = ConvertToBinItems(parts);
|
||||||
|
|
||||||
|
var engine = new MultiBinEngine
|
||||||
|
{
|
||||||
|
Spacing = cuttingTool.Kerf,
|
||||||
|
Bins = multiBins
|
||||||
|
};
|
||||||
|
|
||||||
|
return engine.Pack(binItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MultiBin> ConvertToMultiBins(List<BinInputItem> stockBins)
|
||||||
|
{
|
||||||
|
var multiBins = new List<MultiBin>();
|
||||||
|
|
||||||
|
foreach (var item in stockBins)
|
||||||
|
{
|
||||||
|
multiBins.Add(new MultiBin
|
||||||
|
{
|
||||||
|
Length = item.Length.Value,
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
Priority = item.Priority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiBins;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BinItem> ConvertToBinItems(List<PartInputItem> parts)
|
||||||
|
{
|
||||||
|
var binItems = new List<BinItem>();
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
if (part.Length == null || part.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (int i = 0; i < part.Quantity; i++)
|
||||||
|
{
|
||||||
|
binItems.Add(new BinItem
|
||||||
|
{
|
||||||
|
Name = part.Name,
|
||||||
|
Length = part.Length.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
CutList/Services/DocumentService.cs
Normal file
64
CutList/Services/DocumentService.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using CutList.Forms;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace CutList.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service class that handles document persistence operations.
|
||||||
|
/// Separates file I/O logic from UI concerns.
|
||||||
|
/// </summary>
|
||||||
|
public class DocumentService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a document to the specified file path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="document">The document to save</param>
|
||||||
|
/// <param name="filePath">The file path to save to</param>
|
||||||
|
/// <exception cref="IOException">Thrown when file cannot be saved</exception>
|
||||||
|
public void Save(Document document, string filePath)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(document, Formatting.Indented);
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a document from the specified file path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">The file path to load from</param>
|
||||||
|
/// <returns>The loaded document</returns>
|
||||||
|
/// <exception cref="IOException">Thrown when file cannot be read</exception>
|
||||||
|
/// <exception cref="JsonException">Thrown when file contains invalid JSON</exception>
|
||||||
|
public Document Load(string filePath)
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var document = JsonConvert.DeserializeObject<Document>(json);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that a document has the minimum required data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="document">The document to validate</param>
|
||||||
|
/// <param name="validationMessage">Output parameter containing validation error message</param>
|
||||||
|
/// <returns>True if document is valid, false otherwise</returns>
|
||||||
|
public bool Validate(Document document, out string validationMessage)
|
||||||
|
{
|
||||||
|
if (document.PartsToNest == null || document.PartsToNest.Count == 0)
|
||||||
|
{
|
||||||
|
validationMessage = "No parts to nest.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.StockBins == null || document.StockBins.Count == 0)
|
||||||
|
{
|
||||||
|
validationMessage = "No stock bins available.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationMessage = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user