Erweitern des TreeView-Steuerelements

 

Duncan Mackenzie
Microsoft Developer Network

Mai 2002

Zusammenfassung: Behandelt das Hinzufügen von Datenbindungsfunktionen zum TreeView-Steuerelement als eines von einer Reihe von Beispielen zur Entwicklung von Microsoft Windows-Steuerelementen, die in Verbindung mit einem zugehörigen Übersichtsartikel gelesen werden sollen. (16 gedruckte Seiten)

Laden Sie WinFormControls.exeherunter.

Dieser Artikel ist der vierte in einer Fünf-Artikel-Reihe über die Entwicklung von Steuerelementen in Microsoft® .NET:

Inhalte

Einführung
Entwerfen einer Data-Bound Strukturansicht
Implementieren der Datenbindung
Verwenden des CurrencyManager-Objekts
Umwandeln der Datenquelle in eine Struktur
Die Beispielanwendung
Zusammenfassung

Einführung

Wenn möglich, sollten Sie mit einem vorhandenen Steuerelement beginnen. es gibt eine große Menge an Codierung und Tests, die in alle bereitgestellten Microsoft® Windows® Forms-Steuerelemente integriert wurden, und Sie verpassen all diese Arbeit, wenn Sie vollständig von Grund auf neu beginnen müssen. Vor diesem Hintergrund erbe ich in diesem Beispiel von einem vorhandenen Windows Form-Steuerelement, der Strukturansicht, und passe es an. Der Code für dieses neue TreeView-Steuerelement ist in einem einzelnen Download zusammen mit den restlichen Steuerelemententwicklungsbeispielen und einer Beispielanwendung enthalten, die zeigt, wie diese erweiterte TreeView zusammen mit anderen datengebundenen Steuerelementen verwendet werden kann.

Entwerfen einer Data-Bound Strukturansicht

Das Hinzufügen von Datenbindungen zum TreeView-Steuerelement ist ein Thema, das unter Windows-Entwicklern immer wieder aufgekommen ist, aber das Basissteuerelement unterstützt dieses Feature aufgrund eines wichtigen Unterschieds zwischen der TreeView und anderen Steuerelementen wie ListBox oder DataGrid immer noch nicht. In einer TreeView werden hierarchische Daten angezeigt. Bei einer einzelnen Tabelle mit Daten ist es relativ klar, wie Sie diese Informationen in einem ListBox - oder DataGrid-Objekt anzeigen würden, aber die Nutzung der hierarchischen Natur einer TreeView zum Anzeigen derselben Daten ist nicht so einfach. Persönlich kann ich mir viele verschiedene Möglichkeiten vorstellen, wie ich eine TreeView zum Anzeigen von Daten verwendet habe, aber eine Möglichkeit war die häufigste: Gruppieren der Daten aus einer Tabelle nach bestimmten Feldern, wie in Abbildung 1 gezeigt.

Abbildung 1. Anzeigen von Daten in einer TreeView

Daher habe ich für dieses Beispiel ein TreeView-Steuerelement erstellt, in das ich einen flachen Satz von Daten übergeben konnte, wie in Abbildung 2 dargestellt, und das Ergebnis in Abbildung 1 problemlos erzeugen konnte.

Abbildung 2. Flaches Resultset mit allen Informationen, die zum Erstellen der Struktur in Abbildung 1 erforderlich sind.

