Проектування пов’язаного з даними Tree View, Windows Forms, ASP, статті

Тема додавання прив’язки даних до елементу управління TreeView періодично зачіпалася розробниками Windows, проте базовий елемент управління, як і раніше не підтримує цю можливість через ключового відмінності між TreeView та іншими елементами керування, такими як ListBox або DataGrid: TreeView відображає ієрархічні дані. Окрему таблицю даних досить нескладно відобразити в ListBox або DataGrid, проте використовувати переваги ієрархічного характеру TreeView для відображення тих же даних не так просто. Існує багато різних способів використання TreeView для відображення даних, але один спосіб є найбільш загальним: угруповання даних з таблиці по певним полям, зображена на рис. 1.

Рис. 1. Відображення даних в TreeView

Для цього прикладу передбачалося створити елемент управління TreeView, в який можна передати плоский набір даних (див. рис. 2) і легко досягти результату, зображеного на рис. 1.

Рис. 2. Плоский набір результатів, що містить всю інформацію, необхідну для створення дерева, зображеного на рис. 1.

Перед початком кодування був розроблений дизайн нового елемента управління, при якому можна обробляти цей специфічний набір даних і, можливо, багато інших подібних ситуацій. Додавання колекції “Групи”, в якій поле угруповання, поле відображення і поле значення (Будь-які з них можуть бути тим же полем) визначені для кожного рівня ієрархії, має бути достатньо універсальним для створення ієрархій з більшості плоских даних. Щоб перетворити дані (див. рис. 2) в TreeView (див. рис. 1), для нового елемента керування потрібно вказати два рівні угруповання – Publisher (видавець) і Title (назва), визначаючи pub_id як поле угруповання для групи Publisher і title_id – для групи Title. Крім поля угруповання для кожної групи також необхідно вказати поле відображення і поле значення, щоб визначити текст, що показується на відповідному сайті групи, і значення, використовуване для однозначної ідентифікації певної групи. У випадку з цими даними pub_name / pub_id і title / title_id будуть використовуватися як поля відображення / значення для цих двох груп. Ім’я автора буде кінцевим вузлом дерева (вузлом в кінці ієрархії угруповання), і для цих вузлів також необхідно визначити поля ідентифікатора (au_id) і відображення (au_lname).

При побудові користувача елементу управління перед початком кодування рекомендується чітко уявити собі, для якої мети цей елемент управління буде використовуватися іншим розробником. В даному випадку розробник (дані та потрібний результат описані вище) зможе виконати щойно описану угруповання за допомогою кількох рядків наступного коду:

        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

Примітка. Цей код – не зовсім те, що було потрібно, але він не дуже далекий від бажаного результату. При розробці елемента управління стало очевидно, що необхідно пов’язати індекс зображення із зв’язаного ImageList елемента управління TreeView з кожним рівнем угруповання. Тому був доданий додатковий параметр для методу AddGroup.

Щоб фактично побудувати дерево, переглянемо дані і знайдемо зміни в полях, зазначених як групують значення для кожного поля, створимо нові вузли групи по мірі необхідності та по одному кінцевому вузлу для кожного елемента даних. Загальне число вузлів буде більше числа елементів у джерелі даних через вузлів угруповання, але для кожного елемента базових даних завжди буде тільки один кінцевий вузол.

Примітки до малюнка:

Group Nodes – вузли груп
Leaf Nodes – кінцеві вузли

Рис. 3. Вузли груп і кінцеві вузли

Різниця між кінцевим вузлом і вузлом групи (див. рис. 3) дуже важливо для цієї статті, оскільки було вирішено обробляти ці два типи вузлів по-різному шляхом створення користувацьких класів для кожного типу і генерації різних подій в залежності від обраного типу вузла.

Реалізація зв’язування даних

Першим кроком у написанні коду для цього елемента керування потрібно створити проект і відповідний стартовий клас. В даному випадку спочатку створимо новий проект Windows Control Library і потім, видаливши клас UserControl за замовчуванням, замінимо його новим класом, який успадковується з елемента управління TreeView:

Public Class dbTreeControl
    Inherits System.Windows.Forms.TreeView

