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

Tuesday 5 June 2012

Silverlight FlowChart Designer - Part 1 of 3

Hi ,
    Long time guys . I have been working on that FlowChart designer and Social Networking site in my free time , and things are going quite good.  So here's the thing.  I will take you step by step through creation of a flowchart designer ( Which can also be used as a workflow designer with little modifications ) .  We will do it in a three part series .

1) The first part will contain how to build building basic controls and UI  and implement the dragging functionality.
2) The second part will build upon the first part, and will be covering linking two flow chart items and related functionality.
3) The final part will deal with saving the stuff in different formats , so that it can be used somewhere else.

So lets dive in . 

Step 1:   Create a new Silverlight Navigation application.

  a)  You will find a default set of Views created in the Views folder . 
  b)  Add a new Silverlight Page , and name it FlowChart.xaml.This will contain flowchart designer.
  c)  In MainPage.Xaml , you see that there are two menu items ...Home and About.
  d) Add a third one, by copying and pasting the rectangle and Hyperlink. Name it FlowChart.




Step 2:  Creating the FlowChart designer UI.

 a)  In the FlowChart.xaml ,  we will split the screen into two regions.
      The right region will contain the designer canvas,
      The left region will contain the options to add new items to the designer.
 b)  So the Left region will be a StackPanel  , the right region and the LayoutRoot will be a canvas.
 c)  Add three buttons to the StackPanel, and name them  "Add Start","Add Process", "Add decision".
 d) So your FlowChart.xaml should look like this ... 

<Canvas x:Name="LayoutRoot" Height="340">
 <Canvas x:Name="DesignCanvas" Width="507" Height="317" Canvas.Left="130" Canvas.Top="16"></Canvas>
<StackPanel Canvas.Left="11" Canvas.Top="16" Height="316" Name="FlowItemsList" Width="113">
<Button Content="Add Start" Height="26" Name="btnAddStart" Width="78" Click="btnAdd_Click" />
<Button Content="Add Process" Height="31" Name="btnAddFlowProcess" Width="81" Click="btnAddFlowProcess_Click" />
<Button Content="Add Decision" Height="31" Name="btnAddFlowDecision" Width="81" Click="btnAddFlowDecision_Click" />
</StackPanel>
</Canvas>



 e) Write appropriate event handlers for Click event for the buttons , and leave them empty for now.

Step 3:  Creating the Flow Chart controls .

a)  Create a new Folder in your Project.  Name it FlowItems. Add three usercontrols  to it , named  FlowStart, FlowProcess and FlowDecision . Your project structure should look like this.



b) The FlowStart will represent the start nodeand the the FlowProcess will represent the process node of the flowchart. The FlowDesicion will be diamond shaped,  used to represent the desicion box.
c)  So lets design each of the controls and their properties further.

d) Open  FlowStart.xaml.  
    This should be a rounded rectangle shaped object according to convention.
     i)  Let the size of the control be  d:DesignHeight="39" d:DesignWidth="134"
     ii) Add a rectangle to the grid, and set its RadiusX="30" RadiusY="30"    
     iii)Add a textblock , and set its isHitTestVisible to false  .
   
Your code should look like this
<Grid>
 <Rectangle Height="39" HorizontalAlignment="Left" Name="inputRect" StrokeThickness="1" VerticalAlignment="Top" Width="134" RadiusX="30" RadiusY="30" Fill="#FF41C136" />
<TextBlock IsHitTestVisible="False" Height="23" HorizontalAlignment="Left" Margin="33,10,0,0" Name="txtInput" Text="Start" VerticalAlignment="Top" />
<Grid.Background>
<SolidColorBrush />
</Grid.Background>
</Grid>


e) Open  FlowProcess.xaml
   i) This should be a regualr rectangle with a similar structure
   ii) Set its d:DesignHeight="53"  and d:DesignWidth="96" .
   Just add the following code to that page.
<Grid x:Name="LayoutRoot" Background="Transparent" Width="94">
<Rectangle Height="53" HorizontalAlignment="Left" Name="inputRect" Stroke="Black" VerticalAlignment="Top" Width="97" Fill="LightBlue" Margin="-3,0,0,0" />
<TextBlock Height="29" IsHitTestVisible="False" HorizontalAlignment="Left" Name="textBlock1" Text="Process" VerticalAlignment="Top" Width="82" Margin="2,13,0,0" TextWrapping="Wrap" />
</Grid>
f) Open the FlowDecision.Xaml.
   i) This needs a Rotate transform for the rectangle to be shown like a diamond shape.
   ii) Set its d:DesignHeight="81" and d:DesignWidth="81" .
   iii) Paste the following code

 <Grid x:Name="LayoutRoot" Height="81" Width="95" Margin="0" Background="Transparent">
