Populate WinForms TreeView from DataTable

26,131

Solution 1

To attempt to solve this problem, I created a sample windows form and wrote the following code. I envisioned the datatable design as follows:

 NoteID  NoteName  ParentNoteID
   "1"    "One"        null
   "2"    "Two"        "1"
   "3"    "Three"      "2"
   "4"    "Four"       null
...

This should create a Tree as (sorry, I'm not very good with ASCII art!):

One
 |
 ——Two
 |
 ————Three
 |
Four

Pseudocode goes like this:

  1. Iterate through all the rows in the datatable.
  2. For each row, create a TreeNode and set it's properties. Recursively repeat the process for all rows that have a ParentNodeID matching this row's ID.
  3. Each complete iteration returns a node that will contain all matching childnodes with infinite nesting.
  4. Add the completed nodelist to the TreeView.

The problem in your scenario arises from the fact the "foreign key" refers to a column in the same table. This means that when we iterate through the rows, we have to keep track of which rows have already been parsed. For example, in the table above, the node matching the second and third rows are already added in the first complete iteration. Therefore, we must not add them again. There are two ways to keep track of this:

  1. Maintain a list of ID's that have been done (doneNotes). Before adding each new node, check if the noteID exists in that list. This is the faster method and should normally be preferred. (this method is commented out in the code below)
  2. For each iteration, use a predicate generic delegate (FindNode) to search the list of added nodes (accounting for nested nodes) to see if the to-be added node exists in that list. This is the slower solution, but I kinda like complicated code! :P

Ok, here's the tried and tested code (C# 2.0):


public partial class TreeViewColor : Form
{
  private DataTable dt;
  // Alternate way of maintaining a list of nodes that have already been added.
  //private List<int> doneNotes;
  private static int noteID;

  public TreeViewColor()
  {
    InitializeComponent();
  }

  private void TreeViewColor_Load(object sender, EventArgs e)
  {
    CreateData();
    CreateNodes();

    foreach (TreeNode rootNode in treeView1.Nodes)
    {
      ColorNodes(rootNode, Color.MediumVioletRed, Color.DodgerBlue);
    }
  }

  private void CreateData()
  {
    dt = new DataTable("CaseNotes");
    dt.Columns.Add("NoteID", typeof(string));
    dt.Columns.Add("NoteName", typeof(string));
    DataColumn dc = new DataColumn("ParentNoteID", typeof(string));
    dc.AllowDBNull = true;
    dt.Columns.Add(dc);

    // Add sample data.
    dt.Rows.Add(new string[] { "1", "One", null });
    dt.Rows.Add(new string[] { "2", "Two", "1" });
    dt.Rows.Add(new string[] { "3", "Three", "2" });
    dt.Rows.Add(new string[] { "4", "Four", null });
    dt.Rows.Add(new string[] { "5", "Five", "4" });
    dt.Rows.Add(new string[] { "6", "Six", null });
    dt.Rows.Add(new string[] { "7", "Seven", null });
    dt.Rows.Add(new string[] { "8", "Eight", "7" });
    dt.Rows.Add(new string[] { "9", "Nine", "8" });
  }

  private void CreateNodes()
  {
    DataRow[] rows = new DataRow[dt.Rows.Count];
    dt.Rows.CopyTo(rows, 0);
    //doneNotes = new List<int>(9);

    // Get the TreeView ready for node creation.
    // This isn't really needed since we're using AddRange (but it's good practice).
    treeView1.BeginUpdate();
    treeView1.Nodes.Clear();

    TreeNode[] nodes = RecurseRows(rows);
    treeView1.Nodes.AddRange(nodes);

    // Notify the TreeView to resume painting.
    treeView1.EndUpdate();
  }

  private TreeNode[] RecurseRows(DataRow[] rows)
  {
    List<TreeNode> nodeList = new List<TreeNode>();
    TreeNode node = null;

    foreach (DataRow dr in rows)
    {
      node = new TreeNode(dr["NoteName"].ToString());
      noteID = Convert.ToInt32(dr["NoteID"]);

      node.Name = noteID.ToString();
      node.ToolTipText = noteID.ToString();

      // This method searches the "dirty node list" for already completed nodes.
      //if (!doneNotes.Contains(doneNoteID))

      // This alternate method using the Find method uses a Predicate generic delegate.
      if (nodeList.Find(FindNode) == null)
      {
        DataRow[] childRows = dt.Select("ParentNoteID = " + dr["NoteID"]);
        if (childRows.Length > 0)
        {
          // Recursively call this function for all childRowsl
          TreeNode[] childNodes = RecurseRows(childRows);

          // Add all childnodes to this node.
          node.Nodes.AddRange(childNodes);
        }

        // Mark this noteID as dirty (already added).
        //doneNotes.Add(noteID);
        nodeList.Add(node);
      }
    }

    // Convert this List<TreeNode> to an array so it can be added to the parent node/TreeView.
    TreeNode[] nodeArr = nodeList.ToArray();
    return nodeArr;
  }

  private static bool FindNode(TreeNode n)
  {
    if (n.Nodes.Count == 0)
      return n.Name == noteID.ToString();
    else
    {
      while (n.Nodes.Count > 0)
      {
        foreach (TreeNode tn in n.Nodes)
        {
          if (tn.Name == noteID.ToString())
            return true;
          else
            n = tn;
        }
      }
      return false;
    }
  }

  protected void ColorNodes(TreeNode root, Color firstColor, Color secondColor)
  {
    root.ForeColor = root.Index % 2 == 0 ? firstColor : secondColor;

    foreach (TreeNode childNode in root.Nodes)
    {
      Color nextColor = childNode.ForeColor = childNode.Index % 2 == 0 ? firstColor : secondColor;

      if (childNode.Nodes.Count > 0)
      {
        // alternate colors for the next node
        if (nextColor == firstColor)
          ColorNodes(childNode, secondColor, firstColor);
        else
          ColorNodes(childNode, firstColor, secondColor);
      }
    }
  }
}

Solution 2

I've created much simplier extension method for TreeView, involving use of new simple extending class that adds two useful properties to TreeNode.

    internal class IdNode : TreeNode
    {
        public object Id { get; set; }
        public object ParentId { get; set; }
    }

    public static void PopulateNodes(this TreeView treeView1, DataTable dataTable, string name, string id, string parentId)
    {
        treeView1.BeginUpdate();
        foreach (DataRow row in dataTable.Rows)
        {
            treeView1.Nodes.Add(new IdNode() { Name = row[name].ToString(), Text = row[name].ToString(), Id = row[id], ParentId = row[parentId], Tag = row });
        }
        foreach (IdNode idnode in GetAllNodes(treeView1).OfType<IdNode>())
        {
            foreach (IdNode newparent in GetAllNodes(treeView1).OfType<IdNode>())
            {
                if (newparent.Id.Equals(idnode.ParentId))
                {
                    treeView1.Nodes.Remove(idnode);
                    newparent.Nodes.Add(idnode);
                    break;
                }
            }
        }
        treeView1.EndUpdate();
    }

    public static List<TreeNode> GetAllNodes(this TreeView tv)
    {
        List<TreeNode> result = new List<TreeNode>();
        foreach (TreeNode child in tv.Nodes)
        {
            result.AddRange(GetAllNodes(child));
        }
        return result;
    }
    public static List<TreeNode> GetAllNodes(this TreeNode tn)
    {
        List<TreeNode> result = new List<TreeNode>();
        result.Add(tn);
        foreach (TreeNode child in tn.Nodes)
        {
            result.AddRange(GetAllNodes(child));
        }
        return result;
    }

Thanks to the modiX for his methods to get all (nested) nodes.

Share:
26,131
Refracted Paladin
Author by

Refracted Paladin

My Philosophy

Updated on June 22, 2020

Comments

  • Refracted Paladin
    Refracted Paladin almost 4 years

    I have a WinForm TreeView Control that displays the Parent Child relationship of CaseNotes(I know that means nothing to most of you but it helps me visualize the answers).

    I have a DataTable of the CaseNotes that I need to display. The Parent/Child is defined as: If the row has a ParentNoteID then it is a childNode of that note otherwise it is a rootNode. It could also be a parent note(but not a rootNode) if another row has it's ID as it's ParentNoteID.

    To complicate(maybe simplify) things I have the below working(mostly) code that colors the nodes alternatingly. I manually created a static collection for the treeview and it colors them fairly correctly. Now I need to dynamically populate the Nodes from my DataTable.

    Since I already am going thru the treeview node by node shouldn't I be able to append the data into this process somehow? Maybe I need to build the nodes first and then color as a separate routine but the Recursion Method would still apply, correct?

    Lets say I want to display CaseNoteID for each Node. That is returned in the DataTable and is unique.

    foreach (TreeNode rootNode in tvwCaseNotes.Nodes)
            {
                ColorNodes(rootNode, Color.MediumVioletRed, Color.DodgerBlue);
    
            }
    protected void ColorNodes(TreeNode root, Color firstColor, Color secondColor)
        {
            root.ForeColor = root.Index % 2 == 0 ? firstColor : secondColor;
    
            foreach (TreeNode childNode in root.Nodes)
            {
                Color nextColor = childNode.ForeColor = childNode.Index % 2 == 0 ? firstColor : secondColor;
    
                if (childNode.Nodes.Count > 0)
                {
                    // alternate colors for the next node
                    if (nextColor == firstColor)
                        ColorNodes(childNode, secondColor, firstColor);
                    else
                        ColorNodes(childNode, firstColor, secondColor);
                }
            }
        }
    

    EDIT

    My thoughts/attempts so far:

            public void BuildSummaryView()
        {
            tvwCaseNotes.Nodes.Clear();
    
            DataTable cNotesForTree = CurrentCaseNote.GetAllCNotes(Program._CurrentPerson.PersonID);
            foreach (var cNote in cNotesForTree.Rows)
            {
    
                tvwCaseNotes.Nodes.Add(new TreeNode("ContactDate"));
            }
            FormPaint();
        }
    

    Obviously this is flawed. One it just display's ContactDate over and over. Granted it shows it the correct number of times but I would like the Value of ContactDate(which is a Column in the database and is being returned in the DataTable. Second I need to add the ChildNode Logic. A if (node.parentNode = node.CaseNoteID) blah...

    EDIT 2

    So I found this link, here, and it makes it seem like I need to get my DataTable into an ArrayList. Is that correct?

    EDIT 3

    Okay, thanks to Cerebus this is mostly working. I just have one more question. How do I take this-->

    DataTable cNotesForTree = CurrentCaseNote.GetAllCNotes(Program._CurrentPerson.PersonID);
    

    and use my returned DataTable in this? Do I just replace this -->

        dt = new DataTable("CaseNotes");
    dt.Columns.Add("NoteID", typeof(string));
    dt.Columns.Add("NoteName", typeof(string));
    DataColumn dc = new DataColumn("ParentNoteID", typeof(string));
    dc.AllowDBNull = true;
    dt.Columns.Add(dc);
    
    // Add sample data.
    dt.Rows.Add(new string[] { "1", "One", null });
    dt.Rows.Add(new string[] { "2", "Two", "1" });
    dt.Rows.Add(new string[] { "3", "Three", "2" });
    dt.Rows.Add(new string[] { "4", "Four", null });
    dt.Rows.Add(new string[] { "5", "Five", "4" });
    dt.Rows.Add(new string[] { "6", "Six", null });
    dt.Rows.Add(new string[] { "7", "Seven", null });
    dt.Rows.Add(new string[] { "8", "Eight", "7" });
    dt.Rows.Add(new string[] { "9", "Nine", "8" });
    

    My confusion, I think, is do I still need to do the Column.Add and Row.Adds? Also how would the DataColumn translate to my real data structure? Sorry for the very ignorant questions, the good news is I never have to ask twice.

    EDIT 4

    The following is providing a runtime error.

    if (nodeList.Find(FindNode) == null)
      {
        DataRow[] childRows = dt.Select("ParentNoteID = " + dr["NoteID"]);
        if (childRows.Length > 0)
        {
          // Recursively call this function for all childRowsl
          TreeNode[] childNodes = RecurseRows(childRows);
    
          // Add all childnodes to this node.
          node.Nodes.AddRange(childNodes);
        }
    
        // Mark this noteID as dirty (already added).
        //doneNotes.Add(noteID);
        nodeList.Add(node);
      }
    

    The error is as follows --> Cannot find column [ea8428e4] Which is the first 8 digits of the correct NoteID(I have to use a Guid). Should it be looking for a column of that name?? Because I am using a Guid is there something else I need to do? I changed all the references in mine and your code to Guid...

  • Cerebrus
    Cerebrus about 15 years
    Please "note" (pun intended) that I have left your ColorNodes function as-is.
  • Refracted Paladin
    Refracted Paladin about 15 years
    This may be a dumb question but when creating and using partial class like this where do you traditionally place this. A seperate class library? As part of my BLL? Thanks!
  • Cerebrus
    Cerebrus about 15 years
    No, this is a pure Windows form.
  • Refracted Paladin
    Refracted Paladin about 15 years
    yep, sorry I figured that out a little after posting but forgot to come back here.
  • Sagotharan
    Sagotharan over 12 years
    Thanks Cerebrus. But this Code code not works perfectly on some data format,... Please see the stackoverflow.com/questions/9044104/…
  • slhck
    slhck about 12 years
    By the way, there's someone 1:1 copying your answer: stackoverflow.com/a/9045072/435093