Implemented save load functionality to save and load the editor

This commit is contained in:
Ankitkumar Satapara
2026-04-19 00:37:42 +05:30
parent ec620bf30d
commit e2c43af907
5 changed files with 545 additions and 44 deletions

View File

@@ -39,45 +39,26 @@ namespace Nodify.Calculator
}); });
SaveFileCommand = new DelegateCommand(() => SaveFileCommand = new DelegateCommand(() =>
{ {
var firstEditor = Editors.First(); try
var allNodes = firstEditor.Calculator.Operations;
SaveGraphModel svm = new SaveGraphModel();
foreach (var item in allNodes)
{ {
SaveNodes svn = new SaveNodes() var firstEditor = Editors.First();
{ GraphSerializer.Save(firstEditor.Calculator);
Location = item.Location,
};
svm.Nodes.Add(svn);
} }
catch (Exception ex)
var jsonEditors = JsonConvert.SerializeObject(svm, new JsonSerializerSettings
{ {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore MessageBox.Show($"Failed to save: {ex.Message}", "Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
}); }
//var jsonEditors = JsonConvert.SerializeObject(Editors);
File.WriteAllText("SaveFile.AEXN", jsonEditors);
}); });
OpenFileCommand = new DelegateCommand(() => OpenFileCommand = new DelegateCommand(() =>
{ {
if (File.Exists("SaveFile.AEXN")) try
{ {
var allText = File.ReadAllText("SaveFile.AEXN");
var edts = JsonConvert.DeserializeObject<SaveGraphModel>(allText);
var firstEditor = Editors.First(); var firstEditor = Editors.First();
var nodesToAdd = edts.Nodes; GraphSerializer.Load(firstEditor.Calculator, firstEditor.Calculator.OperationsMenu);
foreach (var item in nodesToAdd) }
{ catch (Exception ex)
firstEditor.Calculator.Operations.Add(new() {
{ MessageBox.Show($"Failed to load: {ex.Message}", "Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
Location = item.Location,
NodeId = "H",
Title = "HHA"
});
}
} }
}); });
Editors.WhenAdded((editor) => Editors.WhenAdded((editor) =>
@@ -98,6 +79,14 @@ namespace Nodify.Calculator
{ {
Name = $"Editor {Editors.Count + 1}" Name = $"Editor {Editors.Count + 1}"
}); });
// Auto-load saved graph from project DB
try
{
var firstEditor = Editors.First();
GraphSerializer.Load(firstEditor.Calculator, firstEditor.Calculator.OperationsMenu);
}
catch { }
} }
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator) private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)

View File

