Saturday 25 May 2013

Using FluentSharp API’s to refactor C# TreeView sample

While reading reddit’s r/csharp I saw the Can anyone help me answer a quick question about treeview? thread which asked this question:

image


The best answer was from BCProgramming , which provided a C# code sample as the example:

image

So I opened an O2 Platform SourceCodeEditor:

image

Clicked on Sample C#

image

and copied the BCProgramming response source code

image

into it:

image

The first attempt to compile the source code failed:

image

because the code-behind code is missing:

image

Which in this case can be easily fixed by adding the using O2.DotNetWrappers.ExtensionMethods reference and adding an TreeView here:

image

Next step is to create a test class and test method that will fire up the form:

image

Next step is to add the button

image

which when clicked adds the nodes:

image

Now that we have this working lets start refectoring the code, using the ExtensionMethods from the FluenSharp.BCL.dll library (available from Nuget).

I’m a big fan of REPL development, so the first extra feature I’m going to add is a link to open a C# REPL window with the current Form passed as parameter (see line 57 below):

image

which will make the form look like this (note the extra link):

image

and clicking on the link will open the REPL in a popup window:

image

Note how the first command on the REPL was return form1;

What that means is that we have access to that variable from our script, and that form1 variable is the Form created by the original C# class, ie. NodesTest.Form1

image

Also note that we have complete control over that Form object, and even have intelisense into its public properties, fields and methods:

image

for example the tvwTest TreeView object:

image

which we can manipulate in real time (for example to add new Node, change color and get all nodes)

image

Note how treeView.nodes() returned all nodes, and how (below) treeView.nodes(true) will return a recursive search of all nodes:

image

This means that (back into the original code we copied from reddit) we can remove these methods:

image

which are used from here:

image

with just (note how the result is the same):

image

Next we clear the treeView using tvwTest.clear()

image

and create the child nodes like this:

image

Since the idea is to add the node to the root when ParentId is 0, we can refactor the code like this:

image

And here is a confirmation that the refactored code behaves as the original (note the expandAll() call at the end, which save us to need to expand each node to check it)

image

To make the code easier to read, let’s remove the comments with the old code (I also changed like 76 to use the treeNode.get<T>() extension method):

image

And if you are into Linq we can replace the search with a dictionary:

image

The previous one looks to complex (and not optimized since we need to get all nodes all the time), so this version keeps an XRef of the articles added:

image

Here is a cleaner version:

image

I’m a big fan of Lambda methods, so here is version using them (note that the code below now handles the case when there is an invalid parent):

image

Here is a simpler version (by adding another lambda method called findParent)

image

Which can be further simplified to:

image

And here is the exact same code using functions (instead of Lamdba methods):

image

Let’s try this on the O2 C# REPL

In the previous example I was using a normal C# class and a code editor.

But for code samples/PoCs like this, I would normally use the O2 Platform C# REPL since it is a much faster and efficient code-development environment.

Since we have some data hold on external classes, I will create a simple C# file (saved to E:\O2\O2.Temp\5_22_2013\tmpCCF6.tmp.cs ) with just that data:

image

Then we open the C# REPL:

image

and add a reference to the TestData file, and invoke the TestData.GetTestData() static method:

image

Then add the TreeView and Button:

image

Then we add the lambda methods previously created, and we have a working PoC:

image

Lets add one more lambda method so that the tree population happens when the button is clicked:

image

Finally lets change the topPanel variable to come from a stand-alone form:

image

and now clicking on Execute will give us a popup Form:

image

with the PoC:

image

Note that you can also package this PoC has a stand-alone exe using

image

which will create a 902Kb file:

image

of the PoC form:

image



Code samples used in this post