Bevor ich mit dem Programmieren begann, habe ich einen Entwurf für mein neues Steuerelement erstellt, das diesen bestimmten Datensatz und hoffentlich viele andere ähnliche Situationen verarbeiten konnte. Das Hinzufügen einer Groups-Auflistung, in der ein Gruppierungsfeld, ein Anzeigefeld und ein Wertfeld (alle oder alle, die das gleiche Feld sein können) für jede Hierarchieebene angegeben ist, sollte ausreichend generisch sein, um das Erstellen von Hierarchien aus den meisten flachen Daten zu verarbeiten. Um die in Abbildung 2 gezeigten Daten in die in Abbildung 1 gezeigte TreeView umzuwandeln, müssen Sie für mein neues Steuerelement zwei Gruppierungsebenen definieren: Verleger und Titel, wobei sie pub_id als Gruppierungsfeld für die Verlegergruppe und title_id für die Gruppe Titel definieren. Zusätzlich zum Gruppierungsfeld müssen für jede Gruppe Anzeige- und Wertfelder angegeben sein, um zu bestimmen, welcher Text auf dem entsprechenden Gruppenknoten angezeigt wird und welcher Wert verwendet wird, um eine bestimmte Gruppe eindeutig zu identifizieren. Bei diesen Daten funktioniert die Verwendung von pub_name/pub_id und title/title_id als Anzeige-/Wertfelder für diese beiden Gruppen. Die Autoreninformationen würden zu den Blattknoten der Struktur (die Knoten am Ende der Gruppierungshierarchie) werden, und Sie müssten die ID (au_id) angeben und (au_lname) Felder für diese Knoten anzeigen.

Wenn Sie ein benutzerdefiniertes Steuerelement erstellen, hilft es wirklich, am Ende der Verwendung zu arbeiten, indem sie bestimmen, wie ein Programmierer Ihr Steuerelement verwenden würde, bevor Sie mit der Programmierung beginnen. In diesem Fall wollte ich, dass ein Programmierer (angesichts der zuvor gezeigten Daten und des gewünschten Ergebnisses) in der Lage ist, die gerade beschriebene Gruppierung mit ein paar Codezeilen zu erreichen, die etwa wie folgt aussehen:

        With DbTreeControl
            .ValueMember = "au_id"
            .DisplayMember = "au_lname"

            .DataSource = myDataTable.DefaultView
            .AddGroup("Publisher", "pub_id", "pub_name", "pub_id")
            .AddGroup("Title", "title_id", "title", "title_id")
        End With

Hinweis Das ist nicht ganz das, was ich am Ende hatte, aber es ist auch nicht allzu weit entfernt. Während der Steuerungsentwicklung wurde mir klar, dass ich den Index eines Bilds aus der zugeordneten ImageList der TreeView jeder Gruppierungsebene zuordnen musste, sodass ich der AddGroup-Methode einen zusätzlichen Parameter hinzufügen musste.

Um die Struktur tatsächlich zu erstellen, werde ich die Daten durchlaufen und nach Änderungen in den Feldern suchen, die als Gruppierungswerte für die einzelnen Felder angegeben sind, wobei bei Bedarf neue Gruppenknoten erstellt und ein Blattknoten pro Datenelement erstellt wird. Die Gesamtanzahl der Knoten ist aufgrund der Gruppierungsknoten größer als die Anzahl der Elemente in der Datenquelle, aber es gibt immer einen und nur einen Blattknoten für jedes Element in den zugrunde liegenden Daten.

Abbildung 3. Gruppenknoten im Vergleich zu Blattknoten

Die Unterscheidung zwischen Blatt- und Gruppenknoten, wie in Abbildung 3 dargestellt, wird im weiteren Verlauf dieses Artikels wichtig sein, da ich beschlossen habe, diese beiden Arten von Knoten als unterschiedlich zu behandeln, soweit ich so weit ging, benutzerdefinierte Knotenklassen für jeden Typ zu erstellen und unterschiedliche Ereignisse auszulösen, je nachdem, welcher Knotentyp ausgewählt wurde.

Implementieren der Datenbindung

Der erste Schritt zum Schreiben des Codes für dieses Steuerelement besteht darin, ein Projekt und die entsprechende Startklasse zu erstellen. In meinem Fall habe ich zunächst eine neue Windows-Steuerelementbibliothek erstellt und dann die Standardklasse UserControl entfernt und durch eine neue Klasse ersetzt, die vom TreeView-Steuerelement erbt:

Public Class dbTreeControl
    Inherits System.Windows.Forms.TreeView

Ab diesem Punkt arbeite ich mit einem Steuerelement, das ich in einem Formular platzieren kann, damit es wie eine normale TreeView aussieht und sich verhält. Der nächste Schritt besteht darin, Code hinzuzufügen, um die neue Funktionalität zu behandeln, die ich in meiner Version der TreeView haben möchte, nämlich Die Datenbindung und das Gruppieren von Daten.