З цього моменту робота ведеться з елементом управління, який можна помістити в форму і зробити так, щоб він виглядав і працював точно так само, як і звичайний TreeView. Наступним кроком необхідно додати код, обробляє нові функціональні можливості в даній версії TreeView – Зв’язування і угрупування даних.

Додавання властивості DataSource

Усі функціональні можливості нового елемента керування важливі, але двома ключовими особливостями побудови складного пов’язаного з даними елемента управління є обробка властивості DataSource та отримання властивостей окремого елемента з кожного об’єкта в джерелі даних.

Створення процедури властивості

Для початку роботи необхідно, щоб будь-який елемент управління, реалізує складне зв’язування даних, реалізував процедуру DataSource і підтримував відповідні змінні члени:

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 "Використовується джерело даних неприпустимий.
                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 "А тепер джерело даних допустимо.
                        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

Інтерфейс IList

Об’єкти, які можуть використовуватися в якості джерела даних для складного зв’язування даних, як правило, підтримують інтерфейс IList, що надає дані у вигляді колекції об’єктів, а також кілька корисних властивостей, наприклад, Count. Новому елементу управління TreeView потрібно об’єкт, який підтримує IList для його зв’язування, однак інший інтерфейс IListSource також цілком підходить, оскільки він надає простий метод (GetList) для отримання об’єкта IList. Коли властивість DataSource встановлено, спочатку рекомендується визначити, чи був Чи наданий допустимий об’єкт, тобто який підтримує IList або IListSource. Рекомендується вибрати IList, оскільки якщо наданий об’єкт підтримує лише IListSource (наприклад, DataTable), то для отримання допустимого об’єкта використовується метод GetList () цього інтерфейсу.

Деякі об’єкти, що реалізують IListSource (наприклад, DataSet), фактично містять кілька списків, що позначено властивістю ContainsListCollection. Якщо ця властивість має значення True, то GetList поверне об’єкт IList, що представляє список декількох підсписків. У розглянутому прикладі було вирішено підтримувати зв’язки безпосередньо з об’єктами IList або IListSource, що зберігають тільки один об’єкт IList і ігнорують такі об’єкти, як DataSet, для яких необхідно додатково визначити джерело даних.

Примітка. Якщо потрібна підтримка даного типу об’єктів (DataSet або йому подібних), можна додати друга властивість (наприклад, DataMember), що визначає використовуваний для зв’язування конкретний подспісок.

Якщо наданий допустимий джерело даних, то в кінцевому результаті буде створений екземпляр класу CurrencyManager (cm = Me.BindingContext (Value)). Цей примірник зберігається в локальну змінну, так як він буде використовуватися для звернення до базового джерела даних, властивостям об’єкта та інформації про позиції.

Додавання властивостей Display і Value

Наявність DataSource – це перший крок у складному зв’язуванні даних, проте елементу управління необхідно знати, які конкретні поля або властивості даних повинні використовуватися для значення і відображення. Член Display (відображення) використовуватиметься як заголовок вузлів дерева, член Value (значення) буде доступний через властивість Value вузла. Ці властивості є лише рядками, що представляють назви поля чи властивості, і легко додаються до елементу управління:

    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

В даному TreeView ці властивості представляють членів Display і Value тільки для кінцевих вузлів, відповідна інформація для кожного рівня угруповання визначається в методі AddGroup.

Використання об’єкта CurrencyManager

У розглянутому раніше властивості DataSource екземпляр класу CurrencyManager був створений і збережений в змінну рівня класу. Клас CurrencyManager, до якого звертаються через цей об’єкт, є ключовим у реалізації зв’язування даних, оскільки він володіє властивостями, методами і подіями, що дозволяють:

Отримання значень Property / Field

Об’єкт CurrencyManager дозволяє отримати значення властивості або поля окремих елементів у джерелі даних, наприклад, значення полів DisplayMember або ValueMember, через свій метод GetItemProperties. Потім використовуються об’єкти PropertyDescriptor, щоб отримати значення конкретного поля або властивості певного елемента списку. Наведений нижче фрагмент коду показує, як створюються ці об’єкти PropertyDescriptor, і як надалі може використовуватися функція GetValue, щоб отримати значення властивості одного з елементів у базовому джерелі даних. Зверніть увагу на властивість List об’єкта CurrencyManager: він надає доступ до примірника IList, з яким був пов’язаний елемент управління:

