Резюме
В статье рассматривается добавление функциональных возможностей
привязки данных к элементу управления TreeView как один из ряда примеров
разработки элементов управления Microsoft Windows, которые следует
читать вместе с обзорной статьей на смежную тему. (16 печатных страниц)
Исходный код
WinFormControls.exe можно загрузить из MSDN Code Center.
Эта статья является четвертой статьей в серии из пяти статей по
разработке элементов управления в Microsoft®
.NET:
- Разработка пользовательских элементов управления Windows с
помощью Visual Basic .NET (обзор)
- Добавление проверки правильности регулярного выражения
- Объединение нескольких элементов управления в один
- Расширение элемента управления TreeView
- Рисование собственных элементов управления с помощью GDI+
Содержаниее
По возможности работу всегда необходимо начинать с существующего
элемента управления. Во все предоставленные элементы управления
Microsoft® Windows®
Forms были вложены огромные усилия по кодированию и тестированию, и всю
эту работу нужно выполнять заново, если построение элемента управления
необходимо начинать полностью "с нуля". С учетом этого в данном примере
будет выполняться наследование из существующего элемента управления
Windows Form - Tree View - и его настройка. Код этого нового элемента
управления TreeView можно загрузить одним файлом наряду с прочими
примерами разработки элемента управления и типовым приложением,
демонстрирующим, как этот расширенный TreeView может использоваться
совместно с другими связанными с данными элементами управления.
Проектирование связанного с данными Tree View
Тема добавления привязки данных к элементу управления 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, к которому обращаются через этот объект, является
ключевым в реализации связывания данных, поскольку он обладает
свойствами, методами и событиями, позволяющими:
- обращаться к базовому объекту IList источника данных;
- получать и устанавливать поля или свойства на объекте в
источнике данных;
- синхронизировать элемент управления с другими связанными с
данными элементами управления в той же форме.
Получение значений 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.