Pages

Thursday 7 June 2012

Silverlight FlowChart Designer - Part 2 of 3

Hi and Welcome back to Part 2 of the FlowChart Designer series.

 Till now we saw how to move the elements on the screen . Pretty basic stuff. Now we will enable joining/linking any two Flow elements in this post , which is the crucial part of creating flowcharts.


Step 1:  Creating a clickable Node.
a) Our default Flow elements do not have the feature to connect.
    We can add circular nodes to our controls , and enable links when we click on the nodes.
b) Let us create a custom control called FlowNode , which is a simple ellipse.
    It will have a reference to its parent flow element, its current X and Y positions , and a list of all paths conntected to it.
 c) Create a New user control and name it FlowNode
    In the FlowNode.Xaml ,  set its width and height to 16, and copy the following code.
   <Grid x:Name="LayoutRoot" Background="Transparent" Height="16" Width="16">
        <Ellipse Height="16" HorizontalAlignment="Left" Name="ellipse1" Fill="PowderBlue" IsHitTestVisible="False"  VerticalAlignment="Top" Width="16" />
    </Grid>

 d) As you can see, it is a simple ellipse, with its isHitTestVisible property set to false.
 e) In the FlowNode.Xaml.cs file, add the following...

     public partial class FlowNode : UserControl
    {
        public UserControl flowParent;
       
        public double X;
        public double Y;
      
        public List<CustomPath> paths;
        public FlowNode()
        {
            InitializeComponent();
            paths = new List<CustomPath>();
        }
    }
 
This is the foundation for the linking operations .

Step 2: Creating a Custom Path

a) We can use a normal Path object in our code, but it will require a lot of extra effort to track the location, parents of that path and other parameters.  Creating a custom path is easier as we can define additional properties.

b) In the FlowItems folder, Add a new Class , name it CustomPath , and it extends from Path class.
    It has 4 public properties  PathSource, PathDestination, message and pathNumber.
    PathSource and PathDestination are of type FlowNode ,message is a string, and pathNumber is an int.
 
    public class CustomPath :Path
    {
        public FlowNode PathSource;
        public FlowNode PathDestination;
        public string message;
        public int pathNumber;
    }

c) So you have the custom path ready, as well as the FlowNode.  We need to write logic such that whenever two nodes are clicked, a  path is created between them .

Your project structure should look like this now...



Step 3:  Customizing the existing flow controls.

a) As you have the Flow node ready, instances of the FlowNode can be added to our Flow Controls.
b) Once build your project, and you will find the flowNode user control in the toolbox.
c) Open the  FlowStart , FlowDecision and FlowProcess, and add the flowNodes as shown below.





Copy the following code to your Flow Elements.

FlowStart.xaml
 <Grid x:Name="LayoutRoot">
        <Rectangle Height="39" HorizontalAlignment="Left" Name="inputRect" StrokeThickness="1" VerticalAlignment="Top" Width="134" RadiusX="30" RadiusY="30" Fill="LightGreen" />
        <TextBlock IsHitTestVisible="False" Height="23" HorizontalAlignment="Left" Margin="51,12,0,0" Name="txtInput" Text="Start" VerticalAlignment="Top" Width="40" />
        <my:FlowNode HorizontalAlignment="Left" Margin="111,7,0,0" x:Name="flowNode1" VerticalAlignment="Top" Height="23" Width="29" />
        <Grid.Background>
            <SolidColorBrush />
        </Grid.Background>
    </Grid>


FlowProcess.xaml

 <Grid x:Name="LayoutRoot" Background="Transparent" Width="124" Height="76">
        <Rectangle Height="53" HorizontalAlignment="Left" Name="inputRect"   VerticalAlignment="Top" Width="97" Fill="BlanchedAlmond" Margin="13,9,0,0" />
        <TextBlock Height="18"  IsHitTestVisible="False" HorizontalAlignment="Left" Name="textBlock1" Text="Process" VerticalAlignment="Top" Width="58" Margin="38,24,0,0" TextWrapping="Wrap" />
        <my:FlowNode HorizontalAlignment="Left" Margin="53,52,0,0" x:Name="flowNode4" VerticalAlignment="Top" />
        <my:FlowNode HorizontalAlignment="Left" Margin="4,24,0,0" x:Name="flowNode2" VerticalAlignment="Top" />
        <my:FlowNode HorizontalAlignment="Left" Margin="102,24,0,0" x:Name="flowNode3" VerticalAlignment="Top" />
        <my:FlowNode HorizontalAlignment="Left" Margin="55,2,0,0" x:Name="flowNode1" VerticalAlignment="Top" />
    </Grid>