Dim myNewLeafNode As TreeLeafNode
Dim currObject As Object
currObject = cm.List(currentListIndex)
If Me.DisplayMember <> "" AndAlso Me.ValueMember <> "" Then "Додамо вершину?
    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 повертає об’єкт незалежно від типу базових даних властивості, тому перед використанням повертається значення необхідно конвертувати.

Підтримка синхронізації пов’язаних з даними елементів керування

CurrencyManager має ще одну важливу особливість: крім надання доступу до пов’язаного джерела даних і до властивостей елемента, він здійснює координацію зв’язування даних між цим елементом управління і будь-якими іншими елементами керування, використовують той же DataSource. Ця підтримка забезпечує те, що кілька елементів управління, пов’язаних з одним джерелом даних, залишаються на тому ж самому елементі в джерелі даних. Для розглянутого елемента управління необхідно зробити так, щоб при виборі елемента в дереві будь-які інші елементи управління, пов’язані з тим же джерелом даних, вказували на один і той же елемент (запис, рядок або кортеж, використовуючи термінологію баз даних). Для цього необхідно перевизначити метод OnAfterSelect базового елементу управління TreeView. У цьому методі, що викликається після вибору вузла дерева, встановимо властивість Position об’єкта CurrencyManager на індекс поточного вибраного елемента. Типове додаток, що надається разом з елементом управління TreeView, ілюструє, як явище синхронізованих елементів управління полегшує побудова пов’язаних з даними користувача інтерфейсів. Щоб полегшити визначення позиції поточного вибраного елемента у списку, використовуйте створені користувальницькі класи TreeNode (TreeLeafNode або TreeGroupNode) і збережіть індекс списку кожного вузла у властивість Position:

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

У попередньому фрагменті коду використовується функція FindFirstLeafNode, яку необхідно трохи роз’яснити. У даному TreeView тільки кінцеві вузли ієрархії відповідають елементам в DataSource, інші вузли служать тільки для створення структури групи. Щоб побудувати хороший пов’язаний з даними елемент управління, в ньому завжди повинен бути вибраний елемент, відповідний DataSource, оскільки при виборі вузла групи в ній завжди буде знаходиться перший кінцевий вузол, який розглядається як поточний вибір. Як це працює, можна побачити на цьому прикладі, однак поки перевірити неможливо, що це дійсно працює, тому будемо довіряти автору:

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

Установка властивості Position об’єкта CurrencyManager дозволяє зберегти синхронізацію інших елементів управління з поточним вибраним елементом, але CurrencyManager також генерує події, коли інші елементи керування змінюють позицію так, щоб можна було змінити вибраний елемент відповідним чином. Щоб побудувати невеликий хороший пов’язаний з даними компонент, вибір повинен переміщатися при зміну позиції джерела даних, і якщо дані елемента змінилися, відображення має оновитися. CurrencyManager генерує три події: CurrentChanged, ItemChanged і PositionChanged. Остання подія досить пряме; однією з цілей CurrencyManager є управління індикатором поточної позиції для джерела даних так, щоб кілька пов’язаних елементів управління відображали ту ж запис або елемент списку, і ця подія буде генеруватися при будь-якій зміні цієї позиції. В деяких випадках інші події накладаються один на одного, тому вони не дуже зрозумілі. Тут представлена ​​розгортка їх використання в своєму користувацькому елементі управління: PositionChanged – це просте подія, тому розглянемо його відразу; використовуйте його, коли необхідно скорегувати поточний вибраний елемент в складному пов’язаному з даними елементі управління, такому як дане дерево. Подія ItemChanged генерується при зміні будь-якого елементу в джерелі даних, а CurrentChanged – тільки при зміні поточного елемента.

