Search code examples
c#wpfxamlmvvmtreeviewitem

How do I add a second ItemCollection (bindable) to a TreeViewItem?


I need to get a second ItemCollection to a TreeViewItem in WPF. To do that, I want to make a custom TreeViewItem (inherits the normal TreeViewItem) and add the needed Properties hardcoded with C#. What are the steps to do that?

Why do I need this? I am trying to make a FlowChartEditor for an existing WPF program (with a existing TreeView).

For my if element, I need a true and a false collection to be bindable. It should look like this afterwards:

'If' element diagram

I made a LoopItem for example like this:

public class LoopItem : TreeViewItem
{
    [Bindable(false)]
    [Browsable(false)]
    public bool HasFooter
    {
        get { return (bool)GetValue(HasFooterProperty); }
        private set { SetValue(HasFooterProperty, value); }
    }
    public static readonly DependencyProperty HasFooterProperty =
        DependencyProperty.Register(
            "HasFooter",
            typeof(bool),
            typeof(LoopItem),
            new PropertyMetadata(false
        );

    [Browsable(true)]
    [Bindable(true)]
    public object Footer
    {
        get { return (object)GetValue(FooterProperty); }
        set { SetValue(FooterProperty, value); }
    }
    public static readonly DependencyProperty FooterProperty =
        DependencyProperty.Register(
            "Footer",
            typeof(object),
            typeof(LoopItem),
            new PropertyMetadata(null)
        );
}

Now I was able to bind not only a header, but also a footer. After writing my own style in XAML I have a result like this:

Loop element diagram

The arrows are drawn on a Canvas, which is used as a ItemsPanel on the TreeView. To be clear, this is the view I wanted to get. The only question is how to make it that way for an if.

So which properties do I need for an if item like in the first image? Somebody done that before?


Solution

  • You don't need to modify the TreeViewItem. It's about how your data structures are designed. You need to categorize or specialize your data models (or nodes) e.g.:

    • a conditional node, that must contain two child nodes, which are representing each branch.
    • a common expression node, which only has a single child node.
    • a junction node (join of multiple parent nodes), that has one child but multiple parents (collection of nodes).
    • a loop node, that has a child node (the body) and additional attributes, like a set of loop conditions
    • an iteration node, that might also have a child node (the body) and attributes, like a set of iteration conditions

    If you decide to stay with the TreeView, I don't recommend this, then you would use a HierachicalDataTemplate to design the appearance of a TreeView.

    But it will be more flexible to leave the TreeView alone and draw the whole tree instead using node controls. You can convert from tree data model representation (using specialized node model classes) to visual representation (and vice versa) by traversing the tree data model and add corresponding visual node objects to the drawing canvas (a Control that you can template and style too, to give them the desired look). When you let these controls extend the Thumb control, it will be very easy to add dragging to the visual nodes.

    The following code is a simple (and ugly) example to show how to connect two objects (of type Node) by pressing and holding Ctrl-Key and drawing a line between them. There are two Node objects, that are already created and positioned on the canvas. Later they should be either dragged to the canvas from a pool of node objects or automatically drawn to the canvas by converting the graph to visual and connected Node objects. Since Nodeextends Thumb you are able to drag the Node controls across the DrawingArea:

    XAML usage example

    <local:DrawingArea Focusable="True">
        <local:DrawingArea.Resources>
            <Style TargetType="Line">
                <Setter Property="Stroke" Value="Blue"/>
                <Setter Property="StrokeThickness" Value="2"/>
                <Setter Property="IsHitTestVisible" Value="False"/>
            </Style>
        </local:DrawingArea.Resources>
        <local:Node CurrentPosition="0, 0" />
        <local:Node CurrentPosition="150, 150" />      
    </local:DrawingArea>
    

    The Node drawing object. It extends Thumb to make it support dragging

    class Node : Thumb
    {
      public static readonly DependencyProperty CurrentPositionProperty = DependencyProperty.Register(
        "CurrentPosition",
        typeof(Point),
        typeof(Node),
        new PropertyMetadata(default(Point), OnCurrentPositionChanged));
    
      public Point CurrentPosition { get { return (Point) GetValue(Node.CurrentPositionProperty); } set { SetValue(Node.CurrentPositionProperty, value); } }
    
      public static readonly DependencyProperty ChildNodesProperty = DependencyProperty.Register(
        "ChildNodes",
        typeof(ObservableCollection<Node>),
        typeof(Node),
        new PropertyMetadata(default(ObservableCollection<Node>)));
    
      public ObservableCollection<Node> ChildNodes { get { return (ObservableCollection<Node>) GetValue(Node.ChildNodesProperty); } set { SetValue(Node.ChildNodesProperty, value); } }
    
      public static readonly DependencyProperty DrawingAreaProperty = DependencyProperty.Register(
        "DrawingArea",
        typeof(DrawingArea),
        typeof(Node),
        new PropertyMetadata(default(DrawingArea)));
    
