自定义 ListBox 控件: 第 1 部分 - 集合事件






4.25/5 (6投票s)
创建一个 ListBox 控件,该控件提供添加或删除项目时的事件。
引言
在这个简短的文章系列中,我将解释如何编写一个具有一些扩展功能的自定义 ListBox 控件,我相信这些功能对大多数开发人员来说都很有用。除了文中解释的具体功能外,这些文章还将介绍如何着手创建自己的自定义组件,为 WinForms 控件工具箱提供您自己的附加功能。在第一篇文章中,我将讨论集合事件问题,特别是如何在添加或删除项目时添加事件通知。
许多包含项目集合的 .NET WinForms 控件缺少集合更改时的重要事件。通常,您需要通过数据绑定或自定义数据源来解决这个问题,但有时,如果事件直接连接到控件本身就好了。因此,我们将重点介绍这个问题以及派生自定义组件的基础知识。关于代码的一点说明;我决定使用 VB.NET(尽管我更喜欢 C#),因为我在 VB.NET 论坛上看到更多关于此主题的问题。
创建派生组件
经典的 ListBox
控件有其自己的实现,我们实际上无法对其进行太多更改。但是,我们可以创建自己的控件并继承经典 ListBox
控件的所有功能。我们可以走一条更长的路,创建一个用户控件并从头开始实现所有功能,但这将花费无数的开发时间,这并不是这里的重点。我们想要的是保留基本功能,只更改某些选定功能的行为。如果您熟悉继承的概念,那么理解这一点将毫无问题。但是,您可能没有想到的是,类继承与控件的工作方式相同。毕竟,像 Label
这样的控件,就像您编写代码的任何其他类一样。
让我们通过一个小例子来尝试一下
- 创建一个新的 Windows 应用程序。
- 向您的项目添加一个新类,并将其命名为
MyLabel
。 - 打开类的代码,并插入语句“
Inherits Label
”,如下面的代码片段所示。 - 编译解决方案。
Public Class MyLabel
Inherits Label
End Class
如果您打开 Form1 设计器并查看工具箱,您将在顶部找到一个名为 MyLabel
的新工具。现在,此控件就像经典的 Label
控件一样。您可以将其放置在 Form1
表面上并为其设置属性。它实际上与 Label
控件没有区别。为什么会这样?这是因为我们继承了 Label
控件及其所有功能,或者换句话说,我们派生自 Label
。
因此,现在您已经熟悉了创建自己的控件的概念,让我们开始处理我们的 CustomListBox
控件。
新增项目属性接口
我们实现 CustomListBox
控件的第一个目标是能够生成项目添加或删除时的事件。查看我们的基类 ListBox
控件,我们可以看到所有项目都存储在 ListBox.ObjectCollection
类中,该类可以通过 Items
属性访问。在我们的 CustomListBox
中,我们将创建一个新类,作为新的 Items
接口。这个类将是我们的 CustomListBox
控件用户在处理集合时将访问的类。让我们看一下这个类的代码。
Public Class CollectionObjectInterface
Implements IEnumerable
Private owner As CustomListBox
Friend Sub New(ByVal owner As CustomListBox)
Me.owner = owner
End Sub
Public Sub AddRange(ByVal items() As Object)
For Each item As Object In items
Me.Add(item)
Next
End Sub
Public Sub AddRange(ByVal value As ListBox.ObjectCollection)
For Each item As Object In value
Me.Add(item)
Next
End Sub
Public Sub Add(ByVal item As Object)
Me.Insert(Me.Count, item)
End Sub
Public Sub Insert(ByVal index As Integer, ByVal item As Object)
Me.owner.InnerItems.Insert(index, item)
Me.owner.OnItemAdded(index)
End Sub
Public Sub Remove(ByVal item As Object)
Dim index As Integer = Me.IndexOf(item)
If (index > -1) Then
Me.RemoveAt(index)
End If
End Sub
Public Sub Clear()
For i As Integer = (Me.Count - 1) To 0 Step -1
Me.RemoveAt(i)
Next
End Sub
Public Sub RemoveAt(ByVal index As Integer)
Me.owner.InnerItems.RemoveAt(index)
Me.owner.OnItemRemoved(index)
End Sub
Public Function Contains(ByVal item As Object) As Boolean
Return Me.owner.InnerItems.Contains(item)
End Function
Public Sub CopyTo(ByVal destination() As Object, ByVal arrayIndex As Integer)
Me.owner.InnerItems.CopyTo(destination, arrayIndex)
End Sub
Public Function GetEnumerator() As System.Collections.IEnumerator _
Implements IEnumerable.GetEnumerator
Return Me.owner.InnerItems.GetEnumerator()
End Function
Public Function IndexOf(ByVal value As Object) As Integer
Return Me.owner.InnerItems.IndexOf(value)
End Function
Public ReadOnly Property Count() As Integer
Get
Return Me.owner.InnerItems.Count
End Get
End Property
Public Property Item(ByVal index As Integer) As Object
Get
Return Me.owner.InnerItems(index)
End Get
Set(ByVal value As Object)
Me.owner.InnerItems(index) = value
End Set
End Property
End Class
ObjectCollectionInterface
具有 ListBox.ObjectCollection
类的所有公共成员。可以说它是 ListBox.ObjectCollection
类的一个副本,尽管实现不同。这是因为我们希望用户能够以与 ListBox
控件相同的方式使用 Items
属性。出于某些原因(超出了本文的范围),我们不继承 ListBox.ObjectCollection
。因此,我们必须完全实现所有成员。
这个类的想法是在用户和基类项目集合之间提供一个层,以便我们可以引发 ItemAdded
和 ItemRemoved
事件。它本身不存储任何项目,只是调用原始集合的方法。CustomListBox
将拥有这个类,所以我们在构造函数中传递对父 CustomListBox
的引用。CustomListBox
将通过一个名为 InnerItems
的属性公开基类 (ListBox
) 的 Items
集合。ObjectCollectionInterface
将使用此属性来委派在 Items
集合上进行的所有工作。正如您所看到的,所有与添加或删除项目相关的方法调用都被路由到 Insert
和 RemoveAt
方法,我们在其中对原始集合执行适当的操作,并引发事件。
CustomListBox 控件
CustomListBox
控件应负责提供辅助方法来引发 ItemAdded
和 ItemRemoved
事件,实现一个私有属性以将基类项目集合公开给 ObjectCollectionInterface
类,并提供 Items
属性的新实现,该实现使用 ObjectCollectionInterface
。让我们在代码中看看这三个目标。
''' <summary>
''' Represents a ListBox with events for when adding and removing items.
''' </summary>
Public Class CustomListBox
Inherits ListBox
' Omitted the code for the internal classes ObjectCollectionInterface and
' ListBoxItemEventArgs...
Public Event ItemAdded(ByVal sender As Object, ByVal e As ListBoxItemEventArgs)
Public Event ItemRemoved(ByVal sender As Object, ByVal e As ListBoxItemEventArgs)
Private itemsInterface As CollectionObjectInterface
''' <summary>
''' Constructor. Creates a new CollectionObjectInterface that references
''' this CustomListBox instance.
''' </summary>
Public Sub New()
MyBase.New()
Me.itemsInterface = New CollectionObjectInterface(Me)
End Sub
''' <summary>
''' Gets the items of the System.Windows.Forms.CustomListBox.
''' </summary>
''' <returns>An ObjectCollectionInterface representing the items in
''' the CustomListBox.</returns>
''' <remarks>Hides the ListBox Items interface in favor
''' of our own interface.</remarks>
Public Shadows ReadOnly Property Items() As CollectionObjectInterface
Get
Return Me.itemsInterface
End Get
End Property
''' <summary>
''' Gets the base Items collection. This property exposes the ListBox's
''' original Items collection to the ObjectCollectionInterface.
''' </summary>
Private ReadOnly Property InnerItems() As ListBox.ObjectCollection
Get
Return MyBase.Items
End Get
End Property
''' <summary>
''' Fires the ItemAdded event.
''' </summary>
''' <param name="index">Specifies the zero-based index for the item
''' that was added.</param>
Protected Overridable Sub OnItemAdded(ByVal index As Integer)
RaiseEvent ItemAdded(Me, New ListBoxItemEventArgs(index))
End Sub
''' <summary>
''' Fires the ItemRemoved event.
''' </summary>
''' <param name="index">Specifies the zero-based index for the item
''' that was removed.</param>
Protected Overridable Sub OnItemRemoved(ByVal index As Integer)
RaiseEvent ItemRemoved(Me, New ListBoxItemEventArgs(index))
End Sub
End Class
除了我们继承 ListBox
控件这一事实外,CustomListBox
类中最有趣的是 Items
属性的声明。请记住,我们从基类 ListBox
继承了一个 Items
属性,如果用户访问该属性,我们将无法使用我们的 ObjectCollectionInterface
类。因此,为了利用我们自己的接口,我们必须隐藏基类属性。要隐藏基类成员,我们必须使用 Shadows
修饰符。新的 Items
属性返回 ObjectCollectionInterface
的一个实例。该实例由一个私有变量持有,该变量在构造函数中赋值。
ListBoxItemEventArgs 类
您可能已经注意到,在事件声明中,有一个参数 e
被声明为 ListBoxItemEventArgs
。此类派生自 EventArgs
类,并存储一个整数,该整数表示添加或删除的项目的零基索引。ListBoxItemEventArgs
类的代码如下所示。
''' <summary>
''' Represents the data for the ItemAdded and ItemRemoved events.
''' </summary>
Public Class ListBoxItemEventArgs
Inherits EventArgs
Private _index As Integer
Public Sub New(ByVal index As Integer)
Me._index = index
End Sub
''' <summary>
''' Gets the zero-based index for the item.
''' </summary>
Public ReadOnly Property Index() As Integer
Get
Return Me._index
End Get
End Property
End Class
当然,您可以通过多种方式扩展事件的功能。例如,您可以通过向事件数据添加取消标志,为事件订阅者提供阻止删除项目的可能性。或者,您可以在 ItemRemoved
事件的事件参数中添加对已删除对象的引用。
关于可重写成员的说明
在为本文进行一些背景研究时,我发现了一篇采访微软 C# 首席架构师 Anders Hejlsberg 的文章,其中讨论了将基类成员声明为可重写的优缺点。根据 Anders Hejlsberg 的说法,.NET 框架中大多数类成员不可重写的两个主要原因是:首先,这给覆盖基成员的开发人员带来了很大的责任。他或她必须知道从覆盖成员内部可以调用哪些其他方法,以及它们应该以什么顺序调用,以免导致无效状态。其次,调用一个被覆盖的类成员会引入额外的开销。就个人而言,我认为在某些时候无法覆盖成员是有限制的。根据上下文的复杂性,我认为 .NET 框架的许多基类成员可以被标记为可重写,而不会引入总体性能下降或程序稳定性下降。在 CustomListBox
的情况下,如果 ListBox.ObjectCollection
的所有成员都被标记为可重写,事情可能会变得更好。
您可以在 此处 找到与 Anders Hejlsberg 的完整采访。
结论
在第一篇文章中,我们研究了如何通过从现有 UI 控件派生新控件来扩展或更改其功能。我们实现了一个 CustomListBox
控件,该控件可以在存储在基类集合中的项目被添加或删除时生成事件。在下一篇文章中,我们将研究如何实现将选定项目在列表中向上或向下移动的方法。