1) original code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace NodesTest
{
    public partial class Form1 : Form
    {
        private class TestData
        {
            public TestData(int pNodeId, int pParent, String pText)
            {
                NodeId = pNodeId;
                ParentId = pParent;
                Text = pText;
            }

            public int NodeId { get; set; }
            public int ParentId { get; set; }
            public String Text { get; set; }
        }

        //static test data.
        private TestData[] testinfo = new TestData[]
                                          {
                                              new TestData(1, 0, "ID 1"),
                                              new TestData(2, 0, "ID 2"),
                                              new TestData(3, 1, "ID 3 Child 1 of ID 1 "),
                                              new TestData(4, 1, "ID 4 Child 2 of ID 1"),
                                              new TestData(5, 2, "ID 5 Child 1 of ID 2"),
                                              new TestData(6, 2, "ID 5 Child 2 of ID 2"),
                                              new TestData(7, 2, "ID 5 Child 3 of ID 2"),
                                              new TestData(8, 5, "ID 8 Child 1 of ID 5")
                                          };
        public Form1()
        {
            InitializeComponent();
        }

        public IEnumerable<TreeNode> getAllNodes(TreeView tvw)
        {
            foreach (TreeNode iterate in tvw.Nodes)
            {
                yield return iterate;
                foreach (TreeNode childiterate in getAllNodes(iterate))
                {
                    yield return childiterate;
                }
            }
        }

        public IEnumerable<TreeNode> getAllNodes(TreeNode tvw)
        {
            //iterate through all nodes...
            foreach (TreeNode iterateNode in tvw.Nodes)
            {
                yield return iterateNode;
                foreach (TreeNode childiterate in getAllNodes(iterateNode))
                {
                    yield return childiterate;
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            tvwTest.Nodes.Clear();
            foreach (var iterate in testinfo)
            {
                TreeNode CreateNode = new TreeNode(iterate.Text);
                CreateNode.Tag = iterate;
                TreeNode ParentNode = null;
                //if the parentID is 0, then add it to the TreeView.
                if (iterate.ParentId != 0)
                {
                    //if there is a parent, look through all the nodes, and find it.
                    foreach (TreeNode IterateNode in getAllNodes(tvwTest))
                    {
                        TestData grabdata = IterateNode.Tag as TestData;
                        if (grabdata != null)
                        {
                            if (grabdata.NodeId == iterate.ParentId)
                            {
                                ParentNode = IterateNode;
                                break;
                            }
                        }
                    }
                }
                if (ParentNode == null)
                    tvwTest.Nodes.Add(CreateNode);
                else
                    ParentNode.Nodes.Add(CreateNode);
            }
        }

    }
}



2) version using lamda

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace NodesTest
{
    public partial class Form1 : Form
    {
        private class TestData
        {
            public TestData(int pNodeId, int pParent, String pText)
            {
                NodeId = pNodeId;
                ParentId = pParent;
                Text = pText;
            }

            public int NodeId { get; set; }
            public int ParentId { get; set; }
            public String Text { get; set; }
        }

        //static test data.
        private TestData[] testinfo = new TestData[]
                                          {
                                              new TestData(1, 0, "ID 1"),
                                              new TestData(2, 0, "ID 2"),
                                              new TestData(3, 1, "ID 3 Child 1 of ID 1 "),
                                              new TestData(4, 1, "ID 4 Child 2 of ID 1"),
                                              new TestData(5, 2, "ID 5 Child 1 of ID 2"),
                                              new TestData(6, 2, "ID 5 Child 2 of ID 2"),
                                              new TestData(7, 2, "ID 5 Child 3 of ID 2"),
                                              new TestData(8, 5, "ID 8 Child 1 of ID 5")
                                          };
        public Form1()
        {
            InitializeComponent();
        }

        public IEnumerable<TreeNode> getAllNodes(TreeView tvw)
        {
            foreach (TreeNode iterate in tvw.Nodes)
            {
                yield return iterate;
                foreach (TreeNode childiterate in getAllNodes(iterate))
                {
                    yield return childiterate;
                }
            }
        }

        public IEnumerable<TreeNode> getAllNodes(TreeNode tvw)
        {
            //iterate through all nodes...
            foreach (TreeNode iterateNode in tvw.Nodes)
            {
                yield return iterateNode;
                foreach (TreeNode childiterate in getAllNodes(iterateNode))
                {
                    yield return childiterate;
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            tvwTest.Nodes.Clear();
            foreach (var iterate in testinfo)
            {
                TreeNode CreateNode = new TreeNode(iterate.Text);
                CreateNode.Tag = iterate;
                TreeNode ParentNode = null;
                //if the parentID is 0, then add it to the TreeView.
                if (iterate.ParentId != 0)
                {
                    //if there is a parent, look through all the nodes, and find it.
                    foreach (TreeNode IterateNode in getAllNodes(tvwTest))
                    {
                        TestData grabdata = IterateNode.Tag as TestData;
                        if (grabdata != null)
                        {
                            if (grabdata.NodeId == iterate.ParentId)
                            {
                                ParentNode = IterateNode;
                                break;
                            }
                        }
                    }
                }
                if (ParentNode == null)
                    tvwTest.Nodes.Add(CreateNode);
                else
                    ParentNode.Nodes.Add(CreateNode);
            }
        }

    }
}


3) version using functions


   1: using System;

   2: using System.Collections.Generic;

   3: using System.ComponentModel;

   4: using System.Data; 

   5: using System.Drawing;

   6: using System.Linq;

   7: using System.Text;

   8: using System.Windows.Forms;

   9: using O2.DotNetWrappers.ExtensionMethods;

  10:  

  11: namespace NodesTest

  12: {

  13:     public class TestForm1                    // add a test class

  14:     {

  15:         public void showForm()                // create a first method to be invoked

  16:         {

  17:             new Form1().ShowDialog();        // start the dialog    

  18:         }

  19:     }

  20:  

  21:     public partial class Form1 : Form

  22:     {

  23:         private class TestData

  24:         {

  25:             public TestData(int pNodeId, int pParent, String pText)

  26:             {

  27:                 NodeId = pNodeId;

  28:                 ParentId = pParent;

  29:                 Text = pText;

  30:             }

  31:  

  32:             public int NodeId { get; set; }

  33:             public int ParentId { get; set; }

  34:             public String Text { get; set; }

  35:         }

  36:  

  37:         //static test data.

  38:         private TestData[] testinfo = new TestData[]

  39:                                           {

  40:                                               new TestData(1, 0, "ID 1"),

  41:                                               new TestData(2, 0, "ID 2"),

  42:                                               new TestData(3, 1, "ID 3 Child 1 of ID 1 "),

  43:                                               new TestData(4, 1, "ID 4 Child 2 of ID 1"),

  44:                                               new TestData(5, 2, "ID 5 Child 1 of ID 2"),

  45:                                               new TestData(6, 2, "ID 5 Child 2 of ID 2"),

  46:                                               new TestData(7, 2, "ID 5 Child 3 of ID 2"),

  47:                                               new TestData(8, 5, "ID 8 Child 1 of ID 5")

  48:                                           };

  49:         public Form1()

  50:         {

  51:             //InitializeComponent()

  52:             tvwTest        = this.add_TreeView();                  // Creating a TreeView            

  53:             button1        = this.insert_Above(30)                // Adding a Panel above

  54:                                  .add_Button("click me");        // Adding a Button                          

  55:             button1.top(5).Click +=  button1_Click;                    // Setting the Click event

  56:             

  57:             button1.append_Link("REPL Form", ()=> this.script_Me()).top(10);

  58:             

  59:             //O2Thread.mtaThread(()=>button1.click());

  60:         }

  61:         

  62:         public TreeView                   tvwTest             { get; set; }    // TreeView as an Property

  63:         public Button                      button1             { get; set; }    // Button as an Property

  64:         public Dictionary<int, TreeNode> nodes_XRefs        { get; set; }

  65:         

  66:         private TreeNode findParent(int parentId)

  67:         {

  68:             if (parentId ==0)

  69:                 return tvwTest.rootNode();

  70:             return nodes_XRefs.value(parentId);

  71:         }             

  72:         private void  addItem(TreeNode parentNode, int id, string text)                

  73:         {

  74:             if (parentNode.notNull())

  75:             {

  76:                 var newNode = parentNode.add_Node(text,id);

  77:                 nodes_XRefs.add(id, newNode);

  78:             }                        

  79:         }         

  80:         private void populateTreeView()

  81:         {

  82:             tvwTest.clear();     

  83:             nodes_XRefs  = new Dictionary<int, TreeNode>();          

  84:             foreach (var iterate in testinfo)                                           

  85:             {                

  86:                 var parentNode = findParent(iterate.ParentId);

  87:                 addItem(parentNode, iterate.NodeId, iterate.Text);                                                                    

  88:             }

  89:         }

  90:         private void button1_Click(object sender, EventArgs e)

  91:         {                                                                                  

  92:             populateTreeView();            

  93:             tvwTest.expandAll();

  94:         }

  95:  

  96:     }

  97: }


4) class with TestData

using System;

public class TestData
{
        
    public int NodeId { get; set; }
    public int ParentId { get; set; }
    public String Text { get; set; }
    
    public TestData(int pNodeId, int pParent, String pText)
    {
        NodeId = pNodeId;
        ParentId = pParent;
        Text = pText;
    }
    public static TestData[] GetTestData()
    {
        return new TestData[]
                      {
                          new TestData(1, 0, "ID 1"),
                          new TestData(2, 0, "ID 2"),
                          new TestData(3, 1, "ID 3 Child 1 of ID 1 "),
                          new TestData(4, 1, "ID 4 Child 2 of ID 1"),
                          new TestData(5, 2, "ID 5 Child 1 of ID 2"),
                          new TestData(6, 2, "ID 5 Child 2 of ID 2"),
                          new TestData(7, 2, "ID 5 Child 3 of ID 2"),
                          new TestData(8, 5, "ID 8 Child 1 of ID 5")
                      };
    }
}


5) H2 script that consumes the TestData class and creates the stand-alone exe:

