diff --git a/Examples/Nodify.Calculator/CreateFunctionDialog.xaml b/Examples/Nodify.Calculator/CreateFunctionDialog.xaml
new file mode 100644
index 0000000..cf46ca6
--- /dev/null
+++ b/Examples/Nodify.Calculator/CreateFunctionDialog.xaml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ string
+ int
+ double
+ bool
+ float
+ decimal
+ long
+ DateTime
+ object
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ string
+ int
+ double
+ bool
+ float
+ decimal
+ long
+ DateTime
+ object
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Examples/Nodify.Calculator/CreateFunctionDialog.xaml.cs b/Examples/Nodify.Calculator/CreateFunctionDialog.xaml.cs
new file mode 100644
index 0000000..845456c
--- /dev/null
+++ b/Examples/Nodify.Calculator/CreateFunctionDialog.xaml.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Windows;
+
+namespace Nodify.Calculator
+{
+ public partial class CreateFunctionDialog : Window
+ {
+ public string FunctionName { get; private set; } = string.Empty;
+ public List InputParameters { get; } = new List();
+ public List OutputParameters { get; } = new List();
+
+ private readonly ObservableCollection _inputs = new ObservableCollection();
+ private readonly ObservableCollection _outputs = new ObservableCollection();
+
+ public CreateFunctionDialog()
+ {
+ InitializeComponent();
+ InputParamsGrid.ItemsSource = _inputs;
+ OutputParamsGrid.ItemsSource = _outputs;
+ }
+
+ private void AddInput_Click(object sender, RoutedEventArgs e)
+ {
+ _inputs.Add(new FunctionParameterInfo { Name = $"param{_inputs.Count + 1}", Type = "string" });
+ }
+
+ private void AddOutput_Click(object sender, RoutedEventArgs e)
+ {
+ _outputs.Add(new FunctionParameterInfo { Name = $"result{_outputs.Count + 1}", Type = "string" });
+ }
+
+ private void OnCreateClick(object sender, RoutedEventArgs e)
+ {
+ if (string.IsNullOrWhiteSpace(FunctionNameBox.Text))
+ {
+ MessageBox.Show("Please enter a function name.", "Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ FunctionName = FunctionNameBox.Text.Trim();
+ InputParameters.AddRange(_inputs);
+ OutputParameters.AddRange(_outputs);
+ DialogResult = true;
+ }
+
+ private void OnCancelClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+ }
+}
diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml
index a56e430..d24b30f 100644
--- a/Examples/Nodify.Calculator/EditorView.xaml
+++ b/Examples/Nodify.Calculator/EditorView.xaml
@@ -433,6 +433,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs
index ffb806a..9d3ec6e 100644
--- a/Examples/Nodify.Calculator/Executor.cs
+++ b/Examples/Nodify.Calculator/Executor.cs
@@ -428,6 +428,14 @@ namespace Nodify.Calculator
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
return;
}
+
+ // Handle Function nodes — execute the inner flow
+ if (op is FunctionOperationViewModel funcOp)
+ {
+ OnLogMe?.Invoke($"Executing function: {funcOp.FunctionName}");
+ ExecuteFunction(funcOp, connections);
+ return;
+ }
OnLogMe?.Invoke($"Starting Execution : {url}");
var res = GetResponse(url, "get");
if (!string.IsNullOrEmpty(res))
@@ -509,16 +517,83 @@ namespace Nodify.Calculator
private static string GetConnectorTextValue(ConnectorViewModel connector)
{
- // When the connector is not connected, the user enters text in the Value text box.
- // Value is a double, so we try to use it as a string representation.
- // However, for string inputs (URL, token, etc.) the value won't be meaningful as a double.
- // The user-typed string is actually stored in the Title field for unconnected inputs in some cases,
- // but here we rely on the ConnectorViewModel.Value being 0 (default) and the actual string
- // is not captured as double. So we return empty — the AuthOperationViewModel properties are the
- // primary source set via the node's text boxes.
return string.Empty;
}
+ private void ExecuteFunction(FunctionOperationViewModel funcOp, ICollection connections)
+ {
+ // Propagate outer input values into the function
+ for (int i = 0; i < funcOp.InputParameters.Count; i++)
+ {
+ var outerInputIndex = i + 1; // skip flow triangle
+ if (outerInputIndex < funcOp.Input.Count)
+ {
+ var outerConnector = funcOp.Input[outerInputIndex];
+ var inputCon = connections.FirstOrDefault(c => c.Input == outerConnector);
+ if (inputCon != null)
+ {
+ var sourceNodeId = inputCon.Output.Operation.NodeId;
+ if (outputs.TryGetValue(sourceNodeId, out var val))
+ {
+ outerConnector.Value = double.TryParse(val, out var d) ? d : 0;
+ }
+ }
+ }
+ }
+
+ // Trigger propagation of input values to inner Begin node
+ funcOp.PropagateInputsToInner();
+
+ // Execute the inner calculator's flow (Begin -> End)
+ var innerOps = funcOp.InnerCalculator.Operations;
+ var innerConns = funcOp.InnerCalculator.Connections;
+
+ OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] Resolving inner GET variables...");
+ ResolveGetVariableNodes(innerOps, innerConns);
+
+ OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] Executing inner chain...");
+ var innerStart = innerOps.FirstOrDefault(n => n.Title?.Equals("Begin", StringComparison.OrdinalIgnoreCase) == true);
+ if (innerStart == null)
+ {
+ OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] No Begin node found inside function!", logType.Error);
+ return;
+ }
+
+ var visited = new HashSet();
+ TraverseChain(innerStart, "end", innerConns, visited, true);
+
+ // Collect outputs from the inner End node
+ var innerEnd = innerOps.FirstOrDefault(n => n.Title?.Equals("End", StringComparison.OrdinalIgnoreCase) == true);
+ if (innerEnd != null)
+ {
+ for (int i = 0; i < funcOp.OutputParameters.Count; i++)
+ {
+ var endInputIndex = i + 1; // skip flow triangle
+ if (endInputIndex < innerEnd.Input.Count)
+ {
+ var endConnector = innerEnd.Input[endInputIndex];
+ var endCon = innerConns.FirstOrDefault(c => c.Input == endConnector);
+ if (endCon != null)
+ {
+ var sourceId = endCon.Output.Operation.NodeId;
+ if (outputs.TryGetValue(sourceId, out var resultVal))
+ {
+ var outerOutputIndex = i + 1; // skip flow triangle
+ if (outerOutputIndex < funcOp.Output.Count)
+ {
+ funcOp.Output[outerOutputIndex].Value = double.TryParse(resultVal, out var dv) ? dv : 0;
+ }
+ outputs[funcOp.NodeId] = resultVal;
+ OnLogMe?.Invoke($" [Function '{funcOp.FunctionName}'] Output '{funcOp.OutputParameters[i].Name}' = {resultVal}");
+ }
+ }
+ }
+ }
+ }
+
+ OnLogMe?.Invoke($"Function '{funcOp.FunctionName}' execution completed.");
+ }
+
private string GetResponse(string url, string type)
{
string baseURL = !string.IsNullOrWhiteSpace(_authBaseUrl) ? _authBaseUrl : "https://localhost:7107";
diff --git a/Examples/Nodify.Calculator/FunctionOperationViewModel.cs b/Examples/Nodify.Calculator/FunctionOperationViewModel.cs
new file mode 100644
index 0000000..7d0b98b
--- /dev/null
+++ b/Examples/Nodify.Calculator/FunctionOperationViewModel.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+using System.Windows;
+
+namespace Nodify.Calculator
+{
+ public class FunctionOperationViewModel : CalculatorOperationViewModel
+ {
+ private string _functionName = "Function";
+ public string FunctionName
+ {
+ get => _functionName;
+ set
+ {
+ if (SetProperty(ref _functionName, value))
+ {
+ Title = value;
+ }
+ }
+ }
+
+ public List InputParameters { get; } = new List();
+ public List OutputParameters { get; } = new List();
+
+ private OperationViewModel InnerBegin { get; }
+ private OperationViewModel InnerEnd { get; }
+
+ public FunctionOperationViewModel()
+ {
+ // Add Begin and End nodes inside the inner calculator for flow-based editing
+ InnerBegin = new SystemOperationViewModel
+ {
+ Title = "Begin",
+ SystemOperationType = SystemOperations.BEGIN,
+ Location = new Point(50, 150)
+ };
+ InnerBegin.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
+
+ InnerEnd = new SystemOperationViewModel
+ {
+ Title = "End",
+ SystemOperationType = SystemOperations.END,
+ Location = new Point(600, 150)
+ };
+ InnerEnd.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
+
+ InnerCalculator.Operations.Add(InnerBegin);
+ InnerCalculator.Operations.Add(InnerEnd);
+ }
+
+ public void ConfigureParameters(List inputs, List outputs)
+ {
+ InputParameters.Clear();
+ InputParameters.AddRange(inputs);
+ OutputParameters.Clear();
+ OutputParameters.AddRange(outputs);
+
+ // Add data output connectors to InnerBegin (these feed the inner flow with input values)
+ foreach (var inp in inputs)
+ {
+ InnerBegin.Output.Add(new ConnectorViewModel
+ {
+ Title = $"{inp.Name} ({inp.Type})",
+ Shape = ConnectorShape.Circle,
+ IsInput = false
+ });
+
+ // Also add an outer input connector on the function node
+ Input.Add(new ConnectorViewModel
+ {
+ Title = $"{inp.Name} ({inp.Type})"
+ });
+ }
+
+ // Add data input connectors to InnerEnd (these collect inner flow results)
+ foreach (var outp in outputs)
+ {
+ InnerEnd.Input.Add(new ConnectorViewModel
+ {
+ Title = $"{outp.Name} ({outp.Type})",
+ Shape = ConnectorShape.Circle
+ });
+
+ // Also add an outer output connector on the function node
+ Output.Add(new ConnectorViewModel
+ {
+ Title = $"{outp.Name} ({outp.Type})",
+ IsInput = false
+ });
+ }
+ }
+
+ protected override void OnInputValueChanged()
+ {
+ PropagateInputsToInner();
+ }
+
+ public void PropagateInputsToInner()
+ {
+ // Propagate outer input values into the InnerBegin's data output connectors
+ // The first output of InnerBegin is the flow triangle, data starts at index 1
+ for (var i = 0; i < InputParameters.Count; i++)
+ {
+ var outerIndex = i; // outer Input: first is flow triangle (index 0), data starts at 1
+ var innerIndex = i + 1; // InnerBegin Output: first is flow triangle, data starts at 1
+
+ if (outerIndex + 1 < Input.Count && innerIndex < InnerBegin.Output.Count)
+ {
+ InnerBegin.Output[innerIndex].Value = Input[outerIndex + 1].Value;
+ }
+ }
+ }
+ }
+
+ public class FunctionParameterInfo
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Type { get; set; } = "string";
+ }
+}
diff --git a/Examples/Nodify.Calculator/OperationInfoViewModel.cs b/Examples/Nodify.Calculator/OperationInfoViewModel.cs
index 171e743..e038130 100644
--- a/Examples/Nodify.Calculator/OperationInfoViewModel.cs
+++ b/Examples/Nodify.Calculator/OperationInfoViewModel.cs
@@ -32,5 +32,8 @@ namespace Nodify.Calculator
public string VariableType { get; set; } = string.Empty;
public string DefaultValue { get; set; } = string.Empty;
public bool IsSimpleVariable { get; set; }
+ public bool IsFunction { get; set; }
+ public List FunctionInputs { get; set; } = new List();
+ public List FunctionOutputs { get; set; } = new List();
}
}
diff --git a/Examples/Nodify.Calculator/Operations/OperationFactory.cs b/Examples/Nodify.Calculator/Operations/OperationFactory.cs
index 9a88ab6..70a082c 100644
--- a/Examples/Nodify.Calculator/Operations/OperationFactory.cs
+++ b/Examples/Nodify.Calculator/Operations/OperationFactory.cs
@@ -275,6 +275,24 @@ namespace Nodify.Calculator
//_o.Input.AddRange(input);
return _o;
case OperationType.System:
+ if (info.sysOp == SystemOperations.FUNCTION)
+ {
+ var funcOp = new FunctionOperationViewModel
+ {
+ Title = info.Title,
+ FunctionName = info.Title
+ };
+ // Add flow connectors (triangle)
+ var funcFlowIn = new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle };
+ var funcFlowOut = new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false };
+ funcOp.Input.Add(funcFlowIn);
+ funcOp.Output.Add(funcFlowOut);
+
+ // Configure typed input/output parameters
+ funcOp.ConfigureParameters(info.FunctionInputs, info.FunctionOutputs);
+ return funcOp;
+ }
+
if (info.sysOp == SystemOperations.AUTH)
{
var authOp = new AuthOperationViewModel
diff --git a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs
index 1509a75..5e382bd 100644
--- a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs
+++ b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs
@@ -113,6 +113,7 @@ namespace Nodify.Calculator
public NodifyObservableCollection SwaggerOperations { get; }
public NodifyObservableCollection AvailableModels { get; }
public NodifyObservableCollection AvailableVariables { get; }
+ public NodifyObservableCollection AvailableFunctions { get; }
public INodifyCommand CreateOperationCommand { get; }
[Newtonsoft.Json.JsonIgnore]
@@ -125,6 +126,7 @@ namespace Nodify.Calculator
List operations = new List();
AvailableModels = new NodifyObservableCollection();
AvailableVariables = new NodifyObservableCollection();
+ AvailableFunctions = new NodifyObservableCollection();
LoadVariablesFromDb();
var customModelDir = "CustomModels";
@@ -158,6 +160,7 @@ namespace Nodify.Calculator
CreateOperationCommand = new DelegateCommand(CreateOperation);
ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
AddVariableCommand = new DelegateCommand(AddVariable);
+ CreateFunctionCommand = new DelegateCommand(CreateFunction);
}
@@ -180,6 +183,37 @@ namespace Nodify.Calculator
public INodifyCommand ImportSwaggerCommand { get; }
public INodifyCommand AddVariableCommand { get; }
+ public INodifyCommand CreateFunctionCommand { get; }
+
+ private void CreateFunction()
+ {
+ var dialog = new CreateFunctionDialog();
+ dialog.Owner = System.Windows.Application.Current.MainWindow;
+ if (dialog.ShowDialog() != true)
+ return;
+
+ var funcName = dialog.FunctionName;
+
+ if (AvailableFunctions.Any(f => f.Title == funcName))
+ {
+ MessageBox.Show($"A function named '{funcName}' already exists.", "Duplicate Function", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var funcInfo = new OperationInfoViewModel
+ {
+ Title = funcName,
+ Type = OperationType.System,
+ sysOp = SystemOperations.FUNCTION,
+ IsFlowNode = true,
+ IsFunction = true,
+ FunctionInputs = new System.Collections.Generic.List(dialog.InputParameters),
+ FunctionOutputs = new System.Collections.Generic.List(dialog.OutputParameters)
+ };
+
+ AvailableFunctions.Add(funcInfo);
+ AvailableOperations.Add(funcInfo);
+ }
private void AddVariable()
{
diff --git a/Examples/Nodify.Calculator/SystemOperationViewModel.cs b/Examples/Nodify.Calculator/SystemOperationViewModel.cs
index 5100d2c..3be161b 100644
--- a/Examples/Nodify.Calculator/SystemOperationViewModel.cs
+++ b/Examples/Nodify.Calculator/SystemOperationViewModel.cs
@@ -17,7 +17,8 @@ namespace Nodify.Calculator
GET_SET,
PARSEJSON,
SPLIT,
- AUTH
+ AUTH,
+ FUNCTION
}
public class SystemOperationViewModel : OperationViewModel