      public DrawingArea DrawingArea { get { return (DrawingArea) GetValue(Node.DrawingAreaProperty); } set { SetValue(Node.DrawingAreaProperty, value); } }
    
      static Node()
      {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(typeof(Node)));
      }
    
      public Node()
      {
        this.DragDelta += MoveOnDragStarted;
        Canvas.SetLeft(this, this.CurrentPosition.X);
        Canvas.SetTop(this, this.CurrentPosition.Y);
      }
    
      private static void OnCurrentPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        var _this = d as Node;
        Canvas.SetLeft(_this, _this.CurrentPosition.X);
        Canvas.SetTop(_this, _this.CurrentPosition.Y);
      }
    
      private void MoveOnDragStarted(object sender, DragDeltaEventArgs dragDeltaEventArgs)
      {
        if (this.DrawingArea.IsDrawing)
        {
          return;
        }
    
        this.CurrentPosition = new Point(Canvas.GetLeft(this) + dragDeltaEventArgs.HorizontalChange, Canvas.GetTop(this) + dragDeltaEventArgs.VerticalChange);
      }
    }
    

    The Node style. Add this to the ResourceDictionary of Generic.xaml file

    <Style TargetType="local:Node">
      <Setter Property="Height" Value="100"/>
      <Setter Property="Width" Value="100"/>
      <Setter Property="IsHitTestVisible" Value="True"/>
      <Setter Property="DrawingArea" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Canvas}}"/>
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="local:Node">
            <Border Background="Red"></Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    

    The DrawingArea which is an extended Canvas that does the line drawing (exposing the editor features)

    class DrawingArea : Canvas
    {
      static DrawingArea()
      {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(DrawingArea), new FrameworkPropertyMetadata(typeof(DrawingArea)));
      }
    
      public DrawingArea()
      {
        this.TemporaryDrawingLine = new Line();
      }
    
      #region Overrides of UIElement
    
      /// <inheritdoc />
      protected override void OnPreviewKeyDown(KeyEventArgs e)
      {
        base.OnKeyDown(e);
        if (e.Key.HasFlag(Key.LeftCtrl) || e.Key.HasFlag(Key.RightCtrl))
        {
          this.IsDrawing = true;
        }
      }
    
      /// <inheritdoc />
      protected override void OnPreviewKeyUp(KeyEventArgs e)
      {
        base.OnKeyDown(e);
        if (e.Key.HasFlag(Key.LeftCtrl) || e.Key.HasFlag(Key.RightCtrl))
        {
          this.IsDrawing = false;
          this.Children.Remove(this.TemporaryDrawingLine);
        }
      }
    
      /// <inheritdoc />
      protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
      {
        base.OnMouseLeftButtonDown(e);
        if (!this.IsDrawing)
        {
          return;
        }
    
        if (!(e.Source is Node linkedItem))
        {
          return;
        }
    
        this.StartObject = linkedItem;
        this.TemporaryDrawingLine = new Line()
        {
          X1 = this.StartObject.CurrentPosition.X, Y1 = this.StartObject.CurrentPosition.Y,
          X2 = e.GetPosition(this).X, Y2 = e.GetPosition(this).Y,
          StrokeDashArray = new DoubleCollection() { 5, 1, 1, 1}
        };
        this.Children.Add(this.TemporaryDrawingLine);
      }
    
    
      /// <inheritdoc />
      protected override void OnPreviewMouseMove(MouseEventArgs e)
      {
        Focus();
        if (!this.IsDrawing)
        {
          return;
        }
    
        this.TemporaryDrawingLine.X2 = e.GetPosition(this).X;
        this.TemporaryDrawingLine.Y2 = e.GetPosition(this).Y ;
      }
    
      /// <inheritdoc />
      protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
      {
        base.OnPreviewMouseLeftButtonUp(e);
        if (!this.IsDrawing)
        {
          return;
        }
    
        if (!(e.Source is Node linkedItem))
        {
          this.Children.Remove(this.TemporaryDrawingLine);
          this.IsDrawing = false;
          return;
        }
    
        e.Handled = true;
        this.Children.Remove(this.TemporaryDrawingLine);
        var line = new Line();
        var x1Binding = new Binding("CurrentPosition.X") {Source = this.StartObject};
        var y1Binding = new Binding("CurrentPosition.Y") { Source = this.StartObject };
        line.SetBinding(Line.X1Property, x1Binding);
        line.SetBinding(Line.Y1Property, y1Binding);
    
        this.EndObject = linkedItem;
        var x2Binding = new Binding("CurrentPosition.X") { Source = this.EndObject };
        var y2Binding = new Binding("CurrentPosition.Y") { Source = this.EndObject };
        line.SetBinding(Line.X2Property, x2Binding);
        line.SetBinding(Line.Y2Property, y2Binding);
        this.Children.Add(line);
        this.IsDrawing = false;
      }
    
      public bool IsDrawing { get; set; }
    
      private Node EndObject { get; set; }  
      private Node StartObject { get; set; }  
      private Line TemporaryDrawingLine { get; set; }    
    
      #endregion
    }