Hinzufügen einer DataSource-Eigenschaft

Die gesamte Funktionalität meines neuen Steuerelements ist wichtig, aber die beiden Schlüssel zum Erstellen eines komplexen datengebundenen Steuerelements behandeln die DataSource-Eigenschaft und rufen einzelne Elementeigenschaften aus jedem Objekt in der Datenquelle ab.

Erstellen der Eigenschaftsroutine

Zunächst muss jedes Steuerelement, das komplexe Datenbindungen implementiert, eine DataSource-Eigenschaftsroutine implementieren und die entsprechenden Membervariablen verwalten:

Private WithEvents cm As CurrencyManager
Private m_DataSource As Object

<Category("Data")> _
Public Property DataSource() As Object
    Get
        Return m_DataSource
    End Get
    Set(ByVal Value As Object)
        If Value Is Nothing Then
            cm = Nothing
            GroupingChanged()
        Else
            If Not (TypeOf Value Is IList Or _
                      TypeOf Value Is IListSource) Then
                'not a valid data source for this purpose
                Throw New System.Exception("Invalid DataSource")
            Else
                If TypeOf Value Is IListSource Then
                    Dim myListSource As IListSource
                    myListSource = CType(Value, IListSource)
                    If myListSource.ContainsListCollection = True Then
                        Throw New System.Exception("Invalid DataSource")
                    Else
                        'ok, ok.. it is a valid data source
                        m_DataSource = Value
                        cm = CType(Me.BindingContext(Value), _
                            CurrencyManager)
                        GroupingChanged()
                    End If
                Else
                    m_DataSource = Value
                    cm = CType(Me.BindingContext(Value), _
                        CurrencyManager)
                    GroupingChanged()
                End If
            End If
        End If
    End Set
End Property

Die IList-Schnittstelle

Objekte, die als Datenquelle für komplexe Datenbindungen verwendet werden können, unterstützen in der Regel die IList-Schnittstelle, die die Daten als Sammlung von Objekten verfügbar macht und mehrere nützliche Eigenschaften bereitstellt, z. B. Count. Mein neues TreeView-Steuerelement erfordert ein Objekt, das IList für seine Bindung unterstützt, aber eine andere Schnittstelle, die IListSource-Schnittstelle, ist ebenfalls in Ordnung, da es eine einfache Methode (GetList) zum Abrufen eines IList-Objekts bereitstellt. Wenn die DataSource-Eigenschaft festgelegt ist, versuche ich zuerst zu ermitteln, ob ein gültiges Objekt angegeben wurde – eines, das IList oder IListSource unterstützt. Was ich wirklich möchte, ist IList. Wenn das angegebene Objekt also nur IListSource (wie eine DataTable) unterstützt, verwende ich die GetList() -Methode dieser Schnittstelle, um das richtige Objekt abzurufen.

Einige Objekte, die IListSource implementieren (z . B. DataSet), enthalten tatsächlich mehrere Listen, wie in der ContainsListCollection-Eigenschaft angegeben. Wenn diese Eigenschaft True ist, gibt GetList ein IList-Objekt zurück, das eine Liste von Listen darstellt. In meinem Beispiel habe ich mich entschieden, Verbindungen direkt mit IList-Objekten oder mit IListSource-Objekten zu unterstützen, die nur ein einzelnes IList-Objekt enthalten, und Ignorierte Objekte wie das DataSet , für die zusätzliche Arbeit zum Angeben einer Datenquelle erforderlich wäre.

Hinweis Wenn Sie diesen Objekttyp (ein DataSet oder ähnliches) unterstützen möchten, können Sie eine zweite Eigenschaft (z. B . DataMember) hinzufügen, um anzugeben, welche bestimmte Unterliste für die Bindung verwendet werden soll.

Wenn eine gültige Datenquelle bereitgestellt wird, ist das Endergebnis die Erstellung eines instance der CurrencyManager-Klasse (cm = Me.BindingContext(Value)). Diese instance wird in einer lokalen Variablen gespeichert, da sie für den Zugriff auf die zugrunde liegende Datenquelle, objekteigenschaften und Positionsinformationen verwendet wird.

