65.9K
CodeProject 正在变化。 阅读更多。
Home

绑定 WinForms ListControls 的最佳实践

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (56投票s)

2004年9月27日

5分钟阅读

viewsIcon

264665

downloadIcon

2417

本文演示了 .NET Windows Forms 中 ListBox 和 ComboBox 控件的数据绑定最佳技术。

引言

根据 .NET Framework 文档,要将 Windows Forms 的 ListControl(例如 ListBox 或 ComboBox)绑定为查找控件(即,控件的显示值映射到不同的底层值),您需要设置三个属性:DataSourceValueMemberDisplayMember。据我所知,文档并未说明或展示的是,设置这些属性的顺序可能会对代码的性能和/或行为产生巨大影响。在本示例中,我将尝试阐明此问题,并展示我认为在代码中创建查找控件的正确方法。

背景

在 Windows Forms 中,我们将抽象类 ListControl 的子类视为“复杂绑定”控件。通俗地说,这意味着它们可用作查找控件,即如果它们的数据源是 ADO.NET DataTable,它们可以显示一列的值(可能是姓名),并使用另一列的值(可能是唯一的标识符)作为控件的相应值。您最可能遇到过的两个复杂绑定控件是 ComboBox 和 ListBox。要复杂绑定这些控件之一,您需要设置 DataSource(值来源)、DisplayMember(提供可见列表项的数据列名称)和 ValueMember(提供控件可能值的底层数据列名称)。您可能会认为设置这些属性的顺序无关紧要,Microsoft 和其他来源的示例也可能支持这种看法。然而,底层存在一些情况可能导致您的代码出现问题。特别是,当更改 DataSource,或者在设置 DataSource 后更改 DisplayMemberValueMember 时,绑定基础结构会强制控件重新绑定。逻辑上,这是有道理的。但这会给处理某些控件事件带来问题,特别是 SelectedIndexChangedSelectedValueChanged。在最坏的情况下,以下简单序列将连续触发三次 SelectedIndexChanged 事件。

listbox.DataSource = dataTable
listbox.ValueMember = "id"
listbox.DisplayMember = "name"

显然,如果您的处理程序执行任何非琐碎的操作,这可能会变成一个严重的性能问题。

更糟糕的是,在设置 DisplayMember 之前,控件的 SelectedValue 将不是您期望的类型,而是 DataRowView。此时,如果您正在处理上述 Selected-Changed 事件,您有两种选择:为 SelectedValue 返回 DataRowView 的情况添加特殊处理,或者处理并捕获尝试错误地使用 DataRowView 时抛出的异常。我实际上在专业代码中见过这两种情况。您不想遇到这种情况,因此在接下来的部分中,我将向您展示如何规避此问题。

您可能没有此问题的原因

如果您使用 Windows Forms 设计器来设置所有查找连接,则不会遇到此问题。据我所知,至少有两个原因。首先,在我所有的测试中,设计器始终在设置窗体控件的属性 -之后- 挂接事件,因此在设置有问题的属性时,事件甚至还没有被调用。其次,设计器在 InitializeComponents 的末尾将控件添加到窗体,在此之前无法触发任何事件。

不幸的是,您经常无法获得这些变通方法的优势。您可能不得不在添加所有控件很长时间后才绑定 ListControl,而暂时移除事件处理程序的变通方法会很麻烦。使用绑定上下文的 SuspendResume 可能会有一种变通方法,但这对于本应是一个简单的过程来说,修复起来太复杂了。

解决方案

解决方案很简单,可以用三个词概括:最后设置 DataSource。与您可能期望的相反,设置 DisplayMemberValueMember 不会触发控件的任何内部验证,如果还没有 DataSource,这些验证可能会失败。所以您这样做是安全的。但是绝大多数示例(包括 Microsoft 的示例)都先设置 DataSource,因此容易出现我提到的困难。

使用代码

包含的项目包含 C# 和 VB.NET 语言的代码,演示了问题的两个不同版本以及两个不同的解决方案。演示窗体包含四个 ListBox 和四个按钮,用于通过设置查找属性来绑定和解绑 ListBox。如果您查看演示窗体,我将按名称从上到下介绍 ListBox。

  1. “糟糕透顶”。此版本按以下顺序设置属性:DataSourceValueMemberDisplayMember。这会导致三次 SelectedIndexChanged 事件,其中 SelectedValue 分别等于 DataRowViewDataRowViewInt32。稳健处理起来很困难。
  2. “仅仅糟糕”。此版本按以下顺序设置属性:DataSourceDisplayMemberValueMember。我称之为“仅仅糟糕”,因为它只触发两次 SelectedIndexChanged 事件而不是三次,但它比“糟糕透顶”也强不到哪里去,因为在每次事件中,ListBox 的 SelectedValue 仍然是 DataRowView。您只得到两次事件,因为如果 DisplayMember 已经设置,那么设置 ValueMember 而不重新绑定似乎是可以的。
  3. “良好”。这是推荐的属性设置顺序:DisplayMemberValueMemberDataSource。在这里,您只会获得一次 SelectedIndexChanged 事件,并且它包含您真正想要的类型,在这种情况下是 Int32
  4. “良好的辅助函数”。为了在您的代码中强制执行良好的行为,您可能希望编写一个辅助函数,该函数接受一个 ListControl 以及要在推荐顺序下插入 DataSourceValueMemberDisplayMember 的值。此解决方案演示了这样一个函数,该函数可在 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 日首次发布。
© . All rights reserved.