FlowDecision.xaml

<Grid x:Name="LayoutRoot"  Height="84" Width="95" Margin="0" Background="Transparent">
        <Rectangle   HorizontalAlignment="Left" Margin="45,7,0,0" Name="inputRect" StrokeThickness="1" VerticalAlignment="Top" Width="49" Height="47" Fill="#FFFFFFAD">
            <Rectangle.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="45"/>
                </TransformGroup>
            </Rectangle.RenderTransform>
        </Rectangle>
        <my:FlowNode HorizontalAlignment="Left" Margin="38,3,0,0" x:Name="flowNode1" VerticalAlignment="Top" />
        <my:FlowNode HorizontalAlignment="Left" Margin="67,34,0,0" x:Name="flowNode3" VerticalAlignment="Top" />
        <my:FlowNode HorizontalAlignment="Left" Margin="40,60,0,0" x:Name="flowNode4" VerticalAlignment="Top" />
        <my:FlowNode HorizontalAlignment="Left" Margin="7,34,0,0" x:Name="flowNode2" VerticalAlignment="Top" />
        <TextBlock Height="16" HorizontalAlignment="Left" Margin="29,34,0,0" Name="textBlock1" Text="" VerticalAlignment="Top" Width="32" IsHitTestVisible="False" />
    </Grid>



Step 4:  In your flowChart.Xaml page, we need to add additional lements.  For example , we need to add global FlowNode objects that enable you to link any two FlowNodes of any two Flow Elements.
we also need to add a static Canvas, that points to our DesignCanvas.

So in your FlowChart.xaml.cs ,change the following...
        public static FlowNode startNode ;
        public static FlowNode endNode ;
        public static Canvas stDesignCanvas;
        public FlowChart()
        {
            InitializeComponent();
            stDesignCanvas = DesignCanvas;
        }

Step 5 :  We need to write two methods in the FlowChart.xaml.cs  , which will take two flowNodes as inputs. One of them is for creating the link for the first time, and the other is for updating the nodes.  Actually one method will be enough, but it looks clean and helps if you want to further customize features while updating the paths.

public static void CreatePath()
        {
            CustomPath cp = new CustomPath();
            LineGeometry mypath = new LineGeometry();
            mypath.StartPoint = new Point(startNode.X, startNode.Y);
            mypath.EndPoint = new Point(endNode.X, endNode.Y);
           
            cp.Data = mypath;
            cp.Stroke = new SolidColorBrush(Colors.Red);
            cp.StrokeThickness = 1;
            cp.PathSource = startNode;
            cp.PathDestination = endNode;
            startNode.paths.Add(cp);
            endNode.paths.Add(cp);

            stDesignCanvas.Children.Add(cp);
            startNode = null;
            endNode = null;
        }


 public static void UpdatePath(FlowNode n1 , FlowNode n2)
        {
            CustomPath cp = new CustomPath();
            LineGeometry mypath = new LineGeometry();
            mypath.StartPoint = new Point(n1.X, n1.Y);
            mypath.EndPoint = new Point(n2.X, n2.Y);
            cp.Data = mypath;
            cp.Stroke = new SolidColorBrush(Colors.Red);
            cp.StrokeThickness = 1;
            cp.PathSource = n1;
            cp.PathDestination = n2;
            n1.paths.Add(cp);
            n2.paths.Add(cp);
            stDesignCanvas.Children.Add(cp);
          
        }

Observe that these methods are static.  this enables us to access them from any of the Flow Elements's events.  Thats all you need to do to the FlowChart control.  

