アップデート
パスごとにツリー全体を繰り返す必要があるため、この問題は実際には私が最初に認識したよりもかなり複雑であることが判明しました。これ以上混乱させたくないので、古いコードを単に削除しました。
再帰データ構造を使用するとこれが簡単になることを記録しておきたいと思います。
public class MyRecursiveObject
{
public MyRecursiveObject Parent { get; set; }
public string Name { get; set; }
public List<MyRecursiveObject> Children { get; set; }
}
以下の実装コードを読むと、なぜこれが簡単なのかがはっきりとわかります。
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;
}
まず、ディクショナリにはルート ノードだけでなく中間ノードのキーも含まれることに気付きました。そのAddToTree
ため、「B」ノードをルートとして取得するために、再帰メソッドで 2 回の再帰呼び出しを行う必要はありません。メソッドの最初のウォークでPopulateTree
既に実行されています。
防御する必要があるのは、最初のウォークでリーフ ノードを追加することです。問題のデータ構造を使用して、これらは親辞書にキーがあるかどうかを確認することで検出できます。再帰的なデータ構造を使用すると、これはずっと簡単になります: をチェックするだけですParent == null
。しかし、再帰構造は私たちが持っているものではないため、上記のコードを使用する必要があります。
ほとんどがユーティリティ メソッドであるため、後でこのAddTreeNode
null チェック ロジックを繰り返し続ける必要はありません。
本当の醜さは、2 番目の再帰的なAddToTree
方法にあります。すべてのサブツリーの一意のコピーを作成しようとしているため、単純にツリー ノードを追加して、そのノードを親として再帰することはできません。ここでは、"A" には "B" という 1 つの子しかありませんが、"B" には "C" と "D" という 2 つの子があります。"A" の 2 つのコピーが必要ですが、"A" が最初にAddToTree
メソッドに渡されたときにそれを知る方法はありません。
したがって、実際に行う必要があるのは、最終段階までノードを作成せず、一時的なパスを保存することです。パスは不変IEnumerable<string>
であり、混乱することはありません。追加する子がさらにある場合、このメソッドは単純にパスに追加して再帰します。子がなくなると、保存されたパス全体をたどり、それぞれにノードを追加します。
の呼び出しごとに新しい列挙型を作成しているため、これは非常にAddToTree
非効率的です。多数のノードの場合、大量のメモリを消費する可能性があります。これは機能しますが、再帰的なデータ構造を使用するとより効率的になります。上部の構造例を使用すると、パスを保存したり、辞書を作成したりする必要はまったくありません。while
子が残っていない場合は、参照を使用してループでパスをたどってParent
ください。
とにかく、これは再帰オブジェクトではないのでアカデミックだと思いますが、将来の設計のために覚えておくべきこととして、とにかく指摘する価値があると思いました。上記のコードはまさにあなたが望む結果を生成します。実際の TreeView でテストしてみました。
更新 2 - したがって、上記のバージョンはメモリ/スタックに関してかなり残忍であることが判明しました。おそらく、これらすべてのIEnumerable<string>
インスタンスを作成した結果です。素晴らしい設計ではありませんが、可変に変更することでその特定の問題を取り除くことができList<string>
ます。次のスニペットは、違いを示しています。
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 ...
}