简体   繁体   English

将我的头缠在N个家长/孩子协会上

[英]Wrapping my head around N parent->child associations

I'll try to explain this the best I can. 我将尽力解释这一点。 I'm having quite a bit of difficulty trying to figure out this logic. 我在弄清楚这种逻辑时遇到了很多困难。

Basically, I have a collection that includes thousands of objects which are each made up of a Parent and a Child property. 基本上,我有一个包含数千个对象的集合,每个对象都由Parent和Child属性组成。

So, roughly, this: 因此,大致而言:


public class MyObject{
     public string Parent { get; set; }
     public string Child { get; set; }
}

What I'm trying to figure out is how to build this out into a plain TreeView control. 我想弄清楚的是如何将其构建到普通的TreeView控件中。 I need to build the relationships but I can't figure out how to because they can be mixed. 我需要建立关系,但是我不知道该怎么做,因为它们可以混合在一起。 I can probably explain this better with what the tree should look like: 我大概可以用树的样子更好地解释这一点:

So if I have the following items inside of my collection: 因此,如果我的收藏夹中有以下物品:


0. Parent: "A", Child: "B"
1. Parent: "B", Child: "C"
2. Parent: "B", Child: "D"

I would want my tree to look this like: 我希望我的树看起来像这样:


-A
--B
---C
-A
--B
---D
-B
--C
-B
--D

How can I do this in C#? 如何在C#中做到这一点? I would need it to support up to N relationships as we have some branches I would expect to reach about 50 nodes deep. 我需要它来支持最多N个关系,因为我们有一些分支预计可以达到约50个节点的深度。

UPDATE 更新

This problem actually turned out to be considerably more complex than I originally realized, given the requirement of repeating the entire tree for each path. 考虑到需要为每条路径重复整个树 ,这个问题实际上比我最初意识到的要复杂得多。 I've simply deleted the old code as I don't want to add any further confusion. 我只是删除了旧代码,因为我不想增加任何混乱。

I do want to keep it on record that using a recursive data structure makes this easier: 我确实希望将其记录在案,因为使用递归数据结构可以使此操作更容易:

public class MyRecursiveObject
{
    public MyRecursiveObject Parent { get; set; }
    public string Name { get; set; }
    public List<MyRecursiveObject> Children { get; set; }
}

You'll see very clearly why this is easier after reading the implementation code below: 阅读下面的实现代码后,您将非常清楚地知道为什么这样做更容易:

private void PopulateTree(IEnumerable<MyObject> items)
{
    var groupedItems =
        from i in items
        group i by i.Parent into g
        select new { Name = g.Key, Children = g.Select(c => c.Child) };
    var lookup = groupedItems.ToDictionary(i => i.Name, i => i.Children);
    foreach (string parent in lookup.Keys)
    {
        if (lookup.ContainsKey(parent))
            AddToTree(lookup, Enumerable.Empty<string>(), parent);
    }
}

private void AddToTree(Dictionary<string, IEnumerable<string>> lookup,
    IEnumerable<string> path, string name)
{
    IEnumerable<string> children;
    if (lookup.TryGetValue(name, out children))
    {
        IEnumerable<string> newPath = path.Concat(new string[] { name });
        foreach (string child in children)
            AddToTree(lookup, newPath, child);
    }
    else
    {
        TreeNode parentNode = null;
        foreach (string item in path)
            parentNode = AddTreeNode(parentNode, item);
        AddTreeNode(parentNode, name);
    }
}

private TreeNode AddTreeNode(TreeNode parent, string name)
{
    TreeNode node = new TreeNode(name);
    if (parent != null)
        parent.Nodes.Add(node);
    else
        treeView1.Nodes.Add(node);
    return node;
}

First of all, I realized that the dictionary will contain keys for intermediate nodes as well as just the root nodes, so we don't need two recursive calls in the recursive AddToTree method in order to get the "B" nodes as roots; 首先,我意识到字典将包含中间节点和根节点的键,因此我们不需要在递归AddToTree方法中进行两次递归调用即可将“ B”节点作为根。 the initial walk in the PopulateTree method already does it. PopulateTree方法中的初始遍历已经做到了。

What we do need to guard against is adding leaf nodes in the initial walk; 我们确实需要警惕在最初的步行路程,将叶节点; using the data structure in question, these are detectable by checking whether or not there is a key in the parent dictionary. 使用所讨论的数据结构,可以通过检查父字典中是否有键来检测这些数据。 With a recursive data structure, this would be way easier: Just check for Parent == null . 使用递归数据结构,这会更容易:只需检查Parent == null But, a recursive structure is not what we have, so the code above is what we have to use. 但是,递归结构不是我们所拥有的,因此上面的代码是我们必须使用的。