Hinzufügen von Anzeige- und Wertelementeigenschaften

Eine DataSource ist der erste Schritt bei der komplexen Datenbindung, aber das Steuerelement muss wissen, welche bestimmten Felder oder Eigenschaften der Daten als Anzeige- und Wertmember verwendet werden sollen. Das Display-Element wird als Untertitel der Strukturknoten verwendet, während auf das Value-Element über die Value-Eigenschaft des Knotens zugegriffen werden kann. Diese Eigenschaften sind nur Zeichenfolgen, die die Feld- oder Eigenschaftsnamen darstellen, und können dem Steuerelement einfach hinzugefügt werden:

    Private m_ValueMember As String
    Private m_DisplayMember As String

    <Category("Data")> _
    Public Property ValueMember() As String
        Get
            Return m_ValueMember
        End Get
        Set(ByVal Value As String)
            m_ValueMember = Value
        End Set
    End Property

    <Category("Data")> _
    Public Property DisplayMember() As String
        Get
            Return m_DisplayMember
        End Get
        Set(ByVal Value As String)
            m_DisplayMember = Value
        End Set
    End Property

In dieser TreeView stellen diese Eigenschaften nur die Elemente Display und Value für die Blattknoten dar, und die entsprechenden Informationen für jede Gruppierungsebene werden in der AddGroup-Methode angegeben.

Verwenden des CurrencyManager-Objekts

In der weiter oben erläuterten DataSource-Eigenschaft wurde ein instance der CurrencyManager-Klasse erstellt und in einer Variablen auf Klassenebene gespeichert. Die CurrencyManager-Klasse , auf die über dieses Objekt zugegriffen wird, ist ein wichtiger Bestandteil der Implementierung der Datenbindung, da sie Über Eigenschaften, Methoden und Ereignisse verfügt, die Folgendes ermöglichen:

  • Zugreifen auf das zugrunde liegende IList-Objekt für Ihre Datenquelle
  • Abrufen und Festlegen von Feldern oder Eigenschaften für ein Objekt in Ihrer Datenquelle und
  • Synchronisieren Sie Ihr Steuerelement mit anderen datengebundenen Steuerelementen im gleichen Formular.

Abrufen von Eigenschafts-/Feldwerten

Mit dem CurrencyManager-Objekt können Sie Eigenschaften- oder Feldwerte aus einzelnen Elementen in der Datenquelle abrufen, z. B. die Werte der Felder DisplayMember oder ValueMember , über die GetItemProperties-Methode . PropertyDescriptor-Objekte werden dann verwendet, um den Wert eines bestimmten Felds oder einer bestimmten Eigenschaft für ein bestimmtes Listenelement abzurufen. Der folgende Codeausschnitt zeigt, wie diese PropertyDescriptor-Objekte erstellt werden und wie die GetValue-Funktion dann verwendet werden kann, um einen Eigenschaftswert aus einem der Elemente in der zugrunde liegenden Datenquelle abzurufen. Beachten Sie die List-Eigenschaft des CurrencyManager-Objekts: Sie bietet Zugriff auf die IList-instance, an die das Steuerelement gebunden wurde:

Dim myNewLeafNode As TreeLeafNode
Dim currObject As Object
currObject = cm.List(currentListIndex)
If Me.DisplayMember <> "" AndAlso Me.ValueMember <> "" Then
    'add leaf node?
    Dim pdValue As System.ComponentModel.PropertyDescriptor
    Dim pdDisplay As System.ComponentModel.PropertyDescriptor
    pdValue = cm.GetItemProperties()(Me.ValueMember)
    pdDisplay = cm.GetItemProperties()(Me.DisplayMember)
    myNewLeafNode = _
    New TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _
          currObject, _
          pdValue.GetValue(currObject), _
          currentListIndex)

GetValue gibt unabhängig vom zugrunde liegenden Datentyp der Eigenschaft ein Objekt zurück. Daher müssen Sie den Rückgabewert vor der Verwendung konvertieren.