@@ -7,6 +7,8 @@ namespace Nodify.Calculator
{ {
public class CalculatorViewModel : ObservableObject public class CalculatorViewModel : ObservableObject
{ {
public bool IsLoading { get; set; }
public CalculatorViewModel() public CalculatorViewModel()
{ {
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>( CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
@@ -27,13 +29,13 @@ namespace Nodify.Calculator
c.Output.ValueObservers.Add(c.Input); c.Output.ValueObservers.Add(c.Input);
// Dynamic Split node: populate outputs when a model connects to the Square input // Dynamic Split node: populate outputs when a model connects to the Square input
HandleSplitNodeConnected(c); if (!IsLoading) HandleSplitNodeConnected(c);
// Dynamic Copy node: adapt connectors when something connects // Dynamic Copy node: adapt connectors when something connects
HandleCopyNodeConnected(c); if (!IsLoading) HandleCopyNodeConnected(c);
// Dynamic Take node: adapt connectors when a list connects // Dynamic Take node: adapt connectors when a list connects
HandleTakeNodeConnected(c); if (!IsLoading) HandleTakeNodeConnected(c);
}) })
.WhenRemoved(c => .WhenRemoved(c =>
{ {
@@ -65,7 +67,7 @@ namespace Nodify.Calculator
Operations.WhenAdded(x => Operations.WhenAdded(x =>
{ {
x.Input.WhenRemoved(RemoveConnection); x.Input.WhenRemoved(RemoveConnection);
x.NodeId = (Operations.Count + 1).ToString(); if (!IsLoading) x.NodeId = (Operations.Count + 1).ToString();
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}"); Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
if (x is CalculatorInputOperationViewModel ci) if (x is CalculatorInputOperationViewModel ci)
{ {

View File

@@ -0,0 +1,420 @@
using Nodify.Calculator.Models;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows;
namespace Nodify.Calculator
{
public static class GraphSerializer
{
public static void Save(CalculatorViewModel calculator)
{
var graph = new SaveGraphModel();
var operations = calculator.Operations;
var connections = calculator.Connections;
// Ensure all nodes have IDs
int idCounter = 1;
foreach (var op in operations)
{
if (string.IsNullOrEmpty(op.NodeId))
op.NodeId = $"node_{idCounter++}";
}
// Serialize nodes
foreach (var op in operations)
{
var nodeData = new NodeData
{
NodeId = op.NodeId,
Title = op.Title ?? string.Empty,
LocationX = op.Location.X,
LocationY = op.Location.Y
};
switch (op)
{
case TakeOperationViewModel take:
nodeData.NodeType = "System";
nodeData.SystemOp = take.SystemOperationType.ToString();
nodeData.NthIndex = take.NthIndex;
nodeData.IsRandom = take.IsRandom;
break;
case FunctionOperationViewModel func:
nodeData.NodeType = "Function";
nodeData.FunctionName = func.FunctionName;
foreach (var p in func.InputParameters)
nodeData.FunctionInputs.Add(new FunctionParamData { Name = p.Name, Type = p.Type });
foreach (var p in func.OutputParameters)
nodeData.FunctionOutputs.Add(new FunctionParamData { Name = p.Name, Type = p.Type });
break;
case SystemOperationViewModel sys:
nodeData.NodeType = "System";
nodeData.SystemOp = sys.SystemOperationType.ToString();
// For GET_SET nodes, derive ClassName from title
if (sys.SystemOperationType == SystemOperations.GET_SET)
{
var title = sys.Title ?? "";
if (title.StartsWith("GET ")) nodeData.ClassName = title.Substring(4).Trim();
else if (title.StartsWith("SET ")) nodeData.ClassName = title.Substring(4).Trim();
}
break;
case APIOperationViewModel api:
nodeData.NodeType = "API";
nodeData.OPType = api.OperationType;
nodeData.ResponseModelClassName = api.ResponseModelClassName;
break;
case ExpandoOperationViewModel:
nodeData.NodeType = "Expando";
break;
case CalculatorOperationViewModel:
nodeData.NodeType = "Calculator";
break;
default:
nodeData.NodeType = "Normal";
break;
}
// Save connector metadata for all nodes
foreach (var c in op.Input)
nodeData.InputConnectors.Add(SerializeConnector(c));
foreach (var c in op.Output)
nodeData.OutputConnectors.Add(SerializeConnector(c));
graph.Nodes.Add(nodeData);
}
// Serialize connections
foreach (var conn in connections)
{
var inputOp = conn.Input?.Operation;
var outputOp = conn.Output?.Operation;
if (inputOp == null || outputOp == null) continue;
var inputIndex = inputOp.Input.IndexOf(conn.Input);
var outputIndex = outputOp.Output.IndexOf(conn.Output);
if (inputIndex < 0 || outputIndex < 0) continue;
graph.Connections.Add(new ConnectionData
{
TargetNodeId = inputOp.NodeId,
TargetConnectorIndex = inputIndex,
SourceNodeId = outputOp.NodeId,
SourceConnectorIndex = outputIndex
});
}
// Save to LiteDB
using var db = new LiteDbHelper<SaveGraphModel>("Graph");
db.DeleteMany(_ => true);
db.Insert(graph);
}
public static void Load(CalculatorViewModel calculator, OperationsMenuViewModel opsMenu)
{
SaveGraphModel graph;
try
{
using var db = new LiteDbHelper<SaveGraphModel>("Graph");
graph = db.FindAll().FirstOrDefault();
}
catch
{
return;
}
if (graph == null || graph.Nodes.Count == 0) return;
// Clear existing
calculator.Connections.Clear();
calculator.Operations.Clear();
calculator.IsLoading = true;
try
{
var nodeMap = new Dictionary<string, OperationViewModel>();
// Recreate nodes
foreach (var nd in graph.Nodes)
{
OperationViewModel op = null;
try
{
op = RecreateNode(nd, opsMenu);
}
catch
{
continue;
}
if (op == null) continue;
op.NodeId = nd.NodeId;
op.Location = new System.Windows.Point(nd.LocationX, nd.LocationY);
nodeMap[nd.NodeId] = op;
calculator.Operations.Add(op);
}
// Recreate connections
foreach (var cd in graph.Connections)
{
if (!nodeMap.TryGetValue(cd.SourceNodeId, out var sourceOp)) continue;
if (!nodeMap.TryGetValue(cd.TargetNodeId, out var targetOp)) continue;
if (cd.SourceConnectorIndex >= sourceOp.Output.Count) continue;
if (cd.TargetConnectorIndex >= targetOp.Input.Count) continue;
var sourceConn = sourceOp.Output[cd.SourceConnectorIndex];
var targetConn = targetOp.Input[cd.TargetConnectorIndex];
calculator.Connections.Add(new ConnectionViewModel
{
Input = targetConn,
Output = sourceConn
});
}
}
finally
{
calculator.IsLoading = false;
}
}
private static OperationViewModel RecreateNode(NodeData nd, OperationsMenuViewModel opsMenu)
{
switch (nd.NodeType)
{
case "API":
{
// Find matching swagger operation or create from saved data
var info = new OperationInfoViewModel
{
Title = nd.Title,
OPType = nd.OPType?.ToLower() ?? "get",
Type = OperationType.API,
ResponseModelClassName = nd.ResponseModelClassName ?? string.Empty
};
// Restore inputs from saved connector data (skip flow triangle at index 0)
foreach (var ic in nd.InputConnectors)
{
if (ic.Shape != "Triangle")
info.Input.Add(ic.Title);
}
info.Output.Add("");
return OperationFactory.GetOperation(info);
}
case "System":
{
if (Enum.TryParse<SystemOperations>(nd.SystemOp, out var sysOp))
{
var info = new OperationInfoViewModel
{
Title = nd.Title,
Type = OperationType.System,
sysOp = sysOp
};
if (sysOp == SystemOperations.GET_SET && !string.IsNullOrEmpty(nd.ClassName))
{
info.IsModelNode = true;
info.ClassName = nd.ClassName;
}
// Set up inputs/outputs based on system operation type
switch (sysOp)
{
case SystemOperations.BEGIN:
info.Output.Add(""); // flow output only
break;
case SystemOperations.END:
info.Input.Add(""); // flow input only
break;
case SystemOperations.TAKE:
info.Input.Add("List");
break;
case SystemOperations.COPY:
info.Input.Add("");
break;
case SystemOperations.SPLIT:
info.Input.Add("");
break;
case SystemOperations.IF:
info.Output.Add("");
info.IsFlowNode = true;
break;
default:
info.Output.Add("");
info.IsFlowNode = true;
break;
}
var op = OperationFactory.GetOperation(info);
// Restore Take properties
if (op is TakeOperationViewModel takeOp)
{
takeOp.NthIndex = nd.NthIndex;
takeOp.IsRandom = nd.IsRandom;
}
// Restore dynamic connectors for SPLIT/COPY/TAKE from saved data
RestoreDynamicConnectors(op, nd);
return op;
}
return null;
}
case "Function":
{
var info = new OperationInfoViewModel
{
Title = nd.FunctionName,
Type = OperationType.System,
sysOp = SystemOperations.FUNCTION,
IsFunction = true
};
foreach (var inp in nd.FunctionInputs)
info.FunctionInputs.Add(new FunctionParameterInfo { Name = inp.Name, Type = inp.Type });
foreach (var outp in nd.FunctionOutputs)
info.FunctionOutputs.Add(new FunctionParameterInfo { Name = outp.Name, Type = outp.Type });
info.Output.Add("");
return OperationFactory.GetOperation(info);
}
case "Expando":
{
var info = new OperationInfoViewModel
{
Title = nd.Title,
Type = OperationType.Expando
};
// Restore input count from saved connectors
foreach (var ic in nd.InputConnectors)
info.Input.Add(ic.Title);
info.Output.Add("");
return OperationFactory.GetOperation(info);
}
default:
{
var info = new OperationInfoViewModel
{
Title = nd.Title,
Type = OperationType.Normal
};
foreach (var ic in nd.InputConnectors)
info.Input.Add(ic.Title);
info.Output.Add("");
return OperationFactory.GetOperation(info);
}
}
}
private static void RestoreDynamicConnectors(OperationViewModel op, NodeData nd)
{
// For SPLIT nodes: if saved outputs have more than just the flow triangle,
// restore the dynamic property outputs
if (op is SystemOperationViewModel sysVm)
{
if (sysVm.SystemOperationType == SystemOperations.SPLIT)
{
// Remove default non-triangle outputs, then add saved ones
var defaultNonTriangle = op.Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList();
foreach (var d in defaultNonTriangle) op.Output.Remove(d);
foreach (var sc in nd.OutputConnectors)
{
if (sc.Shape == "Triangle") continue;
op.Output.Add(DeserializeConnector(sc, false));
}
// Update title from saved
sysVm.Title = nd.Title;
}
else if (sysVm.SystemOperationType == SystemOperations.COPY)
{
// Restore adapted connectors
if (nd.InputConnectors.Count > 0 && nd.InputConnectors[0].Shape != "Circle")
{
// Input was adapted — restore
var savedInput = nd.InputConnectors[0];
if (op.Input.Count > 0)
{
var inp = op.Input[0];
inp.Shape = ParseShape(savedInput.Shape);
inp.ConnectorColor = Color.FromArgb(savedInput.ColorArgb);
inp.DataType = savedInput.DataType;
}
}
// Restore output connectors
var existingOutputs = op.Output.ToList();
foreach (var eo in existingOutputs) op.Output.Remove(eo);
foreach (var sc in nd.OutputConnectors)
{
op.Output.Add(DeserializeConnector(sc, false));
}
}
else if (sysVm.SystemOperationType == SystemOperations.TAKE)
{
// Restore adapted list input
foreach (var inp in op.Input)
{
if (!inp.IsTakeListConnector) continue;
var savedInp = nd.InputConnectors.FirstOrDefault(c => c.IsTakeListConnector);
if (savedInp != null)
{
inp.Shape = ParseShape(savedInp.Shape);
inp.ConnectorColor = Color.FromArgb(savedInp.ColorArgb);
inp.DataType = savedInp.DataType;
}
}
// Restore output
var existingOutputs = op.Output.ToList();
foreach (var eo in existingOutputs) op.Output.Remove(eo);
foreach (var sc in nd.OutputConnectors)
{
op.Output.Add(DeserializeConnector(sc, false));
}
}
}
}
private static ConnectorData SerializeConnector(ConnectorViewModel c)
{
return new ConnectorData
{
Title = c.Title ?? string.Empty,
Shape = c.Shape.ToString(),
ColorArgb = c.RawColor.ToArgb(),
DataType = c.DataType ?? string.Empty,
IsCopyConnector = c.IsCopyConnector,
IsTakeListConnector = c.IsTakeListConnector
};
}
private static ConnectorViewModel DeserializeConnector(ConnectorData cd, bool isInput)
{
return new ConnectorViewModel
{
Title = cd.Title,
Shape = ParseShape(cd.Shape),
ConnectorColor = Color.FromArgb(cd.ColorArgb),
DataType = cd.DataType,
IsCopyConnector = cd.IsCopyConnector,
IsTakeListConnector = cd.IsTakeListConnector,
IsInput = isInput
};
}
private static ConnectorShape ParseShape(string shape)
{
return Enum.TryParse<ConnectorShape>(shape, out var s) ? s : ConnectorShape.Circle;
}
}
}

View File

@@ -1,4 +1,6 @@
using Nodify.Interactivity; using Nodify.Interactivity;
using System.ComponentModel;
using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
@@ -16,6 +18,39 @@ namespace Nodify.Calculator
typeof(UIElement), typeof(UIElement),
Keyboard.PreviewGotKeyboardFocusEvent, Keyboard.PreviewGotKeyboardFocusEvent,
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus); (KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
Closing += MainWindow_Closing;
}
private void MainWindow_Closing(object sender, CancelEventArgs e)
{
var result = MessageBox.Show(
"Do you want to save the project before closing?",
"Save Project",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel)
{
e.Cancel = true;
return;
}
if (result == MessageBoxResult.Yes)
{
try
{
if (DataContext is ApplicationViewModel appVm && appVm.Editors.Count > 0)
{
var firstEditor = appVm.Editors.First();
GraphSerializer.Save(firstEditor.Calculator);
}
}
catch (System.Exception ex)
{
MessageBox.Show($"Failed to save: {ex.Message}", "Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
} }
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)

View File

@@ -1,20 +1,75 @@
using Microsoft.CodeAnalysis; using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
namespace Nodify.Calculator.Models namespace Nodify.Calculator.Models
{ {
public class SaveGraphModel public class SaveGraphModel
{ {
public string Name { get; set; } public int Id { get; set; } = 1;
public List<NodeData> Nodes { get; set; } = new List<NodeData>();
public List<SaveNodes> Nodes { get; set; } = new List<SaveNodes>(); public List<ConnectionData> Connections { get; set; } = new List<ConnectionData>();
} }
public class NodeData
{
/// <summary>Unique ID matching OperationViewModel.NodeId</summary>
public string NodeId { get; set; } = string.Empty;
/// <summary>Discriminator: "API", "System", "Expando", "Calculator", "Function"</summary>
public string NodeType { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public double LocationX { get; set; }
public double LocationY { get; set; }
// API node properties
public string OPType { get; set; } = string.Empty;
public string ResponseModelClassName { get; set; } = string.Empty;
// System node properties
public string SystemOp { get; set; } = string.Empty;
public string ClassName { get; set; } = string.Empty;
// Take node properties
public int NthIndex { get; set; }
public bool IsRandom { get; set; }
// Function node properties
public string FunctionName { get; set; } = string.Empty;
public List<FunctionParamData> FunctionInputs { get; set; } = new List<FunctionParamData>();
public List<FunctionParamData> FunctionOutputs { get; set; } = new List<FunctionParamData>();
/// <summary>Saved input connector metadata (for dynamic nodes like SPLIT/COPY/TAKE)</summary>
public List<ConnectorData> InputConnectors { get; set; } = new List<ConnectorData>();
/// <summary>Saved output connector metadata</summary>
public List<ConnectorData> OutputConnectors { get; set; } = new List<ConnectorData>();
}
public class ConnectorData
{
public string Title { get; set; } = string.Empty;
public string Shape { get; set; } = "Circle";
public int ColorArgb { get; set; }
public string DataType { get; set; } = string.Empty;
public bool IsCopyConnector { get; set; }
public bool IsTakeListConnector { get; set; }
}
public class ConnectionData
{
public string SourceNodeId { get; set; } = string.Empty;
public int SourceConnectorIndex { get; set; }
public string TargetNodeId { get; set; } = string.Empty;
public int TargetConnectorIndex { get; set; }
}
public class FunctionParamData
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
}
// Keep legacy for compatibility
public class SaveNodes public class SaveNodes
{ {
public Point Location { get; set; } public Point Location { get; set; }