The AddTreeNode is mostly a utility method, so we don't have to keep repeating this null-checking logic later. AddTreeNode主要是一种实用程序方法,因此我们不必在以后继续重复此空检查逻辑。

The real ugliness is in the second, recursive AddToTree method. 真正的丑陋是在第二个递归AddToTree方法中。 Because we are trying to create a unique copy of every single subtree, we can't simply add a tree node and then recurse with that node as the parent. 因为我们试图为每个子树创建唯一的副本,所以我们不能简单地添加一个树节点然后以该节点作为父节点进行递归。 "A" only has one child here, "B", but "B" has two children, "C" and "D". “ A”在这里只有一个孩子,“ B”,但是“ B”有两个孩子,“ C”和“ D”。 There needs to be two copies of "A", but there's no way to know about that when "A" is originally passed to the AddToTree method. 必须有两个“ A”副本,但是当“ A”最初传递给AddToTree方法时,没有办法知道。

So what we actually have to do is not create any nodes until the final stage, and store a temporary path, for which I've chosen IEnumerable<string> because it's immutable and therefore impossible to mess up. 因此,我们实际上要做的是直到最后阶段才创建任何节点,并存储一个临时路径,为此我选择了IEnumerable<string>因为它是不可变的,因此不可能搞乱。 When there are more children to add, this method simply adds to the path and recurses; 当要添加的子项更多时,此方法将简单地添加到路径并递归。 when there are no more children, it walks the entire saved path and adds a node for each. 当没有更多孩子时,它将遍历整个保存的路径并为每个添加一个节点。

This is extremely inefficient because we are now creating a new enumerable on every invocation of AddToTree . 这是非常低效的,因为我们现在每次AddToTree都创建一个新的枚举。 For large numbers of nodes, it is likely to chew up a lot of memory. 对于大量节点,可能会占用大量内存。 This works, but it would be a lot more efficient with a recursive data structure. 这行得通,但是使用递归数据结构会更有效率。 Using the example structure at the top, you wouldn't have to save the path at all or create the dictionary; 使用顶部的示例结构,您完全不必保存路径或创建字典。 when no children are left, simply walk up the path in a while loop using the Parent reference. 如果没有孩子,只需使用Parent引用在while循环中沿路径走即可。

Anyway, I guess that's academic because this isn't a recursive object, but I thought it was worth pointing out anyway as something to keep in mind for future designs. 无论如何,我认为这是学术性的,因为这不是递归对象,但是我认为无论如何值得指出的是将来设计时要牢记的东西。 The code above will produce exactly the results you want, I've gone ahead and tested it on a real TreeView. 上面的代码完全产生您想要的结果,我已经在真实的TreeView上进行了测试。


UPDATE 2 - So it turns out that the version above is pretty brutal with respect to memory/stack, most likely a result of creating all those IEnumerable<string> instances. 更新2-事实证明,上述版本在内存/堆栈方面相当残酷,很可能是创建所有IEnumerable<string>实例的结果。 Although it's not great design, we can remove that particular issue by changing to a mutable List<string> . 尽管这不是一个很好的设计,但是我们可以通过更改为可变的List<string>来消除该特定问题。 The following snippet shows the differences: 以下代码段显示了差异:

private void PopulateTree(IEnumerable<MyObject> items)
{
    // Snip lookup-generation code - same as before ...

    List<string> path = new List<string>();
    foreach (string parent in lookup.Keys)
    {
        if (lookup.ContainsKey(parent))
            AddToTree(lookup, path, parent);
    }
}

private void AddToTree(Dictionary<string, IEnumerable<string>> lookup,
    IEnumerable<string> path, string name)
{
    IEnumerable<string> children;
    if (lookup.TryGetValue(name, out children))
    {
        path.Add(name);
        foreach (string child in children)
            AddToTree(lookup, newPath, child);
        path.Remove(name);
    }
    // Snip "else" block - again, this part is the same as before ...
}

like rubens, I tried both, but a little better I think A Generic Tree Collection 像鲁本斯一样,我都尝试过,但是我认为通用树集合要好一点

this tree collection got some nice functionality build-in to move around the tree, go read the whole article 这个树集合具有一些不错的内置功能,可以在树上移动,请阅读整篇文章

