I am working on a WPF node/graph view application. Each node has a fixed (computed) position, some contents like a text, and a list of children. As soon as all the positions are recalculated, I would like to draw these nodes on a Canvas and draw the connections from each node to its children.
What I have
I have followed the advice here:
I am keeping two collections, one for the nodes and one for the connectors between them, and binding them as a CompositeCollection
.
Here is the relevant XAML:
<ItemsControl x:Name="Collection">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:Node}">
<TextBox Text="{Binding Text}" Width="160" Height="100"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Connector}">
<Line Stroke="Black" StrokeThickness="1"
X1="{Binding From.X}" Y1="{Binding From.Y}"
X2="{Binding To.X}" Y2="{Binding To.Y}" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
Here are my data structures.
public class Node
{
public string Text { get; set; }
public double X { get; set; }
public double Y { get; set; }
public List<Node> Children;
public Point RightConnector => new Point(X 160, Y 50);
public Point LeftConnector => new Point(X, Y 50);
}
public class Connector
{
public Point From { get; set; }
public Point To { get; set; }
}
In code: For each node, I am iterating over the children and explicitly creating a Connector
for each. I am filling the two ObservableCollections
, adding them to a CompositeCollection
and telling the XAML about it using Collection.ItemsSource = ObjectCollection
.
What I'm missing
While this works, I would much rather do the following:
- Create a master
ObservableCollection<Node>
where I keep all the nodes. - On each node, have another
ObservableCollection<Node>
storing references to its children (could also beObservableCollection<string>
orObservableCollection<uint32>
using node name/ID - this is how the data will be stored on disk in the end; whichever way, I am able to access the node's children and their positions). - Bind the master
ObservableCollection<Node>
from step 1 to theItemsControl
. - For each item of the collection, let WPF draw both the node representation and the variable number of connectors to the node's n children. Note that in this scenario the
Connector
class doesn't exist anymore, but a connector is implicitly defined by the source Node's position and a child's position.
How can I make WPF react to a single collection item by drawing both the node itself and its connectors?
CodePudding user response:
The short answer is by using a hierarchicaldatatemplate.
There will be some side effects to this though.
Your children will be in the same itemcontainer.
I'm not so sure this is a better plan.
EG In my map editor. My trees are in the same observablecollection as the woods which creates their outline. Trees are logically children of the woods they are "inside" but each wood and tree viewmodel is "just" another object in my bound collection below. A WoodVM also has an ObservableCollection for it's "children".
Different terrain types have different child symbols.
Water has waves and the occasional fish, Swamp has marsh grass symbols. Etc.
You realise, BTW, you can bind an Observablecollection rather than compositecollection?
CodePudding user response:
I would like to present 2 ways to solve the problem. As a first step I will present the Node class which has "Children" property to child Nodes and also a Parent point to the parent Node. The Parent Property can be useful for drawing the line. Here is the Node class
public class Node :Binding
{
public Node()
{
Children = new ObservableCollection<Node>();
}
public Node(string text,double x,double y )
{
Text = text;
X = x;
Y = y;
Children = new ObservableCollection<Node>();
}
Node _parent;
public Node Parent
{
get {
if (_parent == null)
{
return this;
}
return _parent; }
set { _parent = value;NotifyPropertyChanged(nameof(Parent)); }
}
string _text;
public string Text
{
get { return _text; }
set { _text = value; NotifyPropertyChanged(nameof(Text)); }
}
double _x;
public double X
{
get { return _x; }
set { _x = value;NotifyPropertyChanged(nameof(X)); }
}
double _y;
public double Y
{
get { return _y; }
set { _y = value; NotifyPropertyChanged(nameof(Y)); }
}
ObservableCollection<Node> _children;
public ObservableCollection<Node> Children
{
get { return _children; }
set { _children = value;NotifyPropertyChanged(nameof(Children)); }
}
public void Add(Node child)
{
Children.Add(child);
child.Parent = this;
}
}
It seems to me that the combination of ItemsControl and the HierarchicalDataTemplate is not working. If you want to use the HierarchicalDataTemplate please use the TreeView control as following:
<Grid>
<TreeView x:Name="Collection" ItemsSource="{Binding Nodes}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Node}" ItemsSource="{Binding Path=Children}" >
<TextBox Text="{Binding Text}" Width="160" Height="100" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
This is very simple but I don't know if it matches your needs.
Alternatively you can re-build in code the Canvas from scratch on any change. I do not have a code for you .
The other solution I have is a recursive use of user control. and this is its use:
<Grid>
<ItemsControl ItemsSource="{Binding Nodes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:RecursiveSolution/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
The ReursiveSolution User Control is as following:
<Canvas>
<TextBox Text="{Binding Text}" Width="160" Height="100" Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
<Line X1="{Binding X}" Y1="{Binding Y}" X2="{Binding Parent.X}" Y2="{Binding Parent.Y}" Stroke="Black"/>
<ItemsControl x:Name="Collection" ItemsSource="{Binding Children}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:Node}">
<Grid>
<local:RecursiveSolution/>
</Grid>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Canvas>
As one can see, I draw the line. The line is not as requested because I was too lazy to go all the way. But this is simple using some converters.
Due to the recursive solution the Canvas areas are placed one on top of its parent. I do not see a real problem , but we may need an improvement here. Hope it helps