Step 6:  Now i will show how to add functionality to the Flow Elements to process the click on the Flownode.  This process will be common for all the controls .  I am taking the example of FlowDecision .

a) Open the FlowDesicion.Xaml.cs .
In the constructor, we earlier added event handlers for inputRect mouse events.

b) Now we need to add handlers for each of the flownodes.  The only action that you will perform is click.
So create a MouseLeftButtonDown event for each of the flownodes.
 
            flowNode1.MouseLeftButtonDown += new MouseButtonEventHandler(flowNode1_MouseLeftButtonDown);
            flowNode2.MouseLeftButtonDown += new MouseButtonEventHandler(flowNode2_MouseLeftButtonDown);
            flowNode3.MouseLeftButtonDown += new MouseButtonEventHandler(flowNode3_MouseLeftButtonDown);
            flowNode4.MouseLeftButtonDown += new MouseButtonEventHandler(flowNode4_MouseLeftButtonDown);

c) If you think again, all of them will do the same functionality.  You dont need to write the same code again and again in each of the methods.  So we will create a new method, and call this method inside the four event handlers.  easier, right.

d) Lets call that method  as SetGlobalFlowNode. It just checks if the current node will be a start node or an end node.  This is used to create the paths.

 public void SetGlobalFlowNode(object sender , double xX , double yY)
        {
            FlowNode flowNode = sender as FlowNode;
            flowNode.X = xX;
            flowNode.Y = yY;
           
            if (FlowChart.startNode != flowNode && FlowChart.endNode != flowNode)
            {
                if (FlowChart.startNode == null)
                {
                    FlowChart.startNode = flowNode;
                }
                else if (FlowChart.endNode == null)
                {
                    FlowChart.endNode = flowNode;
                    FlowChart.CreatePath();
                }
            }
        }

e) So inside the event handler, just call this method, and pass the current mouse position from the event args.
    I am showing an example for flowNode1 , the same logic is applied for the other 3 flowNodes
   void flowNode1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            SetGlobalFlowNode(sender,e.GetPosition(parent).X,e.GetPosition(parent).Y);
        }
   

Step 7: Updating the paths on moving the elements.

 Yes, this is absolutely a crucial step .The paths should update themselves for which they call the UpdatePaths method written earlier.   But where is that method called ?  The answer is...  in the MouseMove event that is present in every Flow Element.

In this mouse move, we calculate the relative position of the FlowNodes   in each control and pass them as parameters .  

  void inputRect_MouseMove(object sender, MouseEventArgs args)
        {
            FlowDecision item = this;
            if (isMouseCaptured)
            {
                 double newTop = args.GetPosition(parent).Y;
                 double newLeft = args.GetPosition(parent).X;
                 item.SetValue(Canvas.TopProperty, newTop - 50);
                 item.SetValue(Canvas.LeftProperty, newLeft);
                               
                 double deltaV = mouseVerticalPosition - args.GetPosition(parent).Y;
                 double deltaH = mouseHorizontalPosition - args.GetPosition(parent).X;
                 mouseVerticalPosition = args.GetPosition(parent).Y;
                 mouseHorizontalPosition = args.GetPosition(parent).X;
                 
                 double d1 = Canvas.GetTop(this) +20 ;
                 double d2 = Canvas.GetTop(this) +50 + Canvas.GetTop(flowNode2);
                 double d3 = Canvas.GetTop(this) +50 + Canvas.GetTop(flowNode3);
                 double d4 = Canvas.GetTop(this) +80 ;
                 double w1 = Canvas.GetLeft(this)+50;
                 double w2 = Canvas.GetLeft(flowNode2.flowParent)+20;
                 double w3 = Canvas.GetLeft(flowNode3.flowParent)+80;
                 double w4 = Canvas.GetLeft(flowNode4.flowParent)+50;
                 UpdatePaths(item.flowNode1, d1, w1);
                 UpdatePaths(item.flowNode2, d2, w2);
                 UpdatePaths(item.flowNode3, d3, w3);
                 UpdatePaths(item.flowNode4, d4, w4);
            }
        }
 The absolute values which are used, for example 50,20,80 ,are the distances from the top or left  in the designer screen.  You need to use Canvas.GetLeft and Canvas.GetTop  for those actually (in case you frequently change the shape of your Flow element.) but i put in the direct values for now.