У даному TreeView було виявлено, що всі три події генеруються при виборі нового елемента, тому було вирішено обробляти подія PositionChanged при зміні поточного вибраного елемента, а два інших події не обробляти взагалі. В Документації по. Net Framework рекомендується перетворити джерело даних в IBindingList (якщо він підтримує IBindingList) і замість цього використовувати його подія ListChanged, проте ці функціональні можливості не були реалізовані:

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

Перетворення DataSource в дерево

Закінчивши з кодом зв’язування даних, можна приступити до роботи над додаванням решти коду, що дозволяє управляти рівнями угруповання, побудувати дерево відповідним чином і додати кілька користувальницьких подій, методів і властивостей.

Управління групами

Необхідно створити функції AddGroup, RemoveGroup і ClearGroups, щоб сконфигурировать колекцію груп. При кожній зміні колекції груп дерево має перемальовувати (щоб відобразити нову конфігурацію), тому була створена загальна процедура GroupingChanged, що викликається різним кодом всюди в елементі управління при будь-якій зміні і оновлююча дерево: P

rivate 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

Побудова дерева

Фактичне оновлення дерева обробляється двома процедурами: BuildTree і AddNodes. Їх код достатньо довгий, тому спробуємо зробити огляд їх поведінки і не включати весь код в статтю (звичайно, його можна завантажити). Як згадувалося раніше, розробник взаємодіє з цим елементом управління шляхом установки ряду груп, які потім використовуються в BuildTree, щоб встановити вузли дерева. BuildTree очищає поточну колекцію вузла, проходить цикл через весь джерело даних до процесів першого рівня групування (див. Publisher в прикладах і малюнки в цій статті вище), додаючи по одному вузлу для кожного різного значення угруповання (один вузол для кожного значення pub_id в даному прикладі), і потім викликає AddNodes, щоб заповнити всі вузли нижче першого рівня угруповання. AddNodes викликає себе рекурсивно, щоб обробити будь-яке число рівнів, що додаються у вузли групи і кінцеві вузли відповідно. Два користувальницьких класу, засновані на TreeNode, використовуються для розрізнення вузлів групи і кінцевих вузлів і надання кожному типу вузла свого набору релевантних властивостей.

Налаштування подій TreeView

При виборі вузла елемент управління TreeView генерує дві події: BeforeSelect і AfterSelect. Однак для розглянутого елемента управління краще мати різні події для вузлів групи і кінцевих вузлів, тому додамо власні події BeforeGroupSelect / AfterGroupSelect і BeforeLeafSelect / AfterLeafSelect з користувацькими класами параметрів подій, які генеруються в додаток до основних подій:

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

Користувальницькі класи вузлів (TreeLeafNode і TreeGroupNode) і користувальницькі класи параметрів подій доступні в коді для завантаження.

Типове додаток

Щоб повністю зрозуміти весь код в цьому типовому елементі управління, проаналізуйте його роботу в додатку. Надане типове додаток працює з базою даних Microsoft Access pubs.mdb і ілюструє, як елемент управління Tree взаємодіє з іншими пов’язаними з даними елементами управління, щоб створити програми Windows. Головною особливістю цього прикладу є те, що необхідно приділити особливу увагу включенню синхронізації Tree з іншими пов’язаними елементами управління і автоматичного вибору вузла дерева, коли в джерелі даних виконується пошук.

Примітка. Ето типове додаток (що називається “TheSample”) можна завантажити для цієї статті.

Рис. 4. Демонстраційне додаток для пов’язаного з даними
TreeView

Резюме

Пов’язаний з даними елемент управління Tree, розглянутий у цій статті, не є рішенням для кожного проекту, що вимагає елемента управління Tree для відображення інформації з бази даних, проте ілюструє один спосіб своєї настройки для певних цілей. Чи не слід забувати, що більша частина коду в цьому елементі керування збігається з кодом в будь-якому складному пов’язаному з даними елементі управління, який потрібно побудувати, і що можна розширити цю існуючу роботу, щоб спростити розробку елементів управління в майбутньому.

У наступному прикладі “Малювання власних елементів керування за допомогою GDI + “буде проілюстрований значно простіший спосіб реалізації зв’язування даних в тих ситуаціях, коли не потрібно використовувати специфічний базовий клас (як в цьому елементі керування) для наслідування з елемента управління TreeView.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*