0

I have an utility in C# that parses source code and creates an xml report of all misspelled words in quoted literal strings, the file and location in the file where they appear. I have another utility that reads this xml file and loads it into a treeview which looks like the following layout:

MisspelledWords
 |
 |___badd
 |    |__Suggestions
 |    |  |__bad
 |    |  
 |    |__Locations
 |       |__Location
 |          |__FileName
 |          |  |__ C:\Workspaces\MyProject\Project1\program.cs
 |          | 
 |          |__LineNumber
 |          |   |
 |          |   |_ 31
 |          |
 |          |__Original Line 
 |   
 |___spellling
 |    |__Suggestions
 |    |  |__spelling
 |    |  
 |    |__Locations
 |       |__Location
 |          |__FileName
 |          |  |__ C:\Workspaces\MyProject\Project1\program.cs
 |          | 
 |          |__LineNumber
 |          |   |
 |          |   |_ 55
 |          |
 |          |__Original Line 

Everything loads into the treeview successfully sorted the first time, however when I clear the treeview, reload it and sort (treeview1.sort), all of the child nodes are added to the last misspelled word node on level 1.

Here is a snippet of my current code to load and sort.

private void button1_Click(object sender, EventArgs e)
{
    if (!bTreeLoaded)
    {
        //Add the "Ignored" Top Level node. 
        TreeNode ignoreNode = new TreeNode("Ignored List");
        treeView1.Nodes.Add(ignoreNode);

        XmlDocument xmldoc = new XmlDocument();
        xmldoc.Load(textBox1.Text);
        TreeNode misspelledWordsNode = new TreeNode(xmldoc.DocumentElement.Name);
        treeView1.Nodes.Add(misspelledWordsNode);
        AddNode(xmldoc.DocumentElement, misspelledWordsNode);
        treeView1.Sort();
        bTreeLoaded = true;
    }
    else
    {
        MessageBox.Show("Data has already been loaded");
    }
}

private void AddNode(XmlNode inXmlNode, TreeNode inTreeNode)
{
    XmlNode xNode;
    TreeNode tNode;
    XmlNodeList nodeList;
    int i = 0;

    if (inXmlNode.HasChildNodes)
    {
        nodeList = inXmlNode.ChildNodes;
        for (i = 0; i <= nodeList.Count - 1; i++)
        {
            xNode = inXmlNode.ChildNodes[i];
            inTreeNode.Nodes.Add(new TreeNode(xNode.Name));
            tNode = inTreeNode.Nodes[i];
            AddNode(xNode, tNode);
        }
    }
    else
    {
        inTreeNode.Text = (inXmlNode.OuterXml).Trim();
    }
}


private void button2_Click(object sender, EventArgs e)
{
    treeView1.Nodes.Clear();
    bTreeLoaded = false;
}

My XML file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<MisspelledWords xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <badd>
    <Suggestions>
      <Suggestion>bad</Suggestion>
    </Suggestions>
    <Locations>
      <Location>
        <FileName>C:\Workspaces\AHLTA\Current\VB6\global.bas</FileName>
        <LineNumber>31</LineNumber>
        <OriginalLine>s = "badd spellling"</OriginalLine>
      </Location>
    </Locations>
  </badd>
</MisspelledWords>
  • 1) What does your XML look like? 2) How are you clearing the tree before reloading it? – dbc Jan 05 '15 at 21:37
  • OK. So, how do you clear the tree? Do you do `treeView1.Nodes.Clear(); bTreeLoaded = false;`? – dbc Jan 06 '15 at 09:21
  • Added snippet of xml above. – GhostHunterJim Jan 06 '15 at 09:23
  • Thanks, I saw it. But if I run your code with the new XML and try to load the XML file a second time, I get the message box `"Data has already been loaded"`. So how do clear the treeview, reload it and sort? – dbc Jan 06 '15 at 09:24
  • I have another button on the form that runs treeView1.Nodes.Clear() then resets bTreeLoaded to false. Then I repopulate the tree and sort with button1. – GhostHunterJim Jan 06 '15 at 11:01
  • I still can't reproduce your problem given your code to load and clear the tree and your XML. But you might consider using the [WPF TreeView](http://msdn.microsoft.com/en-us/library/system.windows.controls.treeview%28v=vs.110%29.aspx) since it has an [automatic XML binding.](http://msdn.microsoft.com/en-us/library/system.windows.data.xmldataprovider%28v=vs.110%29.aspx). – dbc Jan 06 '15 at 11:21

1 Answers1

0

I can't reproduce your problem since you don't show how you clear your tree of pre-existing nodes before reloading. In the code shown, you just don't allow nodes to be re-loaded in the tree.