Synchronisieren Data-Bound Steuerelemente

Der CurrencyManager verfügt über ein weiteres wichtiges Feature: Neben dem Zugriff auf die gebundene Datenquelle und auf Elementeigenschaften ermöglicht es die Koordination der Datenbindung zwischen diesem Steuerelement und allen anderen Steuerelementen, die dieselbe Datenquelle verwenden. Diese Unterstützung kann verwendet werden, um sicherzustellen, dass mehrere Steuerelemente, die alle an dieselbe Datenquelle gebunden sind, auf demselben Element in Ihrer Datenquelle verbleiben. Für mein Steuerelement möchte ich sicherstellen, dass beim Auswählen eines Elements in der Struktur alle anderen Steuerelemente, die an dieselbe Datenquelle gebunden sind, auf dasselbe Element zeigen (derselbe Datensatz, dieselbe Zeile oder sogar ein Tupel, wenn Sie lieber in Datenbankbegriffen denken). Damit dies funktioniert, überschreiben Sie die OnAfterSelect-Methode aus der zugrunde liegenden TreeView. In dieser Methode, die aufgerufen wird, nachdem ein Tree-Knoten ausgewählt wurde, setz ich die Position-Eigenschaft des CurrencyManager-Objekts auf den Index des aktuell ausgewählten Elements. Die Beispielanwendung, die zusammen mit diesem TreeView-Steuerelement enthalten ist, veranschaulicht, wie dieses Phänomen synchronisierter Steuerelemente das Erstellen datengebundener Benutzeroberflächen erleichtert. Um die Listenposition des aktuell ausgewählten Elements zu vereinfachen, verwende ich benutzerdefinierte TreeNode-Klassen (TreeLeafNode oder TreeGroupNode) und speichere den Listenindex jedes Knotens in einer Position-Eigenschaft , während ich sie erstelle:

Protected Overrides Sub OnAfterSelect _ 
(ByVal e As System.Windows.Forms.TreeViewEventArgs)
    Dim tln As TreeLeafNode
    If TypeOf e.Node Is TreeGroupNode Then
        tln = FindFirstLeafNode(e.Node)
        Dim groupArgs As New groupTreeViewEventArgs(e)
        RaiseEvent AfterGroupSelect(groupArgs)
    ElseIf TypeOf e.Node Is TreeLeafNode Then
        Dim leafArgs As New leafTreeViewEventArgs(e)
        RaiseEvent AfterLeafSelect(leafArgs)
        tln = CType(e.Node, TreeLeafNode)
    End If

    If Not tln Is Nothing Then
        If cm.Position <> tln.Position Then
            cm.Position = tln.Position
        End If
    End If
    MyBase.OnAfterSelect(e)
End Sub

Möglicherweise haben Sie eine Funktion namens FindFirstLeafNode bemerkt, die im vorherigen Codeausschnitt verwendet wurde, daher dachte ich, ich sollte es ein wenig erklären. In meiner TreeView entsprechen nur die Blattknoten (der letzte Knoten in der Hierarchie) Elementen in der DataSource, und jeder andere Knoten ist nur vorhanden, um die Gruppenstruktur zu erstellen. Wenn ich jedoch ein gut verhaltenes datengebundenes Steuerelement möchte, sollte immer ein Element ausgewählt sein, das der Datenquelle entspricht. Wenn also ein Gruppenknoten ausgewählt ist, finde ich den ersten Blattknoten unter dieser Gruppe und handle so, als sei der Blattknoten die aktuelle Auswahl. Überprüfen Sie das Beispiel, um dies in Aktion zu sehen, aber vorerst müssen Sie mir nur vertrauen, dass dies tatsächlich funktioniert:

Private Function FindFirstLeafNode(ByVal currNode As TreeNode) _
        As TreeLeafNode
    If TypeOf currNode Is TreeLeafNode Then
        Return CType(currNode, TreeLeafNode)
    Else
        If currNode.Nodes.Count > 0 Then
            Return FindFirstLeafNode(currNode.Nodes(0))
        Else
            Return Nothing
        End If
    End If
