Home > OS >  WPF - Draw elements and connectors on Canvas with DataBinding
WPF - Draw elements and connectors on Canvas with DataBinding

Time:01-24

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: A preview of the result: three nodes with connectors

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:

  1. Create a master ObservableCollection<Node> where I keep all the nodes.
  2. On each node, have another ObservableCollection<Node> storing references to its children (could also be ObservableCollection<string> or ObservableCollection<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).
  3. Bind the master ObservableCollection<Node> from step 1 to the ItemsControl.
  4. 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.

https://learn.microsoft.com/en-us/dotnet/api/system.windows.hierarchicaldatatemplate?view=windowsdesktop-7.0

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".

https://imgur.com/u720QD5

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

  • Related