diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs
index 1635664..b429c83 100644
--- a/Examples/Nodify.Calculator/CalculatorViewModel.cs
+++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs
@@ -32,9 +32,6 @@ namespace Nodify.Calculator
// Dynamic Split node: populate outputs when a model connects to the Square input
if (!IsLoading) HandleSplitNodeConnected(c);
- // Dynamic New Object node: populate inputs when a model connects
- if (!IsLoading) HandleNewObjectNodeConnected(c);
-
// Dynamic Copy node: adapt connectors when something connects
if (!IsLoading) HandleCopyNodeConnected(c);
@@ -73,9 +70,6 @@ namespace Nodify.Calculator
// Dynamic Split node: clear outputs when model disconnects
HandleSplitNodeDisconnected(c);
- // Dynamic New Object node: clear inputs when model disconnects
- HandleNewObjectNodeDisconnected(c);
-
// Dynamic Copy node: reset on disconnect
HandleCopyNodeDisconnected(c);
@@ -437,144 +431,6 @@ namespace Nodify.Calculator
sysVm.Title = "Split";
}
- private void HandleNewObjectNodeConnected(ConnectionViewModel c)
- {
- // Find the New Object node's Square input connector
- var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
- if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
- if (squareInput == null) return;
-
- var newObjOp = squareInput.Operation;
- if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT)
- return;
-
- // The other end is the source — find which model class it comes from
- var sourceConnector = (c.Input == squareInput) ? c.Output : c.Input;
- var sourceOp = sourceConnector.Operation;
-
- string className = null;
- if (sourceOp is SystemOperationViewModel srcSys)
- {
- var title = srcSys.Title ?? "";
- if (title.StartsWith("GET ")) className = title.Substring(4).Trim();
- else if (title.StartsWith("SET ")) className = title.Split(' ').ElementAtOrDefault(1);
- }
- else if (sourceOp is APIOperationViewModel apiVm)
- {
- className = apiVm.ResponseModelClassName;
- if (!string.IsNullOrEmpty(className) && className.StartsWith("List<") && className.EndsWith(">"))
- className = className.Substring(5, className.Length - 6);
- }
-
- if (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(sourceConnector.DataType))
- {
- var dt = sourceConnector.DataType;
- if (dt.StartsWith("List<") && dt.EndsWith(">"))
- dt = dt.Substring(5, dt.Length - 6);
- if (!new[] { "string", "int", "double", "bool", "object" }.Contains(dt.ToLower()))
- className = dt;
- }
-
- if (string.IsNullOrEmpty(className)) return;
-
- var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
- var filePath = Path.Combine(customModelDir, $"{className}.cs");
- if (!File.Exists(filePath)) return;
-
- var fileContent = File.ReadAllText(filePath);
- var properties = OperationFactory.GetPropertiesFromClassPublic(fileContent);
- if (properties == null || properties.Count == 0) return;
-
- // Remove existing dynamic inputs (non-triangle, non-square)
- var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
- toRemove.ForEach(i =>
- {
- DisconnectConnector(i);
- newObjOp.Input.Remove(i);
- });
-
- // Add property inputs
- foreach (var prop in properties)
- {
- var propType = prop.Type;
- bool isList = propType.StartsWith("List<") && propType.EndsWith(">");
- var innerType = isList ? propType.Substring(5, propType.Length - 6) : propType;
-
- var primTypes = new[] { "string", "int", "double", "bool", "float", "decimal", "long", "object", "datetime" };
- bool isModel = !primTypes.Contains(innerType.ToLower())
- && File.Exists(Path.Combine(customModelDir, $"{innerType}.cs"));
-
- ConnectorShape shape;
- System.Drawing.Color color;
-
- if (isList)
- {
- shape = ConnectorShape.Grid;
- color = System.Drawing.Color.MediumSpringGreen;
- }
- else if (isModel)
- {
- shape = ConnectorShape.Square;
- color = System.Drawing.Color.MediumPurple;
- }
- else
- {
- shape = ConnectorShape.Circle;
- color = ConnectorViewModel.GetColorForType(propType);
- }
-
- newObjOp.Input.Add(new ConnectorViewModel
- {
- Title = $"{prop.Name} ({propType})",
- Shape = shape,
- ConnectorColor = color,
- DataType = isModel || isList ? propType : propType.ToLower()
- });
- }
-
- // Update the output connector to reflect the model type
- var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
- if (modelOutput != null)
- {
- modelOutput.Title = className;
- modelOutput.DataType = className;
- }
-
- sysVm.Title = $"New {className}";
- }
-
- private void HandleNewObjectNodeDisconnected(ConnectionViewModel c)
- {
- var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
- if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
- if (squareInput == null) return;
-
- var newObjOp = squareInput.Operation;
- if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT)
- return;
-
- var stillConnected = Connections.Any(con => con.Input == squareInput || con.Output == squareInput);
- if (stillConnected) return;
-
- // Remove dynamic property inputs (keep triangle and square connectors)
- var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
- toRemove.ForEach(i =>
- {
- DisconnectConnector(i);
- newObjOp.Input.Remove(i);
- });
-
- // Reset output
- var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
- if (modelOutput != null)
- {
- modelOutput.Title = "Object";
- modelOutput.DataType = "object";
- }
-
- sysVm.Title = "New Object";
- }
-
private void HandleCopyNodeConnected(ConnectionViewModel c)
{
// Find the COPY node's input connector
diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml
index c40acde..e4b68d8 100644
--- a/Examples/Nodify.Calculator/EditorView.xaml
+++ b/Examples/Nodify.Calculator/EditorView.xaml
@@ -521,6 +521,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.NEW_OBJECT;
public void Execute(OperationViewModel node, ExecutionContext ctx)
{
- ctx.Log($"Constructing new object: {node.Title}");
- var jObj = new JObject();
+ ctx.Log($"[NEW OBJECT] Constructing: {node.Title}");
+ var root = new JObject();
foreach (var inp in node.Input)
{
- if (inp.Shape == ConnectorShape.Triangle || inp.Shape == ConnectorShape.Square) continue;
+ if (inp.Shape == ConnectorShape.Triangle) continue;
- var propName = inp.Title ?? "";
- var parenIdx = propName.IndexOf(" (", StringComparison.Ordinal);
- if (parenIdx > 0) propName = propName.Substring(0, parenIdx);
+ // Extract property path from title: "Address.Street (string)" → "Address.Street"
+ var title = inp.Title ?? "";
+ var parenIdx = title.IndexOf(" (", StringComparison.Ordinal);
+ var propPath = parenIdx > 0 ? title.Substring(0, parenIdx).Trim() : title.Trim();
+ if (string.IsNullOrEmpty(propPath)) continue;
var val = ctx.ReadInput(inp);
- if (val != null)
- {
- try { jObj[propName] = JToken.Parse(val); }
- catch { jObj[propName] = val; }
- }
+ ctx.Log($"[NEW OBJECT] {propPath} = {(val?.Length > 80 ? val.Substring(0, 80) + "..." : val ?? "null")}");
+
+ SetNestedValue(root, propPath, val);
+ }
+
+ var json = root.ToString(Formatting.None);
+ ctx.Outputs[node.NodeId] = json;
+ ctx.Variables[node.NodeId] = json;
+ ctx.Log($"[NEW OBJECT] Result: {(json.Length > 200 ? json.Substring(0, 200) + "..." : json)}");
+ }
+
+ ///
+ /// Sets a value at a dot-notation path in a JObject, creating intermediate objects as needed.
+ /// e.g., "Address.Street" with value "Main St" → { "Address": { "Street": "Main St" } }
+ ///
+ private static void SetNestedValue(JObject root, string path, string val)
+ {
+ var parts = path.Split('.');
+ var current = root;
+
+ for (int i = 0; i < parts.Length - 1; i++)
+ {
+ if (current[parts[i]] is JObject existing)
+ current = existing;
else
{
- jObj[propName] = null;
+ var child = new JObject();
+ current[parts[i]] = child;
+ current = child;
}
}
- var json = jObj.ToString(Formatting.None);
- ctx.Outputs[node.NodeId] = json;
- ctx.Variables[node.NodeId] = json;
- ctx.Log($"Object constructed: {json}");
+ var leaf = parts[parts.Length - 1];
+ if (val != null)
+ {
+ try { current[leaf] = JToken.Parse(val); }
+ catch { current[leaf] = val; }
+ }
+ else
+ {
+ current[leaf] = null;
+ }
}
}
diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs
index 4111544..2efd72a 100644
--- a/Examples/Nodify.Calculator/Executor.cs
+++ b/Examples/Nodify.Calculator/Executor.cs
@@ -55,7 +55,7 @@ namespace Nodify.Calculator
new AuthHandler(),
new KnotHandler(),
new DebugHandler(),
- new NewObjectHandler(),
+ new NewObjectExecHandler(),
new AssertHandler(),
new ForEachHandler(),
new Execution.Handlers.DeserializeHandler(),
diff --git a/Examples/Nodify.Calculator/NewObjectOperationViewModel.cs b/Examples/Nodify.Calculator/NewObjectOperationViewModel.cs
new file mode 100644
index 0000000..47bbce8
--- /dev/null
+++ b/Examples/Nodify.Calculator/NewObjectOperationViewModel.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+
+namespace Nodify.Calculator
+{
+ public class NewObjectOperationViewModel : SystemOperationViewModel
+ {
+ private string _selectedModel = "";
+
+ public ObservableCollection AvailableModels { get; } = new ObservableCollection();
+
+ public string SelectedModel
+ {
+ get => _selectedModel;
+ set
+ {
+ if (SetProperty(ref _selectedModel, value))
+ {
+ RebuildInputs();
+ }
+ }
+ }
+
+ public NewObjectOperationViewModel()
+ {
+ Title = "New Object";
+ SystemOperationType = SystemOperations.NEW_OBJECT;
+ RefreshAvailableModels();
+ }
+
+ public void RefreshAvailableModels()
+ {
+ var current = _selectedModel;
+ AvailableModels.Clear();
+ AvailableModels.Add("");
+
+ var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
+ if (Directory.Exists(customModelDir))
+ {
+ var modelFiles = Directory.GetFiles(customModelDir, "*.cs");
+ foreach (var file in modelFiles)
+ {
+ AvailableModels.Add(Path.GetFileNameWithoutExtension(file));
+ }
+ }
+
+ if (!string.IsNullOrEmpty(current) && AvailableModels.Contains(current))
+ _selectedModel = current;
+ else
+ _selectedModel = "";
+ }
+
+ public void RebuildInputs()
+ {
+ // Keep only Triangle (flow) connectors
+ var toRemove = Input.Where(i => i.Shape != ConnectorShape.Triangle).ToList();
+ foreach (var c in toRemove) Input.Remove(c);
+
+ // Update output connector
+ var modelOutput = Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
+ if (modelOutput != null)
+ {
+ modelOutput.Title = string.IsNullOrEmpty(_selectedModel) ? "Object" : _selectedModel;
+ modelOutput.DataType = string.IsNullOrEmpty(_selectedModel) ? "object" : _selectedModel;
+ }
+
+ if (string.IsNullOrEmpty(_selectedModel))
+ {
+ Title = "New Object";
+ return;
+ }
+
+ Title = $"New {_selectedModel}";
+
+ // Recursively flatten model properties with dot notation
+ var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
+ var visited = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var flatProps = new List>();
+ FlattenModel(_selectedModel, "", customModelDir, visited, flatProps);
+
+ foreach (var entry in flatProps)
+ {
+ var path = entry.Item1;
+ var type = entry.Item2;
+ var isModel = entry.Item3;
+ var isList = entry.Item4;
+ ConnectorShape shape;
+ Color color;
+
+ if (isList)
+ {
+ shape = ConnectorShape.Grid;
+ color = NodeColors.List;
+ }
+ else if (isModel)
+ {
+ // For nested model types we still use Circle with a label since
+ // the user will type/connect primitives via dot notation
+ shape = ConnectorShape.Square;
+ color = NodeColors.Model;
+ }
+ else
+ {
+ shape = ConnectorShape.Circle;
+ color = ConnectorViewModel.GetColorForType(type);
+ }
+
+ Input.Add(new ConnectorViewModel
+ {
+ Title = $"{path} ({type})",
+ Shape = shape,
+ ConnectorColor = color,
+ DataType = isModel || isList ? type : type.ToLower()
+ });
+ }
+ }
+
+ ///
+ /// Recursively flattens a model's properties. Nested model properties are expanded
+ /// with dot notation (e.g., "Address.Street (string)"). Primitives and lists are leaves.
+ ///
+ private static void FlattenModel(string modelName, string prefix, string modelDir,
+ HashSet visited, List> result)
+ {
+ if (visited.Contains(modelName)) return; // prevent circular references
+ visited.Add(modelName);
+
+ var filePath = Path.Combine(modelDir, $"{modelName}.cs");
+ if (!File.Exists(filePath)) return;
+
+ var content = File.ReadAllText(filePath);
+ var properties = OperationFactory.GetPropertiesFromClassPublic(content);
+ if (properties == null) return;
+
+ var primTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "string", "int", "double", "bool", "float", "decimal", "long",
+ "object", "datetime", "byte", "short", "char", "guid"
+ };
+
+ foreach (var prop in properties)
+ {
+ var propType = prop.Type;
+ var fullPath = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
+
+ bool isList = propType.StartsWith("List<") && propType.EndsWith(">");
+ var innerType = isList ? propType.Substring(5, propType.Length - 6) : propType;
+
+ bool isNestedModel = !primTypes.Contains(innerType)
+ && File.Exists(Path.Combine(modelDir, $"{innerType}.cs"));
+
+ if (isList)
+ {
+ // List properties are always leaf inputs (Grid connector)
+ result.Add(new System.ValueTuple(fullPath, propType, false, true));
+ }
+ else if (isNestedModel)
+ {
+ // Recurse into nested model with dot notation prefix
+ FlattenModel(innerType, fullPath, modelDir, visited, result);
+ }
+ else
+ {
+ // Primitive leaf
+ result.Add(new System.ValueTuple(fullPath, propType, false, false));
+ }
+ }
+
+ visited.Remove(modelName); // allow same model in different branches
+ }
+ }
+}
diff --git a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs
index 129c7c8..763fd9d 100644
--- a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs
+++ b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs
@@ -97,6 +97,7 @@ namespace Nodify.Calculator.NodeHandlers
TakeOperationViewModel => _handlers.OfType().FirstOrDefault(),
ForEachOperationViewModel => _handlers.OfType().FirstOrDefault(),
AssertOperationViewModel => _handlers.OfType().FirstOrDefault(),
+ NewObjectOperationViewModel => _handlers.OfType().FirstOrDefault(),
APIOperationViewModel => _handlers.OfType().FirstOrDefault(),
ExpandoOperationViewModel => _handlers.OfType().FirstOrDefault(),
CalculatorOperationViewModel => _handlers.OfType().FirstOrDefault(),
diff --git a/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs b/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs
index 4e1d71d..f252ada 100644
--- a/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs
+++ b/Examples/Nodify.Calculator/NodeHandlers/SpecializedNodeHandlers.cs
@@ -82,40 +82,41 @@ namespace Nodify.Calculator.NodeHandlers
public OperationViewModel Create(OperationInfoViewModel info)
{
- var op = new SystemOperationViewModel
- {
- Title = info.Title ?? "New Object",
- SystemOperationType = SystemOperations.NEW_OBJECT
- };
+ var op = new NewObjectOperationViewModel();
op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
op.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
- op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple });
op.Output.Add(new ConnectorViewModel { Title = "Object", IsInput = false, Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple, DataType = "object" });
return op;
}
public OperationViewModel Restore(NodeData data)
{
- var op = (SystemOperationViewModel)Create(new OperationInfoViewModel { Title = data.Title, Type = OperationType.System, sysOp = SystemOperations.NEW_OBJECT });
+ var op = new NewObjectOperationViewModel();
+ op.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
+ op.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
+ op.Output.Add(new ConnectorViewModel { Title = "Object", IsInput = false, Shape = ConnectorShape.Square, ConnectorColor = Color.MediumPurple, DataType = "object" });
- // Restore dynamic property inputs (keep triangle + square, replace the rest)
- var dynamicInputs = op.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
- foreach (var d in dynamicInputs) op.Input.Remove(d);
-
- foreach (var ic in data.InputConnectors)
+ // Restore selected model (triggers RebuildInputs via property setter)
+ if (!string.IsNullOrEmpty(data.ClassName))
{
- if (ic.Shape == "Triangle" || ic.Shape == "Square") continue;
- op.Input.Add(NodeHandlerRegistry.DeserializeConnector(ic, true));
+ op.RefreshAvailableModels();
+ op.SelectedModel = data.ClassName;
}
-
- // Restore output connector metadata (Square model output)
- var savedModelOutput = data.OutputConnectors.FirstOrDefault(o => o.Shape == "Square");
- var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
- if (savedModelOutput != null && modelOutput != null)
+ else
{
- modelOutput.Title = savedModelOutput.Title;
- modelOutput.DataType = savedModelOutput.DataType;
- modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb);
+ // Legacy: restore from saved input connectors
+ var dynamicInputs = data.InputConnectors.Where(ic => ic.Shape != "Triangle").ToList();
+ foreach (var ic in dynamicInputs)
+ op.Input.Add(NodeHandlerRegistry.DeserializeConnector(ic, true));
+
+ var savedModelOutput = data.OutputConnectors.FirstOrDefault(o => o.Shape == "Square");
+ var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
+ if (savedModelOutput != null && modelOutput != null)
+ {
+ modelOutput.Title = savedModelOutput.Title;
+ modelOutput.DataType = savedModelOutput.DataType;
+ modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb);
+ }
}
op.Title = data.Title;
@@ -126,6 +127,10 @@ namespace Nodify.Calculator.NodeHandlers
{
data.NodeType = "System";
data.SystemOp = nameof(SystemOperations.NEW_OBJECT);
+ if (vm is NewObjectOperationViewModel newObjVm)
+ {
+ data.ClassName = newObjVm.SelectedModel ?? "";
+ }
}
}