End Function

Durch Festlegen der Position-Eigenschaft des CurrencyManager-Objekts werden andere Steuerelemente mit dem aktuell ausgewählten Element synchronisiert, aber der CurrencyManager löst auch Ereignisse aus, wenn ein anderes Steuerelement die Position ändert, sodass ich mein ausgewähltes Element entsprechend ändern kann. Um eine kleine datengebundene Komponente zu sein, sollte die Auswahl verschoben werden, wenn sich die Position der Datenquelle ändert, und wenn die Daten, wenn ein Element geändert wird, sollte die Anzeige aktualisiert werden. Es gibt drei Ereignisse, die vom CurrencyManager ausgelöst werden: CurrentChanged, ItemChanged und PositionChanged. Das letzte Ereignis ist ziemlich einfach; Einer der Zwecke von CurrencyManager besteht darin, einen aktuellen Positionsindikator für Ihre Datenquelle zu verwalten, sodass mehrere gebundene Steuerelemente denselben Datensatz oder Listenelement anzeigen können, und dieses Ereignis wird ausgelöst, wenn sich diese Position ändert. Die anderen Ereignisse überschneiden sich in einigen Fällen, sodass sie nicht so klar sind. Hier ist eine Aufschlüsselung, wie Sie sie in Ihrem benutzerdefinierten Steuerelement verwenden sollten: PositionChanged ist das einfache Ereignis, daher werde ich das sofort aus dem Weg bringen. verwenden Sie es, wenn Sie das aktuell ausgewählte Element in einem komplexen datengebundenen Steuerelement wie unserer Struktur anpassen möchten. Jedes Mal, wenn ein Element in der Datenquelle geändert wird, wird das ItemChanged-Ereignis ausgelöst, während CurrentChanged nur ausgelöst wird, wenn das aktuelle Element geändert wird.

In meiner TreeView habe ich festgestellt, dass alle drei Ereignisse auftreten würden, wenn ich ein neues Element ausgewählt habe. Daher entschloss ich mich, das PositionChanged-Ereignis zu behandeln, indem ich mein aktuell ausgewähltes Element änderte, und die anderen beiden Ereignisse überhaupt nicht zu behandeln. Die .NET Framework-Dokumentation empfiehlt, dass ich meine Datenquelle in IBindingList umwandeln (wenn meine Datenquelle IBindingList unterstützt) und stattdessen das ListChanged-Ereignis verwenden, aber ich habe diese Funktionalität nicht implementiert:

