Remplir WinForms TreeView à partir de DataTable
Question
J'ai un contrôle WinForm TreeView qui affiche la relation parent-enfant de CaseNotes (je sais que cela ne signifie rien pour la plupart d'entre vous mais cela m'aide à visualiser les réponses).
J'ai un DataTable des CaseNotes que je dois afficher. Le parent / enfant est défini comme suit: Si la ligne a un ParentNoteID, alors c'est un childNode de cette note, sinon c'est un rootNode. Il peut également s'agir d'une note parent (mais pas d'un rootNode) si une autre ligne a son ID en tant que ParentNoteID.
Pour compliquer (peut-être simplifier) ??les choses, j'ai le code ci-dessous qui fonctionne (principalement) et qui colore les nœuds en alternance. J'ai créé manuellement une collection statique pour l'arborescence et elle les colore assez correctement. Maintenant, je dois remplir dynamiquement les nœuds de mon DataTable.
Étant donné que je suis déjà en train de parcourir l'arborescence, nœud par nœud, ne puis-je pas pouvoir ajouter les données à ce processus? Peut-être dois-je d'abord construire les nœuds, puis les colorer en tant que routine distincte, mais la méthode de récursivité s'appliquerait toujours, n'est-ce pas?
Disons que je veux afficher CaseNoteID pour chaque nœud. Cela est retourné dans le DataTable et est 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
Mes pensées / tentatives jusqu'à présent:
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();
}
Évidemment, cela est imparfait. Il suffit simplement d’afficher ContactDate à plusieurs reprises. Certes, il affiche le nombre correct de fois mais je voudrais la valeur de ContactDate (qui est une colonne dans la base de données et est renvoyée dans le DataTable. Deuxièmement, je dois ajouter la logique ChildNode. Un if (noeud. parentNode = node.CaseNoteID) blah ...
EDIT 2
J'ai donc trouvé ce lien, here , et cela donne l’impression que j’ai besoin d’obtenir mon DataTable dans un ArrayList. Est-ce correct?
EDIT 3
D'accord, grâce à Cerebus, cela fonctionne principalement. J'ai juste une question de plus. Comment puis-je prendre cela - >
DataTable cNotesForTree = CurrentCaseNote.GetAllCNotes(Program._CurrentPerson.PersonID);
et utiliser mon DataTable retourné dans ceci? Est-ce que je viens de remplacer ceci - >
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" });
Je pense que la confusion est la suivante: dois-je toujours faire les commandes Column.Add et Row.Adds? Aussi, comment DataColumn se traduirait-il dans ma structure de données réelle? Désolé pour les questions très ignorantes, la bonne nouvelle est que je n’ai jamais à demander deux fois.
EDIT 4
Ce qui suit fournit une erreur d'exécution.
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);
}
L'erreur est la suivante - > La colonne [ea8428e4] est introuvable. Il s'agit des 8 premiers chiffres du bon identificateur de note (je dois utiliser un GUID). Devrait-il chercher une colonne de ce nom? Parce que j'utilise un Guid, y a-t-il autre chose que je doive faire? J'ai changé toutes les références dans le mien et votre code en Guid ...
La solution
Pour tenter de résoudre ce problème, j'ai créé un exemple de formulaire Windows et écrit le code suivant. J'ai envisagé la conception datatable comme suit:
NoteID NoteName ParentNoteID
"1" "One" null
"2" "Two" "1"
"3" "Three" "2"
"4" "Four" null
...
Cela devrait créer un arbre en tant que ( désolé, je ne suis pas très bon avec l'art ASCII! ):
One
|
——Two
|
————Three
|
Four
Le pseudocode ressemble à ceci:
- Parcourez toutes les lignes du datatable.
- Pour chaque ligne, créez un TreeNode et définissez ses propriétés. Répétez le processus de manière récursive pour toutes les lignes dont l'identifiant ParentNodeID correspond à celui de cette ligne.
- Chaque itération terminée retourne un noeud qui contiendra tous les noeuds correspondants avec imbrication infinie.
- Ajoutez la liste de noeuds terminée à la vue TreeView.
Le problème de votre scénario provient du fait que la "clé étrangère" fait référence à une colonne du même tableau. Cela signifie que lorsque nous parcourons les lignes, nous devons savoir quelles lignes ont déjà été analysées. Par exemple, dans le tableau ci-dessus, le nœud correspondant aux deuxième et troisième lignes est déjà ajouté lors de la première itération complète. Par conséquent, nous ne devons pas les ajouter à nouveau. Il y a deux façons de suivre cela:
- Maintenir une liste des identifiants définis (
doneNotes
). Avant d'ajouter chaque nouveau nœud, vérifiez si l'ID de la note existe dans cette liste. C'est la méthode la plus rapide et devrait normalement être préférée. ( cette méthode est commentée dans le code ci-dessous ) - Pour chaque itération, utilisez un délégué générique de prédicat (
FindNode
) pour rechercher dans la liste des noeuds ajoutés (en tenant compte des noeuds imbriqués) pour voir si le noeud à ajouter existe dans cette liste. C'est la solution la plus lente, mais j'aime un peu le code compliqué! : P
Ok, voici le code qui a fait ses preuves (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);
}
}
}
}
Autres conseils
J'ai créé une méthode d'extension beaucoup plus simple pour TreeView, impliquant l'utilisation d'une nouvelle classe d'extension simple qui ajoute deux propriétés utiles à 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;
}
Merci à la modiX pour son méthodes pour obtenir tous les nœuds (imbriqués).
vérifiez ceci:
Public Sub BuildTree(ByVal dt As DataTable, ByVal trv As TreeView, ByVal expandAll As [Boolean])
' Clear the TreeView if there are another datas in this TreeView
trv.Nodes.Clear()
Dim node As TreeNode
Dim subNode As TreeNode
For Each row As DataRow In dt.Rows
'search in the treeview if any country is already present
node = Searchnode(row.Item(0).ToString(), trv)
If node IsNot Nothing Then
'Country is already present
subNode = New TreeNode(row.Item(1).ToString())
'Add cities to country
node.Nodes.Add(subNode)
Else
node = New TreeNode(row.Item(0).ToString())
subNode = New TreeNode(row.Item(1).ToString())
'Add cities to country
node.Nodes.Add(subNode)
trv.Nodes.Add(node)
End If
Next
If expandAll Then
' Expand the TreeView
trv.ExpandAll()
End If
End Sub
Pour plus de code source complet: Comment renseigner l'arborescence depuis datatable in vb.net