Similarly, the MouseMove event for the FlowStart

  void inputRect_MouseMove(object sender, MouseEventArgs args)
        {
               FlowStart item = this;
               if (isMouseCaptured)
               {
                   double newTop = args.GetPosition(parent).Y;
                   double newLeft = args.GetPosition(parent).X;
                   item.SetValue(Canvas.TopProperty, newTop);
                   item.SetValue(Canvas.LeftProperty, newLeft);
                   double deltaV = mouseVerticalPosition - args.GetPosition(parent).Y;
                   double deltaH = mouseHorizontalPosition - args.GetPosition(parent).X;
                   mouseVerticalPosition = args.GetPosition(parent).Y;
                   mouseHorizontalPosition = args.GetPosition(parent).X;

                   double d1 = Canvas.GetTop(this) + 15;
                 
                   double w1 = Canvas.GetLeft(this) + 120;
                   
                   UpdatePaths(item.flowNode1, d1, w1);
               }
        }


and for  FlowProcess

    void inputRect_MouseMove(object sender, MouseEventArgs args)
        {
        
         FlowProcess item = this;
            if (isMouseCaptured)
            {
                double newTop = args.GetPosition(parent).Y;
                double newLeft = args.GetPosition(parent).X;
                item.SetValue(Canvas.TopProperty, newTop -50);
                item.SetValue(Canvas.LeftProperty, newLeft);
                               
                double deltaV = mouseVerticalPosition - args.GetPosition(parent).Y;
                double deltaH = mouseHorizontalPosition - args.GetPosition(parent).X;
                mouseVerticalPosition = args.GetPosition(parent).Y;
                mouseHorizontalPosition = args.GetPosition(parent).X;
                
            
                 double d1 = Canvas.GetTop(this) + 15 ;
                 double d2 = Canvas.GetTop(this) + 30 + Canvas.GetTop(flowNode2);
                 double d3 = Canvas.GetTop(this) + 35 ;
                 double d4 = Canvas.GetTop(this) + 60;

                 double w1 = Canvas.GetLeft(this) + 60;
                 double w2 = Canvas.GetLeft(this) + 15;
                 double w3 = Canvas.GetLeft(this) + 110;
                 double w4 = Canvas.GetLeft(this) + 55;
                UpdatePaths(item.flowNode1, d1, w1);
                UpdatePaths(item.flowNode2, d2, w2);
                UpdatePaths(item.flowNode3, d3, w3);
                UpdatePaths(item.flowNode4, d4, w4);
            }
        }
 
  And there is one more final change.   The SetGlobalFlowNode is common for all controls,  but for the FlowStart, it should change a bit.  The FlowStart can never be the destination node. So we need to write logic such that it is always set as start node when you click on its node.

   public void SetGlobalFlowNode(object sender, double xX, double yY)
        {
            if (flowNode1.paths.Count > 0)
            {
                return;
            }
            FlowNode flowNode = sender as FlowNode;
            flowNode.X = xX;
            flowNode.Y = yY;
            FlowChart.startNode = flowNode;
        }

Now properly attatch eventhandlers for all the controls and start your project.  Your Designer should   almost be functional. 


 All you need to do now is to Save the FlowCharts , and do a bit more tweaking in a couple of places. We will cover this , and few more customizations in the next part of the series.   Hope you found it interesting :D

3 comments:

  1. Phani, I've using your article to create a silverlight designer that i'm working on. Thank you so much for the tutorial. Although I find myself stuck at step 7. I cant seem to understand what the code actually does. Plus your calling the 'UpdatePath' method and sending it three variables when it recieves two the start and the end node. Please clarify this for me. Thank you again.

    ReplyDelete
  2. thank you, still plans for part 3?

    ReplyDelete