Private Sub cm_PositionChanged(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles cm.PositionChanged
    Dim tln As TreeLeafNode
    If TypeOf Me.SelectedNode Is TreeLeafNode Then
        tln = CType(Me.SelectedNode, TreeLeafNode)
    Else
        tln = FindFirstLeafNode(Me.SelectedNode)
    End If

    If tln.Position <> cm.Position Then
        Me.SelectedNode = FindNodeByPosition(cm.Position)
    End If
End Sub

Private Overloads Function FindNodeByPosition(ByVal index As Integer) _
        As TreeNode
    Return FindNodeByPosition(index, Me.Nodes)
End Function

Private Overloads Function FindNodeByPosition(ByVal index As Integer, _
    ByVal NodesToSearch As TreeNodeCollection) As TreeNode
    Dim i As Integer = 0
    Dim currNode As TreeNode
    Dim tln As TreeLeafNode

    Do While i < NodesToSearch.Count
        currNode = NodesToSearch(i)
        i += 1
        If TypeOf currNode Is TreeLeafNode Then
            tln = CType(currNode, TreeLeafNode)
            If tln.Position = index Then
                Return currNode
            End If
        Else
            currNode = FindNodeByPosition(index, currNode.Nodes)
            If Not currNode Is Nothing Then
                Return currNode
            End If
        End If
    Loop
    Return Nothing
End Function

Umwandeln der Datenquelle in eine Struktur

Nachdem der Datenbindungscode abgeschlossen ist, kann ich daran arbeiten, den rest des Codes hinzuzufügen, um die Gruppierungsebenen zu verwalten, die Struktur entsprechend zu erstellen und einige benutzerdefinierte Ereignisse, Methoden und Eigenschaften hinzuzufügen.

Verwalten der Gruppen

AddGroup-, RemoveGroup- und ClearGroups-Funktionen mussten erstellt werden, damit der Programmierer die Sammlung von Gruppen konfigurieren kann. In jedem Fall, in dem die Gruppenauflistung geändert wird, muss die Struktur neu gezeichnet werden (um die neue Konfiguration widerzuspiegeln). Daher habe ich eine allgemeine Prozedur namens GroupingChanged erstellt, die von verschiedenen Code im gesamten Steuerelement aufgerufen wird, wenn sich etwas ändert, das eine Neuerstellung der Struktur erzwingen soll:

Private treeGroups As New ArrayList()

Public Sub RemoveGroup(ByVal group As Group)
    If Not treeGroups.Contains(group) Then
        treeGroups.Remove(group)
        GroupingChanged()
    End If
End Sub

Public Overloads Sub AddGroup(ByVal group As Group)
    Try
        treeGroups.Add(group)
        GroupingChanged()
    Catch
    End Try
End Sub

Public Overloads Sub AddGroup(ByVal name As String, _
        ByVal groupBy As String, _
        ByVal displayMember As String, _
        ByVal valueMember As String, _
        ByVal imageIndex As Integer, _
        ByVal selectedImageIndex As Integer)
    Dim myNewGroup As New Group(name, groupBy, _
        displayMember, valueMember, _
        imageIndex, selectedImageIndex)
    Me.AddGroup(myNewGroup)
End Sub


Public Function GetGroups() As Group()
    Return CType(treeGroups.ToArray(GetType(Group)), Group())
End Function

Erstellen der Struktur

Die eigentliche Neuerstellung der Struktur wird dann von einem Prozedurpaar behandelt: BuildTree und AddNodes. Der Code für diese beiden Prozeduren ist ziemlich lang, daher werde ich versuchen, ihr Verhalten zusammenzufassen, anstatt die gesamte Auflistung in den Artikel einzufassen (natürlich ist es alles im Download für Sie). Wie bereits erwähnt, würde der Programmierer mit diesem Steuerelement interagieren, indem er eine Reihe von Gruppen einrichtet, und in BuildTree werden diese Gruppen dann verwendet, um zu bestimmen, wie die Knoten der Struktur eingerichtet werden. BuildTree löscht die aktuelle Knotenauflistung und durchläuft dann die gesamte Datenquelle, um die erste Gruppierungsebene (Publisher in den Beispielen und Abbildungen weiter oben in diesem Artikel) zu verarbeiten, wobei für jeden eindeutigen Gruppierungswert ein Knoten (ein Knoten für jeden pub_id Wert unter Verwendung meiner Beispieldaten) hinzugefügt wird. Anschließend wird AddNodes aufgerufen, um alle Knoten unterhalb dieser ersten Gruppierungsebene auszufüllen. AddNodes ruft sich rekursiv auf, um eine beliebige Anzahl von Ebenen zu verarbeiten, und fügt nach Bedarf Gruppenknoten und Blattknoten hinzu. Zwei benutzerdefinierte Klassen, die auf TreeNode basieren, werden verwendet, damit Gruppenknoten und Blattknoten unterschieden werden können, und um jeden Knotentyp mit einem eigenen Satz relevanter Eigenschaften bereitzustellen.

Anpassen der TreeView-Ereignisse

Wenn ein Knoten ausgewählt wird, löst treeView zwei Ereignisse aus: BeforeSelect und AfterSelect. Für mein Steuerelement wollte ich jedoch unterschiedliche Ereignisse für Gruppenknoten und Blattknoten haben, daher habe ich meine eigenen Ereignisse hinzugefügt, BeforeGroupSelect/AfterGroupSelect und BeforeLeafSelect/AfterLeafSelect, mit benutzerdefinierten Ereignisargumentklassen, die ich zusätzlich zu den Basisereignissen auslöse:

Public Event BeforeGroupSelect _
    (ByVal sender As Object, ByVal e As groupTreeViewCancelEventArgs)
Public Event AfterGroupSelect _
    (ByVal sender As Object, ByVal e As groupTreeViewEventArgs)
Public Event BeforeLeafSelect _
    (ByVal sender As Object, ByVal e As leafTreeViewCancelEventArgs)
Public Event AfterLeafSelect _
    (ByVal sender As Object, ByVal e As leafTreeViewEventArgs)

Protected Overrides Sub OnBeforeSelect _
    (ByVal e As System.Windows.Forms.TreeViewCancelEventArgs)
    If TypeOf e.Node Is TreeGroupNode Then
        Dim groupArgs As New groupTreeViewCancelEventArgs(e)
        RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs)
    ElseIf TypeOf e.Node Is TreeLeafNode Then
        Dim leafArgs As New leafTreeViewCancelEventArgs(e)
        RaiseEvent BeforeLeafSelect(CObj(Me), leafArgs)
    End If
    MyBase.OnBeforeSelect(e)
