在 ComboBox 中嵌入 ObjectListView
一种将任何 ObjectListView 放入 ComboBox 的简单方法
引言
本文是我之前文章“将 DataGridView 嵌入 ComboBox”的扩展。
在我写完介绍如何在一个 ComboBox
控件中使用 DataGridView
的文章后,我发现了一个惊人的开源替代方案——ObjectListView。它有许多高级的内置功能,其中一个尤其有价值的功能是通用快速搜索和过滤。这个功能使其成为嵌入 ComboBox
的完美选择,结果如下所示:
背景
本文基于以下文章中提出的想法
- 灵活的 ComboBox 和 EditingControl - 利用
ToolStripControlHost
- 将 DataGridView 嵌入 ComboBox - 将
DataGridView
嵌入ComboBox
中
首先,创建自定义的 ToolStripControlHost
,然后用它来创建自定义 ComboBox
。我没有创建自定义的 DataGridViewColumn
,因为将 ObjectListView
这样出色的控件用在 DataGridView
中是一种浪费,而 ObjectListView
有一个通用的机制可以添加任何编辑控件,包括这个。
Using the Code
要使用提供的控件,您需要创建一个新的 UserControl
,并继承自提供的 InfoListControl
类。该控件代表了您要在下拉列表中显示的对象视图(一些列显示对象的属性)。该控件的目的是允许开发人员在 VS 设计器中配置 ObjectListView
(将列绑定到特定数据源类型的属性,设置各种视觉属性等)。因此,对于您将要使用的每种对象类型,您都需要一个 InfoListControl
。如果您使用标准的 Visual Studio 方法创建继承的 UserControl
,则无需编写一行代码。创建继承的控件后,您将看到一个 ObjectListView
设计器视图。在这里,您可以配置所有您需要的东西:设置数据绑定(列的 AspectName
)、配置列标题、视觉外观等。您可以在 此处找到详细描述。
为您特定数据源类型创建 InfoListControl
后,您只需几行代码即可完成控件设置
Dim cntr As AccListComboBox
dim view As InfoListControl = new yourInfoListControlInstance
view.DataSource = GetDataSource(whatever)
' a name of the property that should be used to set SelectedValue
' an empty string if the SelectedValue should contain the object itself
view .ValueMember = ""
' whether to accept a single click to confirm a user selection
view .AcceptSingleClick = True
cntr.AddDataListView(view)
' e.g.. when you want to display integer value 0 as an empty string
cntr.EmptyValueString = "0"
在 InfoListControl
内部,我使用了一个 ObjectListView
的变体——DataListView
,因为它开箱即用地支持数据绑定。但是,如果您需要非标准数据绑定(例如 DataTable
、分层结构),您可以轻松实现自己的控件来支持它。
重要提示
ObjectListView
的当前版本有一个错误,当 ObjectListViewComboBox
用作 ObjectListView
本身的编辑控件时会导致异常。因此,您需要对 ObjectListView
源代码(CellEditKeyEngine
类中的 ItemBeingEdited
属性)进行以下更正,并自行编译(即不要使用预编译的二进制文件)。
/// <summary>
/// Gets the row of the cell that is currently being edited
/// </summary>
protected OLVListItem ItemBeingEdited {
get {
OLVListItem olvi = (this.ListView == null ||
this.ListView.CellEditEventArgs == null) ?
null : this.ListView.CellEditEventArgs.ListViewItem;
if (olvi != null && olvi.Index < 0)
{
if (olvi.RowObject == null)
return olvi;
for (int i = 0; i < this.ListView.Items.Count; i++)
{
if (this.ListView.GetItem(i).RowObject == olvi.RowObject)
{
olvi = this.ListView.GetItem(i);
break;
}
}
}
return olvi;
}
}
关注点
创建 InfoListControl
该控件的核心部分是 ObjectListViewToolStrip
类,它继承自 ToolStripControlHost
。基本上,ToolStripControlHost
类可以自行处理任何控件,包括 ObjectListView
,但在这种情况下,开发人员需要以编程方式创建 ObjectListView
(不太用户友好)。因此,我决定创建一个专用的用户控件,一方面整洁地封装了所有 ObjectListView
特定的逻辑,另一方面提供了完整的设计器支持。
实际上,在 InfoListControl
中,我没有使用通用的 ObjectListView
,而是使用了它的子类——DataListView
(因为我需要数据绑定支持)。对实际控件的封装提供了一种简单的方式来重写该控件以支持通用的 ObjectListView
或 TreeListView
(ObjectListView
的另一个子类)。
InfoListControl
包含一个由 VS 设计器添加的 DataListView
实例,并提供了一个简单的 public
构造函数以及四个几乎不言自明的属性及其后备字段。
Private _AcceptSingleClick As Boolean = False
Private _ValueMember As String = ""
Private _FilterString As String = ""
''' <summary>
''' Whether single click is sufficient to choose an item.
''' </summary>
''' <remarks></remarks>
Public Property AcceptSingleClick() As Boolean
Get
Return _AcceptSingleClick
End Get
Set(ByVal value As Boolean)
_AcceptSingleClick = value
End Set
End Property
''' <summary>
''' A value object property that holds the required value (if any).
''' </summary>
''' <remarks></remarks>
Public Property ValueMember() As String
Get
Return _ValueMember
End Get
Set(ByVal value As String)
If value Is Nothing Then value = ""
_ValueMember = value
End Set
End Property
''' <summary>
''' A <see cref="BindingSource">BindingSource_
</see> that wraps a value object list.
''' </summary>
''' <remarks></remarks>
Public Property DataSource() As Object
Get
Return baseDataListView.DataSource
End Get
Set(ByVal value As Object)
baseDataListView.DataSource = value
End Set
End Property
''' <summary>
''' A string that is used to filter the value object list.
''' </summary>
''' <remarks></remarks>
Public Property FilterString() As String
Get
Return _FilterString
End Get
Set(ByVal value As String)
If value Is Nothing Then value = ""
If value <> _FilterString Then
_FilterString = value
baseDataListView.AdditionalFilter = _
TextMatchFilter.Contains(baseDataListView, _FilterString)
End If
End Set
End Property
Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()
baseDataListView.DefaultRenderer = New HighlightTextRenderer( _
TextMatchFilter.Contains(baseDataListView, New String() {""}))
baseDataListView.SelectColumnsMenuStaysOpen = True
End Sub
这里唯一值得关注的是对 ObjectListView
方法的调用,这些方法用于设置过滤图形渲染器和过滤(string
)本身。
接下来,InfoListControl
将实现事件以将用户选择传递给父 ToolStripControlHost
。
Friend Delegate Sub ValueSelectedEventHandler(ByVal sender As Object, ByVal e As ValueChangedEventArgs)
Friend Event ValueSelected As ValueSelectedEventHandler
Protected Sub OnValueSelected(ByVal e As ValueChangedEventArgs)
RaiseEvent ValueSelected(Me, e)
End Sub
Protected Sub OnValueSelected(ByVal currentObject As Object, ByVal isCanceled As Boolean)
If _ValueMember Is Nothing OrElse String.IsNullOrEmpty(_ValueMember) _
OrElse currentObject Is Nothing Then
RaiseEvent ValueSelected(Me, New ValueChangedEventArgs(currentObject, isCanceled))
Else
If baseDataListView.GetItemCount() < 1 OrElse baseDataListView.GetItem(0). _
RowObject.GetType().GetProperty(_ValueMember.Trim, BindingFlags.Public _
OrElse BindingFlags.Instance) Is Nothing Then
RaiseEvent ValueSelected(Me, New ValueChangedEventArgs(Nothing, isCanceled))
Else
RaiseEvent ValueSelected(Me, New ValueChangedEventArgs( _
GetValueMemberValue(currentObject), isCanceled))
End If
End If
End Sub
Public Class ValueChangedEventArgs
Inherits EventArgs
Private _SelectedValue As Object = Nothing
Private _SelectionCanceled As Boolean = False
Public ReadOnly Property SelectedValue() As Object
Get
Return _SelectedValue
End Get
End Property
Public ReadOnly Property SelectionCanceled() As Boolean
Get
Return _SelectionCanceled
End Get
End Property
Friend Sub New(ByVal newValue As Object, ByVal isCanceled As Boolean)
_SelectedValue = newValue
_SelectionCanceled = isCanceled
End Sub
End Class
这里值得关注的是 OnValueSelected
方法重载,它会引发事件并将选定的对象传递给事件参数。该方法的作用是过滤用户输入的选择,并(如果设置了 ValueMember
属性)返回不是对象本身,而是所需属性的值。所需属性的值是使用一个简单的帮助方法 GetValueMemberValue
获取的,该方法在 Try...Catch
块中使用简单的反射来避免运行时异常。
最后,InfoListControl
需要处理用户输入(通过鼠标或键盘)。
Private Sub baseDataListView_CellClick(ByVal sender As Object, _
ByVal e As CellClickEventArgs) Handles baseDataListView.CellClick
If Not e.Model Is Nothing AndAlso (e.ClickCount = 2 OrElse _
(e.ClickCount = 1 AndAlso _AcceptSingleClick)) Then
OnValueSelected(e.Model, False)
End If
End Sub
Private Sub baseDataListView_KeyDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs) Handles baseDataListView.KeyDown
If e.KeyData = Keys.Enter AndAlso Not baseDataListView.SelectedItem Is Nothing _
AndAlso Not baseDataListView.SelectedItem.RowObject Is Nothing Then
OnValueSelected(baseDataListView.SelectedItem.RowObject, False)
e.Handled = True
ElseIf e.KeyData = Keys.Back Then
If _FilterString <> "" Then
_FilterString = _FilterString.Substring(0, _FilterString.Length - 1)
baseDataListView.AdditionalFilter = _
TextMatchFilter.Contains(baseDataListView, _FilterString)
End If
e.Handled = True
ElseIf e.KeyData = Keys.Delete Then
If _FilterString <> "" Then
_FilterString = ""
baseDataListView.AdditionalFilter = _
TextMatchFilter.Contains(baseDataListView, _FilterString)
End If
e.Handled = True
ElseIf e.KeyData = Keys.Escape Then
OnValueSelected(Nothing, True)
e.Handled = True
End If
End Sub
Private Sub baseDataListView_KeyPress(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles baseDataListView.KeyPress
If Not Char.IsControl(e.KeyChar) AndAlso (Char.IsLetterOrDigit(e.KeyChar) _
OrElse Char.IsPunctuation(e.KeyChar)) Then
_FilterString = _FilterString & e.KeyChar
baseDataListView.AdditionalFilter = _
TextMatchFilter.Contains(baseDataListView, _FilterString)
End If
End Sub
代码本身具有解释性。标准的事件处理程序评估用户输入,并引发 ValueSelected
事件来指示用户选择,或修改过滤 string
。
- 字母、数字或标点符号键 会添加到过滤
string
中。 - 退格键 会删除过滤
string
中的最后一个char
。 - Delete 键 会清除过滤
string
。 - Enter 键 会使用当前选定的对象引发
ValueSelected
事件。 - Escape 键 会引发
ValueSelected
事件,并将null
对象和取消标志设置为true
。 - 单击鼠标 如果
AcceptSingleClick
设置为TRUE
,则会引发ValueSelected
事件并带有被单击的对象。 - 双击鼠标 会引发
ValueSelected
事件并带有被单击的对象。
从外部视角(父 ToolStripControlHost
控件的视角)来看,InfoListControl
是一个 Control
,它公开了 4 个自定义属性和 1 个自定义事件。包含的 DataListView
控件也可以从外部对象访问,但通常只通过 VS 设计器进行配置。
创建 ObjectListViewToolStrip
该控件的核心部分是 ObjectListViewToolStrip
类,它继承自 ToolStripControlHost
。它是一个内部类(仅由 ObjectListViewComboBox
内部使用),并处理 InfoListControl
。
- 提供对所封装
InfoListControl
的FilterString
属性的代理属性。 - 提供对所封装
InfoListControl
的DataSource
属性的代理访问方法GetDataSource
。 - 处理所封装
InfoListControl
的ValueSelected
事件,并封装当前选定的值(SelectedValue
)和取消标志(SelectionCanceled
)。 - 提供下拉列表大小的标准属性(
MinDropDownWidth
和DropDownHeight
)。
特定的 ObjectListViewToolStrip
方法是构造函数以及连接所封装 InfoListControl
事件的方法。
Public Sub New(ByVal listView As InfoListControl)
MyBase.New(listView)
Me.AutoSize = False
Me._MinDropDownWidth = listView.Width
Me._DropDownHeight = listView.Height
End Sub
Private Sub OnDataListViewValueSelected(ByVal sender As Object, _
ByVal e As InfoListControl.ValueChangedEventArgs)
_SelectedValue = e.SelectedValue
_SelectionCanceled = e.SelectionCanceled
DirectCast(Me.Owner, ToolStripDropDown).Close(ToolStripDropDownCloseReason.ItemClicked)
End Sub
' Subscribe and unsubscribe the control events you wish to expose.
Protected Overrides Sub OnSubscribeControlEvents(ByVal c As Control)
' Call the base so the base events are connected.
MyBase.OnSubscribeControlEvents(c)
' Cast the control to a InfoListControl control.
Dim nDataListView As InfoListControl = DirectCast(c, InfoListControl)
' Add the event.
AddHandler nDataListView.ValueSelected, AddressOf OnDataListViewValueSelected
End Sub
Protected Overrides Sub OnUnsubscribeControlEvents(ByVal c As Control)
' Call the base method so the basic events are unsubscribed.
MyBase.OnUnsubscribeControlEvents(c)
' Cast the control to a InfoListControl control.
Dim nDataListView As InfoListControl = DirectCast(c, InfoListControl)
' Remove the event.
RemoveHandler nDataListView.ValueSelected, AddressOf OnDataListViewValueSelected
End Sub
Protected Overrides Sub OnBoundsChanged()
MyBase.OnBoundsChanged()
If Not Control Is Nothing Then
DirectCast(Control, InfoListControl).Size = Me.Size
End If
End Sub
构造函数设置所封装的 InfoListControl
并关闭自动调整大小,将大小控制留给重写的 OnBoundsChanged
方法。出于未知原因,内置的自动调整大小方法在 ToolStripControlHost
中使用时会失败。
OnSubscribeControlEvents
和 OnUnsubscribeControlEvents
方法将 ValueSelected
事件连接到 OnDataListViewValueSelected
处理程序。该处理程序将 ValueSelected
事件参数持久化到 SelectedValue
和 SelectionCanceled
属性中,并关闭选择下拉列表。
创建 ObjectListViewComboBox
控件本身继承自 ComboBox
。它公开以下 public
属性和方法:
HasAttachedInfoList
- 指示是否已将InfoListControl
分配给控件。InstantBinding
- 是否在用户选择值时立即更新数据源(与验证时相对)。SelectedValue
- 当前选定的值对象,或者InfoListControl.ValueMember
属性(如果已设置)的值对象的值。EmptyValueString
- 一个SelectedValue string
表达式(ToString
),应显示为空string
(例如,如果您设置EmptyValueString="0"
,则整数值0
将显示为空string
)。FilterString
- 当前应用的过滤字符串(代理属性)。InfoListControlDataSource
- 嵌套InfoListControl
的数据源(代理属性)。AddDataListView
- 向控件添加一个新的InfoListControl
,即初始化控件。之后无法替换InfoListControl
。
ObjectListViewComboBox
类有一个 private
变量 myDropDown As ToolStripDropDown
,它充当 ObjectListViewToolStrip
的容器。ObjectListViewToolStrip
本身的实例由 AddDataListView
方法创建。
Public Sub AddDataListView(ByVal dataView As InfoListControl)
If Not myListView Is Nothing Then Throw New InvalidOperationException( _
"Error. DataListView is already assigned to the ObjectListViewComboBox.")
myListView = New ObjectListViewToolStrip(dataView)
If myDropDown Is Nothing OrElse myDropDown.IsDisposed Then
myDropDown = New ToolStripDropDown()
myDropDown.AutoSize = False
myDropDown.GripStyle = ToolStripGripStyle.Visible
AddHandler myDropDown.Closed, AddressOf ToolStripDropDown_Closed
Else
myDropDown.Items.Clear()
End If
myDropDown.Items.Add(myListView)
myDropDown.Width = Math.Max(Me.Width, myListView.MinDropDownWidth)
myDropDown.Height = myListView.Height
End Sub
ObjectListViewComboBox
通过重写 WndProc
和拦截消息来处理显示下拉列表。此方法的当前实现是从 CodeProject 文章 灵活的 ComboBox 和 EditingControl 复制的,如果需要手动输入支持,应进行更改,因为它会捕获 combobox
的所有区域的点击,从而阻止文本输入。
Private Const WM_LBUTTONDOWN As UInt32 = &H201
Private Const WM_LBUTTONDBLCLK As UInt32 = &H203
Private Const WM_KEYF4 As UInt32 = &H134
Protected Overrides Sub WndProc(ByRef m As Message)
'#Region "WM_KEYF4"
If m.Msg = WM_KEYF4 Then
Me.Focus()
Me.myDropDown.Refresh()
If Not Me.myDropDown.Visible Then
ShowDropDown()
Else
myDropDown.Close()
End If
Return
End If
'#End Region
'#Region "WM_LBUTTONDBLCLK"
If m.Msg = WM_LBUTTONDBLCLK OrElse m.Msg = WM_LBUTTONDOWN Then
If Not Me.myDropDown.Visible Then
ShowDropDown()
Else
myDropDown.Close()
End If
Return
End If
'#End Region
MyBase.WndProc(m)
End Sub
ObjectListViewComboBox
实际上显示下拉列表的方法主要处理下拉列表的大小、定位以及选择合适的 DataListView
行(其中包含当前 SelectedValue
)。
Private Sub ShowDropDown()
If Not myDropDown Is Nothing AndAlso Not Me.myListView Is Nothing Then
If Not myDropDown.Items.Contains(Me.myListView) Then
myDropDown.Items.Clear()
myDropDown.Items.Add(Me.myListView)
End If
myDropDown.Width = Math.Max(Me.Width, Me.myListView.MinDropDownWidth)
myListView.Size = myDropDown.Size
myListView.SetSelectedValue(_SelectedValue)
myDropDown.Show(Me, CalculatePoz)
SendKeys.Send("{down}")
End If
End Sub
Private Function CalculatePoz() As Point
Dim point As New Point(0, Me.Height)
If (Me.PointToScreen(New Point(0, 0)).Y + Me.Height + Me.myListView.Height) _
> Screen.PrimaryScreen.WorkingArea.Height Then
point.Y = -Me.myListView.Height - 7
End If
Return point
End Function
ObjectListViewComboBox
通过重载 SelectedValue
属性(以绕过本机 ComboBox
逻辑)并提供自定义 setter 方法来实现设置当前值,该方法可以通过 ValueMember
设置值对象。
Private Sub ToolStripDropDown_Closed(ByVal sender As Object, _
ByVal e As ToolStripDropDownClosedEventArgs)
If e.CloseReason = ToolStripDropDownCloseReason.ItemClicked _
AndAlso Not myListView Is Nothing AndAlso Not myListView.SelectionCanceled Then
If Not MyBase.Focused Then MyBase.Focus()
SetValue(myListView.SelectedValue)
If _InstantBinding Then
For Each b As Binding In MyBase.DataBindings
b.WriteValue()
Next
End If
End If
End Sub
Private Sub SetValue(ByVal value As Object)
If value Is Nothing Then
Me.Text = ""
ElseIf Not _EmptyValueString Is Nothing AndAlso _
Not String.IsNullOrEmpty(_EmptyValueString.Trim) AndAlso _
value.ToString.ToLower.Trim = _EmptyValueString.Trim.ToLower Then
Me.Text = ""
Else
Me.Text = value.ToString
End If
_SelectedValue = value
MyBase.OnSelectedValueChanged(New EventArgs)
End Sub
从上面的代码可以看出,ObjectListViewComboBox
还有一个自定义属性 InstantBinding
。它本身不是必需的,但在某些情况下,最好在实际值更改时更新绑定,而不是在验证时更新。
差不多就这些了,尽情享受 ObjectListView
吧!