In general, if you want to update a tree to some hierarchical data, you need to merge the lists at each level, keying the nodes with either their name or their tag. For instance, the following seems to work for your XML:

    private void LoadXML_Click(object sender, EventArgs e)
    {
        treeView1.BeginUpdate();
        try
        {
            //Add the "Ignored" Top Level node. 
            if (!treeView1.Nodes.ContainsKey("Ignored List"))
                treeView1.Nodes.Add(new TreeNode { Name = "Ignored List", Text = "Ignored List" });

            var xmldoc = GetXmlDocument(); // From your UI
            int changed = AddOrMergeNodes(xmldoc);
            Debug.WriteLine("Added or removed " + changed + " nodes");

            if (changed > 0)
            {
                treeView1.ExpandAll();
                treeView1.Sort();
            }
        }
        finally
        {
            treeView1.EndUpdate();
        }

        treeView1.Focus();
    }

    private void clearXML_Click(object sender, EventArgs e)
    {
        treeView1.Nodes.Clear();
    }

    static Dictionary<string, List<TreeNode>> ToNodeDictionary(TreeNodeCollection nodes)
    {
        return nodes.Cast<TreeNode>().Aggregate(new Dictionary<string, List<TreeNode>>(), (d, n) => { d.Add(n.Name, n); return d; });
    }

    int AddOrMergeNodes(XmlDocument xmldoc)
    {
        var dict = ToNodeDictionary(treeView1.Nodes);
        return AddOrMergeNode(treeView1.Nodes, dict, xmldoc.DocumentElement);
    }

    static int AddOrMergeNodes(TreeNodeCollection treeNodeCollection, XmlNodeList xmlNodeList)
    {
        int changed = 0;
        var dict = ToNodeDictionary(treeNodeCollection);
        foreach (XmlNode inXmlNode in xmlNodeList)
        {
            changed += AddOrMergeNode(treeNodeCollection, dict, inXmlNode);
        }
        foreach (var leftover in dict.Values.SelectMany(l => l))
        {
            treeNodeCollection.Remove(leftover);
            changed++;
        }
        return changed;
    }

    static int AddOrMergeNode(TreeNodeCollection treeNodeCollection, Dictionary<string, List<TreeNode>> dict, XmlNode inXmlNode)
    {
        int changed = 0;
        var name = inXmlNode.Name;

        TreeNode node;
        if (!dict.TryRemoveFirst(name, out node))
        {
            node = new TreeNode { Name = name, Text = name };
            treeNodeCollection.Add(node);
            changed++;
        }
        Debug.Assert(treeNodeCollection.Contains(node), "treeNodeCollection.Contains(node)");
        if (inXmlNode.HasChildNodes)
        {
            var text = name;
            if (node.Text != text)
                node.Text = name;
            changed += AddOrMergeNodes(node.Nodes, inXmlNode.ChildNodes);
        }
        else
        {
            var text = (inXmlNode.OuterXml).Trim();
            if (node.Text != text)
                node.Text = text;
            node.Nodes.Clear();
        }
        return changed;
    }

And then some extension methods dealing with dictionaries of lists, for convenience:

public static class DictionaryExtensions
{
    public static void Add<TKey, TValueList, TValue>(this IDictionary<TKey, TValueList> listDictionary, TKey key, TValue value)
        where TValueList : IList<TValue>, new()
    {
        if (listDictionary == null)
            throw new ArgumentNullException();
        TValueList values;
        if (!listDictionary.TryGetValue(key, out values))
            listDictionary[key] = values = new TValueList();
        values.Add(value);
    }

    public static bool TryGetValue<TKey, TValueList, TValue>(this IDictionary<TKey, TValueList> listDictionary, TKey key, int index, out TValue value)
        where TValueList : IList<TValue>
    {
        TValueList list;
        if (!listDictionary.TryGetValue(key, out list))
            return Returns.False(out value);
        if (index < 0 || index >= list.Count)
            return Returns.False(out value);
        value = list[index];
        return true;
    }

    public static bool TryRemoveFirst<TKey, TValueList, TValue>(this IDictionary<TKey, TValueList> listDictionary, TKey key, out TValue value)
        where TValueList : IList<TValue>
    {
        TValueList list;
        if (!listDictionary.TryGetValue(key, out list))
            return Returns.False(out value);
        var count = list.Count;
        if (count > 0)
        {
            value = list[0];
            list.RemoveAt(0);
            if (--count == 0)
                listDictionary.Remove(key);
            return true;
        }
        else
        {
            listDictionary.Remove(key); // Error?
            return Returns.False(out value);
        }
    }
}

public static class Returns
{
    public static bool False<TValue>(out TValue value)
    {
        value = default(TValue);
        return false;
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
  • So if I understand what you're saying, the LoadXML_click method would replace my button1_click, right? – GhostHunterJim Jan 06 '15 at 11:10
  • @GhostHunterJim - yes, sorry for the confusion. I habitually rename buttons to be descriptive of what they do. You should feel free to rename it back. – dbc Jan 06 '15 at 11:11
  • Thanks for your time and suggestions on this. I'm still a bit confused. In your example code, I'm not sure where I would need to add the addition of the "Misspelledwords" node to the tree. I see the ignored list node. Would I add it in between these two lines? var xmldoc = GetXmlDocument(); // From your UI nt changed = AddOrMergeNodes(xmldoc); Does the "AddOrMergedModes()" method replace the AddNodes method I have which is called in Button1? – GhostHunterJim Jan 06 '15 at 11:24
  • OMG! That's awesome! I added your example and it worked straight up. Thanks so much!! :) – GhostHunterJim Jan 06 '15 at 11:30
  • 1) `AddOrMergeNodes(XmlDocument xmldoc)` adds or updates a root node to the tree corresponding to the root node of the XmlDocument. 2) `AddOrMergeNode(TreeNodeCollection treeNodeCollection, Dictionary> dict, XmlNode inXmlNode)` adds or updates a node in the TreeNodeCollection corresponding to the given XmlNode. 3) AddOrMergeNodes(TreeNodeCollection treeNodeCollection, XmlNodeList xmlNodeList) adds or updates all the nodes in the given TreeNodeCollection for the given XmlNodeList. – dbc Jan 06 '15 at 11:31
  • @GhostHunterJim - glad to help. I don't know WPF as well as WinForms, but I did find this example of how to do this in WPF: http://www.codeproject.com/Articles/317766/Displaying-XML-in-a-WPF-TreeView – dbc Jan 06 '15 at 11:32