绑定 WinForms ListControls 的最佳实践






4.94/5 (56投票s)
2004年9月27日
5分钟阅读

264665

2417
本文演示了 .NET Windows Forms 中 ListBox 和 ComboBox 控件的数据绑定最佳技术。
引言
根据 .NET Framework 文档,要将 Windows Forms 的 ListControl
(例如 ListBox 或 ComboBox)绑定为查找控件(即,控件的显示值映射到不同的底层值),您需要设置三个属性:DataSource
、ValueMember
和 DisplayMember
。据我所知,文档并未说明或展示的是,设置这些属性的顺序可能会对代码的性能和/或行为产生巨大影响。在本示例中,我将尝试阐明此问题,并展示我认为在代码中创建查找控件的正确方法。
背景
在 Windows Forms 中,我们将抽象类 ListControl
的子类视为“复杂绑定”控件。通俗地说,这意味着它们可用作查找控件,即如果它们的数据源是 ADO.NET DataTable
,它们可以显示一列的值(可能是姓名),并使用另一列的值(可能是唯一的标识符)作为控件的相应值。您最可能遇到过的两个复杂绑定控件是 ComboBox 和 ListBox。要复杂绑定这些控件之一,您需要设置 DataSource
(值来源)、DisplayMember
(提供可见列表项的数据列名称)和 ValueMember
(提供控件可能值的底层数据列名称)。您可能会认为设置这些属性的顺序无关紧要,Microsoft 和其他来源的示例也可能支持这种看法。然而,底层存在一些情况可能导致您的代码出现问题。特别是,当更改 DataSource
,或者在设置 DataSource
后更改 DisplayMember
或 ValueMember
时,绑定基础结构会强制控件重新绑定。逻辑上,这是有道理的。但这会给处理某些控件事件带来问题,特别是 SelectedIndexChanged
和 SelectedValueChanged
。在最坏的情况下,以下简单序列将连续触发三次 SelectedIndexChanged
事件。
listbox.DataSource = dataTable
listbox.ValueMember = "id"
listbox.DisplayMember = "name"
显然,如果您的处理程序执行任何非琐碎的操作,这可能会变成一个严重的性能问题。
更糟糕的是,在设置 DisplayMember
之前,控件的 SelectedValue
将不是您期望的类型,而是 DataRowView
。此时,如果您正在处理上述 Selected-Changed 事件,您有两种选择:为 SelectedValue
返回 DataRowView
的情况添加特殊处理,或者处理并捕获尝试错误地使用 DataRowView
时抛出的异常。我实际上在专业代码中见过这两种情况。您不想遇到这种情况,因此在接下来的部分中,我将向您展示如何规避此问题。
您可能没有此问题的原因
如果您使用 Windows Forms 设计器来设置所有查找连接,则不会遇到此问题。据我所知,至少有两个原因。首先,在我所有的测试中,设计器始终在设置窗体控件的属性 -之后- 挂接事件,因此在设置有问题的属性时,事件甚至还没有被调用。其次,设计器在 InitializeComponents
的末尾将控件添加到窗体,在此之前无法触发任何事件。
不幸的是,您经常无法获得这些变通方法的优势。您可能不得不在添加所有控件很长时间后才绑定 ListControl,而暂时移除事件处理程序的变通方法会很麻烦。使用绑定上下文的 Suspend
和 Resume
可能会有一种变通方法,但这对于本应是一个简单的过程来说,修复起来太复杂了。
解决方案
解决方案很简单,可以用三个词概括:最后设置 DataSource。与您可能期望的相反,设置 DisplayMember
和 ValueMember
不会触发控件的任何内部验证,如果还没有 DataSource
,这些验证可能会失败。所以您这样做是安全的。但是绝大多数示例(包括 Microsoft 的示例)都先设置 DataSource
,因此容易出现我提到的困难。
使用代码
包含的项目包含 C# 和 VB.NET 语言的代码,演示了问题的两个不同版本以及两个不同的解决方案。演示窗体包含四个 ListBox 和四个按钮,用于通过设置查找属性来绑定和解绑 ListBox。如果您查看演示窗体,我将按名称从上到下介绍 ListBox。
- “糟糕透顶”。此版本按以下顺序设置属性:
DataSource
、ValueMember
、DisplayMember
。这会导致三次SelectedIndexChanged
事件,其中SelectedValue
分别等于DataRowView
、DataRowView
和Int32
。稳健处理起来很困难。 - “仅仅糟糕”。此版本按以下顺序设置属性:
DataSource
、DisplayMember
、ValueMember
。我称之为“仅仅糟糕”,因为它只触发两次SelectedIndexChanged
事件而不是三次,但它比“糟糕透顶”也强不到哪里去,因为在每次事件中,ListBox 的SelectedValue
仍然是DataRowView
。您只得到两次事件,因为如果DisplayMember
已经设置,那么设置ValueMember
而不重新绑定似乎是可以的。 - “良好”。这是推荐的属性设置顺序:
DisplayMember
、ValueMember
和DataSource
。在这里,您只会获得一次SelectedIndexChanged
事件,并且它包含您真正想要的类型,在这种情况下是Int32
。 - “良好的辅助函数”。为了在您的代码中强制执行良好的行为,您可能希望编写一个辅助函数,该函数接受一个 ListControl 以及要在推荐顺序下插入
DataSource
、ValueMember
和DisplayMember
的值。此解决方案演示了这样一个函数,该函数可在 LookupUtility.cs 或 LookupUtility.vb 中找到。
这是 C# 中的辅助函数的源代码。
public static void SetLookupBinding( ListControl toBind,
object dataSource, string displayMember,
string valueMember )
{
toBind.DisplayMember = displayMember;
toBind.ValueMember = valueMember;
// Only after the following line will the listbox
// receive events due to binding.
toBind.DataSource = dataSource;
}
以及 Visual Basic.NET。
Public Shared Sub SetLookupBinding(ByVal toBind As ListControl,
ByVal dataSource As Object, ByVal displayMember As String,
ByVal valueMember As String)
toBind.DisplayMember = displayMember
toBind.ValueMember = valueMember
'// Only after the following line will the listbox
'// receive events due to binding.
toBind.DataSource = dataSource
End Sub
历史
- 2004 年 9 月 25 日首次发布。