<Rectangle HorizontalAlignment="Left" Margin="40,1,0,0" Name="inputRect" StrokeThickness="1" VerticalAlignment="Top" Width="55" Height="53" Fill="#FFFFFFAD">
<Rectangle.RenderTransform>
<TransformGroup>
<RotateTransform Angle="45"/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>

These are just the basic shapes that will be used to implement our logic.  In the next part of the series, we will add more controls to these shapes and make them functional.


Step 4:  Enabling Dragging for the Flow Controls .

a) The elements should be draggable over the surface of the designer.  For that to happen , we will need to use the Top and Left properties of the canvas. (And that is also the reason why Canvas is the best control for these sort of drag operations)

b) So we will attach event handlers to the rectangle inside each of the Flow controls. The events that will be monitored are  MouseLeftButtonDown , MouseLeftbuttonUp , MouseMove  and MouseRightButtonDown.

MouseLeftButtonDown will be used to get the initial location of the  control .
MouseLeftButtonUp will indicate that the dragging is completed.
MouseMove will be used to update the location of the control.
MouseRightButtonDown will be used to show a Context menu , to delete the flow item.

This code will be common for all the controls  for now.  As we go further in the Next lesson , we will add more functionality specific to the individual controls.

c) Every flow control should have the following properties ,
   
bool isMouseCaptured;
Popup p;
public Canvas parent;
double mouseVerticalPosition, mouseHorizontalPosition;

d) The double variables will store the initial location of the Flow control , and the popup control will show the delete menu item (which allows you to remove the flow control from the designer).
e) Open the FlowStart.Xaml.cs , and add the above properties to it.
f) In the constructor,  add the code

public FlowStart(Canvas par)
{
InitializeComponent();
inputRect.MouseRightButtonDown +=new MouseButtonEventHandler(inputRect_MouseRightButtonDown);
inputRect.MouseLeftButtonDown += new MouseButtonEventHandler(inputRect_MouseLeftButtonDown);
inputRect.MouseLeftButtonUp += new MouseButtonEventHandler(inputRect_MouseLeftButtonUp);
inputRect.MouseMove += new MouseEventHandler(inputRect_MouseMove);
parent = par;

p = new Popup();
p.Width = 50;
p.Height = 150;
List<Button> clicks = new List<Button>();
Button b1 = new Button();
b1.Width = 50;"Delete";new RoutedEventHandler(b1_Click);StackPanel sp = new StackPanel();foreach(Button b in clicks)    
b1.Height = 30;
b1.Content =
b1.Click +=
clicks.Add(b1);

sp.Width = 50;
sp.Height = 150;

{
sp.Children.Add(b);
}
p.Child = sp;

}

g) If you notice above, i am adding the buttons to a stackpanel , and that becomes the content for the popup menu.  this way, you can add as many buttons as you want to your popup.
h)  Also note that this is common code for all the flow controls. Each one has the exact same code inside them , except their different shapes for now. Copy the event handlers .


The MouseLeftButtonDown will get the inital location of the control and put it in the mouseVerticalPosition  and the mouseHorizontalPosition variables.

void inputRect_MouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
Rectangle item = sender as Rectangle;
mouseVerticalPosition = args.GetPosition(parent).Y;
mouseHorizontalPosition = args.GetPosition(parent).X;
isMouseCaptured=true;
item.CaptureMouse(); }



The MouseRightButtonDown  will open the Popup menu

void inputRect_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{ e.Handled = true;
p.VerticalOffset = e.GetPosition(null).Y;
p.HorizontalOffset = e.GetPosition(null).X
p.IsOpen = true;
}

The MouseLeftButtonUp  event releases the mouse. 

void inputRect_MouseLeftButtonUp(object sender, MouseButtonEventArgs e){
Rectangle item = sender as Rectangle;
isMouseCaptured =false;
item.ReleaseMouseCapture();
mouseHorizontalPosition = -1;
mouseVerticalPosition = -1;}


void inputRect_MouseMove(object sender, MouseEventArgs args)
{
if (isMouseCaptured)
{
double deltaV = args.GetPosition(parent).Y;
double deltaH = args.GetPosition(parent).X;
double newTop = deltaV ;
double newLeft = deltaH;
this.SetValue(Canvas.TopProperty, newTop);
this.SetValue(Canvas.LeftProperty, newLeft);
mouseVerticalPosition = args.GetPosition(parent).Y;
mouseHorizontalPosition = args.GetPosition(parent).X;
}
}
  
The MouseMove will change the location of the control.

This will allow your Flow controls to be draggable over the canvas.  The next step is to add functionality to link elements .  This will be dealt in the next Part of the Series.  

Keep watching for the next post ! :)