End Sub

Protected Overrides Sub OnAfterSelect _
    (ByVal e As System.Windows.Forms.TreeViewEventArgs)
    Dim tln As TreeLeafNode
    If TypeOf e.Node Is TreeGroupNode Then
        tln = FindFirstLeafNode(e.Node)
        Dim groupArgs As New groupTreeViewEventArgs(e)
        RaiseEvent AfterGroupSelect(CObj(Me), groupArgs)
    ElseIf TypeOf e.Node Is TreeLeafNode Then
        Dim leafArgs As New leafTreeViewEventArgs(e)
        RaiseEvent AfterLeafSelect(CObj(Me), leafArgs)
        tln = CType(e.Node, TreeLeafNode)
    End If

    If Not tln Is Nothing Then
        If cm.Position <> tln.Position Then
            cm.Position = tln.Position
        End If
    End If
    MyBase.OnAfterSelect(e)
End Sub

Die benutzerdefinierten Knotenklassen (TreeLeafNode und TreeGroupNode) und die benutzerdefinierten Ereignisargumentklassen sind alle im herunterladbaren Code enthalten.

Die Beispielanwendung

Um den gesamten Code in diesem Beispielsteuerelement vollständig zu verstehen, müssen Sie sehen, wie er in einer Anwendung funktionieren würde. Die enthaltene Beispielanwendung funktioniert mit der Zugriffsdatenbank pubs.mdb und zeigt, wie das Tree-Steuerelement mit anderen datengebundenen Steuerelementen zusammenarbeiten kann, um Windows-Anwendungen zu erstellen. Zu den wichtigsten Features dieses Beispiels, auf die Sie besonders achten sollten, gehören die Synchronisierung der Struktur mit den anderen gebundenen Steuerelementen und die automatische Auswahl eines Strukturknotens, wenn eine Suche für die Datenquelle ausgeführt wird.

Hinweis Diese Beispielanwendung (mit dem Namen "TheSample") ist im Download für diesen Artikel enthalten.

Abbildung 4. Demoanwendung für datengebundene TreeView

Zusammenfassung

Das in diesem Artikel gezeigte datengebundene Tree-Steuerelement ist nicht die Antwort für jedes Projekt, das ein Tree-Steuerelement zum Anzeigen von Informationen aus einer Datenbank benötigt, aber es veranschaulicht eine Möglichkeit, wie Sie dieses Steuerelement für Ihre eigenen Zwecke anpassen können. Beachten Sie, dass der Großteil des Codes in diesem Steuerelement in jedem komplexen datengebundenen Steuerelement, das Sie erstellen möchten, im Wesentlichen gleich wäre und dass Sie diese vorhandene Arbeit nutzen können, um die Entwicklung zukünftiger Steuerungen zu vereinfachen.

Im nächsten Beispiel, Zeichnen eigener Steuerelemente mit GDI+, sehen Sie eine viel einfachere Möglichkeit zum Implementieren der Datenbindung für Situationen, in denen Sie keine bestimmte Basisklasse verwenden müssen, wie in diesem Steuerelement, um vom TreeView-Steuerelement zu erben.