var topPanel = "PoC - Populate TreeView from data"
                .popupWindow(700,400);
//var topPanel = panel.clear().add_Panel();

var tvwTest  = topPanel.add_TreeView();
var button   = tvwTest.insert_Right(200)
                     .add_Button("click me").fill();
 
var testinfo = TestData.GetTestData();

var nodes_XRefs = new Dictionary<int, TreeNode>();
            
Func<int, TreeNode> findParent = 
    (parentId)=>{
                    if (parentId ==0)
                        return tvwTest.rootNode();
                    return nodes_XRefs.value(parentId);
                };

Action<TreeNode, int, string> addItem = 
    (parentNode,id, text)=>    
        {
            if (parentNode.notNull())
            {
                var newNode = parentNode.add_Node(text,id);
                nodes_XRefs.add(id, newNode);
            }                        
        };

Action populateData = 
    ()=>{                            
            tvwTest.clear();     
            
            foreach (var iterate in testinfo)                                           
            {                
                var parentNode = findParent(iterate.ParentId);
                addItem(parentNode, iterate.NodeId, iterate.Text);                                                                    
            }
            
            tvwTest.expandAll();
        };

button.onClick(()=>populateData());



//O2File:E:\O2\O2.Temp\5_22_2013\tmpCCF6.tmp.cs