sample with the link above 上面的链接示例

Static Class Module1
{

    public static void Main()
    {
        Common.ITree<myObj> myTree = default(Common.ITree<myObj>);
        myObj a = new myObj("a");
        myObj b = new myObj("b");
        myObj c = new myObj("c");
        myObj d = new myObj("d");

        myTree = Common.NodeTree<myObj>.NewTree;
        myTree.InsertChild(a).InsertChild(b).InsertChild(c).Parent.Parent.InsertNext(a).InsertChild(b).InsertChild(d).Parent.Parent.InsertNext(b).InsertChild(c).Parent.InsertNext(b).InsertChild(d);

        Console.WriteLine(myTree.ToStringRecursive);

        Console.ReadKey();
    }
}

Class myObj
{
    public string text;

    public myObj(string value)
    {
        text = value;
    }

    public override string ToString()
    {
        return text;
    }
}

would be exactly what you just showed 就是你刚才展示的

-A -一种
--B --B
---C - -C
-A -一种
--B --B
---D --- D
-B -B
--C - C
-B -B
--D --D

If I understand this correctly, what you're trying to do is take one tree and transform it into another. 如果我正确理解这一点,那么您想要做的就是取一棵树并将其转换为另一棵树。 The transformation essentially takes each non-leaf-node in the input tree and creates a node for it (and its descendants) in the output tree. 转换本质上采用了输入树中的每个非叶节点,并在输出树中为其创建了一个节点(及其后代)。

First off, you'll be happier if you design a data structure for your nodes that is genuinely recursive: 首先,如果为节点设计真正递归的数据结构,您会更快乐:

public class Node
{
   public Node Parent { get; private set; }
   public IEnumerable<Node> Children { get; private set; }
   public bool HasChildren { get { return Children.Count() > 0; } }

   public Node()
   {
      Children = new List<Node>();
   }
}

Your MyObject class represents parent/child relationships between string values. MyObject类表示字符串值之间的父/子关系。 As long as you're able to implement a FindChildren() method that returns the child values for a given parent value, using this class to rationalize the parent/child relationships is straightforward: 只要您能够实现一个FindChildren()方法,该方法返回给定父值的子值,就可以使用此类合理化父/子关系:

public string Value { get; set; }
public static Node Create(string parentKey)
{
   Node n = new Node();
   n.Value = parentKey;
   foreach (string childKey in FindChildren(parentKey))
   {
      Node child = n.Children.Add(Node.Create(childKey));
      child.Parent = n;
   } 
   return n;
}

It's simple to implement a property that returns a node's descendants: 实现返回节点后代的属性很简单:

public IEnumerable<Node> Descendants
{
   get
   {
      foreach (Node child in Children)
      {
         yield return child;
         foreach (Node descendant in child.Descendants)
         {
            yield return descendant;
         }
      }
   }
}

To add a Node to a TreeView, you need two methods. 要将Node添加到TreeView,您需要两种方法。 (Note that these aren't methods of the Node class!) I've made them overloads, but an argument can be made for giving them different names: (请注意,这些不是Node类的方法!)我使它们重载,但是可以为它们赋予不同的名称而使用参数:

public void AddNode(Node n, TreeView tv)
{
   TreeNode tn = tv.Nodes.Add(n.Value);
   tn.Tag = n;
   foreach (Node child in n.Children)
   {
      AddNode(child, tn);
   }
}

public void AddNode(Node n, TreeNode parent)
{
   TreeNode tn = parent.Nodes.Add(n.Value);
   parent.Tag = n;
   foreach (Node child in n.Children)
   {
      AddNode(child, tn);
   }
}

I'm setting the Tag on each TreeNode so that you can find your way back to the original Node . 我在每个TreeNode上设置Tag ,以便您可以找到返回原始Node

So to initialize your TreeView from a list of top-level parent keys, you need a method like this: 因此,要从顶级父键列表中初始化TreeView ,您需要这样的方法:

public void PopulateTreeView(IEnumerable<string> parents, TreeView t)
{
   foreach (string parentKey in parents)
   {
      Node n = Node.Create(parentKey);
      AddNode(n, t);
      foreach (Node descendant in n.Descendants)
      {
         if (n.HasChildren)
         {
            AddNode(descendant, t);
         }
      }
   }
}

Edit: 编辑:

I didn't quite understand how your MyObject class was working; 我不太了解MyObject类的工作方式; I think I do now, and I've edited this accordingly. 我想我现在就这样做了,并且已经对此进行了相应的编辑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM