Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/Connectors-Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ Node input and node output are implementations of `Connector` with a `Header` th

![image](https://user-images.githubusercontent.com/12727904/192117525-a7e1b309-70d6-4ed7-bcd7-8210dbd680ce.png)

---

### Creating Custom Connectors

For a detailed walkthrough on customizing the appearance, templates, and behaviors of your connectors, check out the [Creating Custom Connectors](Custom-Connectors) guide.


111 changes: 111 additions & 0 deletions docs/Custom-Connectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Creating Custom Connectors

Connectors are the interactive endpoints of nodes where users can click-and-drag to make connections. Nodify provides `Connector` as the base class, along with standard implementations: `NodeInput` (inputs with left alignment and a custom header) and `NodeOutput` (outputs with right alignment).

To customize how connectors look, how they behave, or how they are positioned, you can customize the templates of the built-in connectors or create custom style/control definitions.

---

## Minimal Example: Customizing Connector Templates

You can easily change the shape of the connector point (such as rendering a square or a triangle instead of the default circle) by setting the `ConnectorTemplate` property.

Here is a WPF `ControlTemplate` that renders a custom square connector:

```xml
<ControlTemplate x:Key="SquareConnectorTemplate" TargetType="Control">
<Rectangle Width="12"
Height="12"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"
StrokeThickness="2"
RadiusX="2"
RadiusY="2" />
</ControlTemplate>
```

You can then apply this template directly to your `NodeInput` or `NodeOutput`:

```xml
<nodify:NodeInput Header="Input 1"
ConnectorTemplate="{StaticResource SquareConnectorTemplate}"
Background="#FF2F2F2F"
BorderBrush="#FF107C41" />
```

---

## Customizing the Header Layout

`NodeInput` and `NodeOutput` inherit from `HeaderedContentControl`, which means they have `Header` and `HeaderTemplate` properties. You can customize the `HeaderTemplate` to contain any interactive or static controls (e.g., adding input boxes or drop-down menus directly on the connector).

```xml
<DataTemplate x:Key="InteractiveConnectorHeaderTemplate">
<StackPanel Orientation="Horizontal" Margin="0,2">
<TextBlock Text="{Binding Title}" VerticalAlignment="Center" Margin="0,0,6,0" />
<TextBox Text="{Binding DefaultValue, UpdateSourceTrigger=PropertyChanged}"
Width="40"
Height="18"
FontSize="10"
Background="#333333"
Foreground="White"
BorderThickness="0" />
</StackPanel>
</DataTemplate>
```

Use it in your editor:

```xml
<nodify:NodeInput Header="{Binding}"
HeaderTemplate="{StaticResource InteractiveConnectorHeaderTemplate}" />
```

---

## Advanced: Designing Custom Connector Behavior & Style Triggers

In complex applications, you might want connectors to dynamically change their shape, color, or visibility depending on the type of data they carry (e.g., integers, booleans, trigger signals) or whether they are currently connected.

Using WPF `DataTrigger`s makes this simple:

```xml
<Style TargetType="{x:Type nodify:NodeInput}" BasedOn="{StaticResource {x:Type nodify:NodeInput}}">
<!-- Default properties -->
<Setter Property="Anchor" Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="IsConnected" Value="{Binding IsConnected}" />
<Setter Property="Header" Value="{Binding}" />

<Style.Triggers>
<!-- If the data type of the connector is 'Trigger' -->
<DataTrigger Binding="{Binding DataType}" Value="Trigger">
<Setter Property="ConnectorTemplate">
<Setter.Value>
<!-- Chevron / Arrowhead look -->
<ControlTemplate TargetType="Control">
<Polygon Points="0,0 8,6 0,12"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="1.5" />
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush" Value="#FFA3A3A3" />
<Setter Property="Background" Value="Transparent" />
</DataTrigger>

<!-- If the connector is connected, fill the inside -->
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
</DataTrigger>
</Style.Triggers>
</Style>
```

---

## Performance & Interaction Notes

1. **The `Anchor` Point**: The `Anchor` property is critical. It defines the point from which connections start or end. If you are using a custom `ControlTemplate` for the connector, ensure the outer layout of your node doesn't prevent the `Connector` from computing its relative location. The `Anchor` coordinates are automatically updated by the library when `IsConnected` is `True`.
2. **`IsConnected` Setting**: Setting `IsConnected` to `True` enables updates for the connector's `Anchor` dependency property. Make sure your custom styles or bindings preserve this logic.
3. **Hit Testing / Focus**: Connectors are highly interactive. If you put clickable elements (like textboxes) inside the `HeaderTemplate` of a connector, users can focus and interact with them, but dragging starting on those sub-controls might be intercepted depending on routing. Keep interactive elements clean and clearly distinct from the connector point itself.
225 changes: 225 additions & 0 deletions docs/Custom-Nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Creating Custom Nodes

In Nodify, nodes are the basic building blocks of a node graph. While Nodify provides built-in node controls such as `Node`, `GroupingNode`, `KnotNode`, and `StateNode`, you can easily create fully customized nodes to fit your application's unique user interface and experience.

Because Nodify is built from the ground up for MVVM, the `NodifyEditor` wraps every element in its `ItemsSource` inside an `ItemContainer`. This means that **any WPF control** can serve as a node! You are not restricted to subclassing Nodify controls; you can style or layout custom nodes using standard WPF panels (like `Grid` or `StackPanel`) and populate them with standard WPF elements.

---

## Minimal Example: Simple Custom Node

Let's build a simple custom node with:
- A custom title/header.
- An area for node settings or inputs (e.g. a `TextBox`).
- A list of input and output connectors.

### 1. The ViewModel representation

First, we will define our `ConnectorViewModel` and `CustomNodeViewModel` classes. To support WPF binding, these models implement `INotifyPropertyChanged`.

```csharp
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

public class ConnectorViewModel : INotifyPropertyChanged
{
private Point _anchor;
public Point Anchor
{
get => _anchor;
set
{
_anchor = value;
OnPropertyChanged(nameof(Anchor));
}
}

private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set
{
_isConnected = value;
OnPropertyChanged(nameof(IsConnected));
}
}

public string Title { get; set; } // Simplified for brevity; raise PropertyChanged if updating at runtime

public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public class CustomNodeViewModel : INotifyPropertyChanged
{
private Point _location;
public Point Location
{
get => _location;
set
{
_location = value;
OnPropertyChanged(nameof(Location));
}
}

private string _nodeName;
public string NodeName
{
get => _nodeName;
set
{
_nodeName = value;
OnPropertyChanged(nameof(NodeName));
}
}

private string _customText;
public string CustomText
{
get => _customText;
set
{
_customText = value;
OnPropertyChanged(nameof(CustomText));
}
}

public ObservableCollection<ConnectorViewModel> Inputs { get; } = new ObservableCollection<ConnectorViewModel>();
public ObservableCollection<ConnectorViewModel> Outputs { get; } = new ObservableCollection<ConnectorViewModel>();

public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
```

### 2. The XAML View: Styling and Templating the Custom Node

We bind the ViewModel to a View by defining a `DataTemplate` for our `CustomNodeViewModel`. This template can be placed in `NodifyEditor.ItemTemplate` or in a Resource Dictionary. We will lay out the header, internal controls, and lists of inputs and outputs inside standard WPF containers.

```xml
<DataTemplate DataType="{x:Type local:CustomNodeViewModel}">
<Border Background="#2D2D30"
BorderBrush="#3F3F46"
BorderThickness="2"
CornerRadius="6"
Padding="8"
Width="200">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Header -->
<RowDefinition Height="Auto" /> <!-- Custom Content -->
<RowDefinition Height="*" /> <!-- Connectors -->
</Grid.RowDefinitions>

<!-- Node Header -->
<TextBlock Text="{Binding NodeName}"
FontWeight="Bold"
Foreground="White"
FontSize="14"
Margin="0,0,0,8"/>

<!-- Node Content / Internal Controls -->
<StackPanel Grid.Row="1" Margin="0,0,0,8">
<TextBlock Text="Parameter:" Foreground="LightGray" FontSize="11" />
<TextBox Text="{Binding CustomText, UpdateSourceTrigger=PropertyChanged}"
Background="#1E1E1E"
Foreground="White"
BorderBrush="#3F3F46"
Padding="3"/>
</StackPanel>

<!-- Connectors (Inputs Left, Outputs Right) -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<!-- Input Connectors (Left Aligned) -->
<ItemsControl ItemsSource="{Binding Inputs}" Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<!-- Use Nodify's built-in NodeInput control -->
<nodify:NodeInput Header="{Binding Title}"
Anchor="{Binding Anchor, Mode=OneWayToSource}"
IsConnected="{Binding IsConnected}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" HorizontalAlignment="Left" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

<!-- Output Connectors (Right Aligned) -->
<ItemsControl Grid.Column="1" ItemsSource="{Binding Outputs}" Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<!-- Use Nodify's built-in NodeOutput control -->
<nodify:NodeOutput Header="{Binding Title}"
Anchor="{Binding Anchor, Mode=OneWayToSource}"
IsConnected="{Binding IsConnected}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Grid>
</Border>
</DataTemplate>
```

### 3. Registering/Adding the custom node

To display this custom node in the editor, simply add an instance of your `CustomNodeViewModel` to the nodes collection in your `EditorViewModel`:

```csharp
public class EditorViewModel
{
public ObservableCollection<object> Nodes { get; } = new ObservableCollection<object>();

public EditorViewModel()
{
var node = new CustomNodeViewModel
{
NodeName = "My Custom Node",
CustomText = "Default Value",
Location = new Point(100, 100)
};
node.Inputs.Add(new ConnectorViewModel { Title = "In Value" });
node.Outputs.Add(new ConnectorViewModel { Title = "Out Result" });

Nodes.Add(node);
}
}
```

Then bind the `Location` of the node inside the `ItemContainerStyle` of the `NodifyEditor`:

```xml
<nodify:NodifyEditor ItemsSource="{Binding Nodes}">
<nodify:NodifyEditor.ItemContainerStyle>
<Style TargetType="{x:Type nodify:ItemContainer}">
<Setter Property="Location" Value="{Binding Location}" />
</Style>
</nodify:NodifyEditor.ItemContainerStyle>
</nodify:NodifyEditor>
```

---

## Key Design Patterns & MVVM Bindings

1. **The Location Binding (`Location` property on `ItemContainer`)**: Because Nodify wraps each item in an `ItemContainer`, you must use `ItemContainerStyle` to bind the node's coordinate space (`Point`) from your view model.
2. **The Connector Anchors (`Anchor` property on `Connector`)**: If you want connections to follow dragging and move dynamically, you must bind the connector's `Anchor` property in a two-way fashion (`Mode=OneWayToSource`) to a `Point` property in your ViewModel, and set `IsConnected` to `True`.
3. **No Inheritance Necessary**: A custom node does not need to inherit from Nodify's `Node` class. Standard layout controls inside a `Border` or `Grid` are fully compatible. Inherit from `Node` only if you wish to reuse built-in header, footer, or collection templates.
2 changes: 1 addition & 1 deletion docs/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public class EditorViewModel
}
```

And bind them to the view. (We used the built-in `NodeInput` and `NodeOutput` for the view, but there are [other connectors](Connectors-Overview) too. Or you can create your own, depending on your needs.)
And bind them to the view. (We used the built-in `NodeInput` and `NodeOutput` for the view, but there are [other connectors](Connectors-Overview) too. Or you can [create your own](Custom-Connectors), depending on your needs.)

```xml
<nodify:Node Header="{Binding Title}"
Expand Down
8 changes: 7 additions & 1 deletion docs/Nodes-Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,10 @@ This type of node is a ```Connector``` itself, meaning that it will raise ```Pen

The `Content` of the node can be customized using the `ContentTemplate`.

![State Node](https://i.imgur.com/FrI2epL.gif)
![State Node](https://i.imgur.com/FrI2epL.gif)

---

### Creating Custom Nodes

For a comprehensive, step-by-step guide on how to design and implement your own custom nodes using MVVM, check out the [Creating Custom Nodes](Custom-Nodes) guide.
2 changes: 2 additions & 0 deletions docs/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [The grouping node](Nodes-Overview#2-the-groupingnode-control)
- [The knot node](Nodes-Overview#3-the-knotnode-control)
- [The state node](Nodes-Overview#4-the-statenode-control)
- [Creating custom nodes](Custom-Nodes)

[Connections overview](Connections-Overview)

Expand All @@ -48,6 +49,7 @@
[Connectors overview](Connectors-Overview)

- [NodeInput and NodeOutput](Connectors-Overview#nodeinput-and-nodeoutput)
- [Creating custom connectors](Custom-Connectors)

[CuttingLine overview](CuttingLine-Overview)

Expand Down