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

详细数据绑定教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (142投票s)

2008年3月25日

MIT

25分钟阅读

viewsIcon

1065125

downloadIcon

14563

通过几个简单的示例,演示了多种 Windows Forms 数据绑定功能。

引言

Windows Forms 数据绑定的文档相当稀疏。它究竟是如何工作的?你能用它做多少事情?很多人知道如何使用数据绑定,但可能很少有人真正理解它。我仅仅是弄清楚如何使用它就已经很费劲了,所以我开始调查它,希望理解这个神秘的“野兽”。

我相信“数据绑定”传统上指的是控件和数据库行或表之间的自动同步。在 .NET Framework(和 Compact Framework 2.0)中,你仍然可以这样做,但这个概念已经扩展到其他场景,以至于你可以将任何控件的几乎任何属性绑定到几乎任何对象。

System.Windows.Forms.BindingSource 是 .NET Framework 2.0 中的新功能。我的印象是 Microsoft 希望我们使用 BindingSource 而不是旧的类,例如 CurrencyManagerBindingContext,所以本文只会帮助你使用 BindingSource

数据绑定可以使用反射,因此你不仅限于 ADO.NET DataSet 中的数据库表和行;相反,几乎任何具有属性的对象都可以工作。例如,如果你的选项存储在一个普通的 .NET 对象中,数据绑定将有助于实现一个“选项”对话框。

这不是 ADO.NET/DataSetDataGridView 的教程,但请参阅相关文章

注意:本文假定你精通 C#(也可能精通 ADO.NET),但对数据绑定一无所知。

免责声明:我稍微谈论了 .NET Compact Framework 对数据绑定的支持,但我不知道这里描述的一切是否都可以在该平台上实现。

目录

数据绑定 API 简介

请注意以下几点:

  • Control.DataBindings 集合包含 Binding 对象,每个对象都有一个类型为 ObjectDataSource 属性。
  • ListBoxDataGridView 等的 DataSource 属性类型为 Object
  • BindingSource 类也有一个类型为 ObjectDataSource

那么,这些对象必须是什么呢?我发现关于这个主题的文档相当令人困惑,这就是为什么会编写此类教程的原因。在实际应用中,你可能会使用 BindingSource 对象作为列表控件和 BindingDataSource 属性。如果你使用数据库,你的 BindingSourceDataSource 属性通常是一个 DataSet;否则,它可能是你的应用程序中某个类的对象。

数据绑定似乎有多种不同的方法,但我无法在任何地方找到详细说明。因此,我做了一些实验,学习了几种可以做的事情。

让我们从典型情况开始:你将一个 BindingSource 对象分配给控件的 DataSource。你可以将 BindingSource 视为一个“二合一”数据源。它具有

  1. 一个名为 Current 对象的单个对象。Control 的属性可以绑定到 Current 的属性。
  2. 一个实现 IListList 对象。该列表应包含与 Current 对象类型相同的对象。List 是一个只读属性,它要么返回 BindingSource 的“内部列表”(如果未设置 DataMember 字符串),要么返回在设置 DataMember 时使用的“外部列表”。Current 始终是 List 的成员(或 null)。当你将 DataSource 设置为单个(非列表)对象时,你的 List 只包含该一个项。

数据绑定在不同类型的控件之间工作方式不同。

  • ComboBoxListBox 通过它们的 DataSourceDisplayMember 属性绑定到 List。你通常将 DataSource 设置为 BindingSource,并将 DisplayMember 设置为 Current 对象的属性之一的名称。
  • DataGridDataGridView 通过它们的 DataSource 属性绑定到 ListDataGridDataGridView 没有 DisplayMember 属性,因为它们可以显示数据源中的多个属性(每列一个),而不仅仅是一个。DataGridView 还有一个名为 DataMember 的附加属性,它似乎类似于 BindingSourceDataMember 属性。通常情况下,你不会将网格的 DataMember 设置为任何值,除非其 DataSource 不是 BindingSource(如果你使用 BindingSource,则应设置 BindingSource.DataMember。)
  • “简单”控件,例如 TextBoxButtonCheckBox 等,通过 Control.DataBindings 集合绑定到 Current 对象的单个属性。实际上,即使列表控件也有 DataBindings 集合,但它使用不多。DataBindings 列表可以在设计器中“(数据绑定)”节点下的“(高级)”行下更改。

提示:在本教程中,我们经常会为 TextBox 的“Text”属性添加绑定。你可以添加到 DataBindings 的其他常见绑定包括

  • CheckBoxRadioButton 的“Checked”属性。
  • ComboBoxListBoxListView 的“SelectedIndex”属性。
  • ComboBoxListBox 的“SelectedValue”属性。
  • 任何控件的“Enable”或“Visible”属性。
  • 各种控件的“Text”属性。

提示:在桌面端,Microsoft 鼓励你使用 DataGridView,它是 DataGrid 的更强大“升级”版。然而,在 .NET Compact Framework 中,只有 DataGrid 可用。

提示ListViewTreeView 的内容无法数据绑定(只能绑定“SelectedIndex”和“Enabled”等简单属性)。然而,CodeProject 上的一些文章提出了克服此限制的方法。

一架载有乘客的飞机

这里的大多数示例都使用基于对象的数据源,也许我个人对数据库有偏见。假设你有一个类,其中包含一些数据

class Airplane
{
    public Airplane(string model, int fuelKg)
    {
        ID = ++lastID; Model = model; _fuelKg = fuelKg;
    }
    private static int lastID = 0;
    public int ID;
    private int _fuelKg;
    public int GetFuelLeftKg() { return _fuelKg; }
    public string Model;
    public List<Passenger> Passengers = new List<Passenger>();
}
class Passenger
{
    public Passenger(string name)
    {
        ID = ++lastID; Name = name;
    }
     private static int lastID = 0;
    public int ID;
    public string Name;
}

嗯,抱歉,但上面的类无法工作。这里没有任何可绑定的东西,因为数据必须以属性的形式提供,而不是方法或字段。而且,我猜测它们必须是公共的非静态属性,尽管我没有检查。所以,让我们再试一次

class Airplane
{
    public Airplane(string model, int fuelKg)
    {
        _id = ++lastID; Model = model; _fuelKg = fuelKg;
    }
    private static int lastID = 0;
    public int _id;
    public int ID { get { return _id; } }
    public int _fuelKg;
    public int FuelLeftKg { get { return _fuelKg; } set { _fuelKg = value; } }
    public string _model;
    public string Model { get { return _model; } set { _model = value; } }
    public List<Passenger> _passengers = new List<Passenger>();
    public List<Passenger> Passengers { get { return _passengers; } }
}
class Passenger
{
     public Passenger(string name)
    {
        _id = ++lastID; Name = name;
    }
     private static int lastID = 0;
    public int _id;
    public int ID { get { return _id; } }
    public string _name;
    public string Name { get { return _name; } set { _name = value; } }
}

好多了。假设你将此放入你的项目中,并且你希望有一个 DataGridView 显示 Airplane 列表。并且,你希望有一个 TextBox,用户可以在其中更改当前选定 AirplaneModel 名称。如何实现?

设计器方法

通常在 Visual Studio 设计器中设置数据绑定。如果你想跟着做,创建一个新的 Windows Forms 项目,并创建一个包含上述 AirplanePassenger 类的 C# 代码文件。然后,在设计器中,将一个 DataGridView(命名为“grid”)和一个 TextBox(命名为“txtModel”)放在你的 Form1 上。如果你选择你的 DataGridView,右上角有一个小箭头,你可以点击它找到一个配置框。从那里,你可以找到“数据源配置向导”(要到达它,点击“选择数据源”,“添加项目数据源”)。

注意:在项目构建之前,向导不会看到 AirplanePassenger

你可以告诉这个向导从一个“Object”获取数据,在第二页上,你可以从树中选择 Airplane。向导将在组件托盘中创建一个 BindingSource 并将其 DataSource 设置为 typeof(Airplane)

向导还为 Airplane 的三个属性创建了三列(DataGridViewTextBoxColumn 对象)。然而,没有乘客列表的列;我猜向导知道你不能在单个单元格中显示复杂对象的列表。(你可以在单元格中显示字符串下拉列表,但我不会在本文中讨论。)

TextBox 的属性中,打开“(数据绑定)”节点,点击“(高级)”行,然后点击“...”。选择 Text 属性,然后打开“绑定”列表,你可以在其中选择 airplaneBindingSourceModel 属性。

然后,点击确定。现在,你只需将一些飞机添加到列表中。这通过将 Airplane 添加到 BindingSource 来完成。所以,创建一个 Form1_Load() 处理程序(双击窗体上的空白区域),并添加一些代码,例如

private void Form1_Load(object sender, EventArgs e)
{
      airplaneBindingSource.Add(new Airplane("Boeing 747", 800));
      airplaneBindingSource.Add(new Airplane("Airbus A380", 1023));
      airplaneBindingSource.Add(new Airplane("Cessna 162", 67));
}

运行程序,你会得到以下结果

提示:请注意,ID 字段是只读的,因此 DataGridView 会自动阻止用户更改它。其他控件则不那么智能。例如,如果 Model 是只读的,用户仍然可以更改 txtModel。为了防止这种情况,你需要(手动)设置 txtModel.ReadOnly = true

一切都很好,但就我个人而言,我喜欢在代码中设置绑定。所以请配合我。让我们重新开始。在设计器中,删除 airplaneBindingSource,控件将再次突然解除绑定。

手动方法

现在,将你的 Form1_Load() 更改为如下所示(差异用 //** 标记)

BindingSource bs = new BindingSource();                              //**

private void Form1_Load(object sender, EventArgs e)
{
    bs.DataSource = typeof(Airplane);                                //**
    bs.Add(new Airplane("Boeing 747", 800));
    bs.Add(new Airplane("Airbus A380", 1023));
    bs.Add(new Airplane("Cessna 162", 67));

    grid.DataSource = bs;                                            //**
    grid.AutoGenerateColumns = true; // create columns automatically //**
    txtModel.DataBindings.Add("Text", bs, "Model");                  //**
}

请注意,由于我们删除了设计器中的 BindingSource,因此我们必须定义自己的 BindingSource。无论如何,运行程序,你应该会得到与之前相同的结果

同样,DataGridView 知道它无法为 Airplane.Passengers 创建一列。

这比使用向导更容易,不是吗?只需多写五行代码,你就不再需要在设计器中设置它。另一方面,设计器可以更容易地自定义列。

我注意到数据绑定一个有趣的地方是它对你做事的顺序相当宽容。你可以按任何顺序将上述语句放在 Form1_Load() 中,程序仍然能完美运行。另一个有趣的地方是,我们不必告诉 BindingSource 它将包含什么类型的对象:你可以删除对 DataSource 的赋值(并且 DataSource 将保持等于 null),但内部,BindingSource 仍然会记录第一个对象是 Airplane 的事实。如果你然后向其中添加一个非 Airplane 对象,它会抛出 InvalidOperationException

顺便说一句,DataSource 是一个奇怪的属性。你可以将其设置为 Airplane 而不是 typeof(Airplane)。你能猜到如果你用这个替换三个 Add() 语句会发生什么吗?

bs.Add(new Airplane("Boeing 747", 800));
bs.DataSource = new Airplane("Airbus A380", 1023);
bs.Add(new Airplane("Cessna 162", 67));

剧透:你最终会得到两架飞机,空客和赛斯纳。这几乎就像你写了

bs.Add(new Airplane("Boeing 747", 800));
bs.Clear();
bs.Add(new Airplane("Airbus A380", 1023));
bs.Add(new Airplane("Cessna 162", 67));

顺便说一句,如果你在代码中修改 txtModel.Text,当前 AirplaneModel 不会更新,至少不会立即更新(未来的事件倾向于以某种方式触发更新)。一种解决方法是更改底层数据(Airplane.Model),然后调用 bs.ResetCurrentItem() 来更新控件。

总之,继续……

它是如何工作的?

不可否认,Model 文本框确实不需要,因为你可以在 DataGridView 上直接编辑 Model 列中的单元格。但是,这个例子表明这两个控件是自动同步的。

  • 如果你更改当前行,文本框会自动显示当前行的模型。
  • 如果你在一个控件上更改模型文本并按下 Tab 键,另一个控件也会更新以匹配。

魔法!这两个控件是如何通信的?幕后发生了什么?BindingSource 实际上是领导者。你可以通过阅读文档来了解一些关于它的信息,文档中说 BindingSource“通过在 Windows Forms 控件和数据源之间提供货币管理、更改通知和其他服务,简化了将窗体上的控件绑定到数据的过程。”货币管理?当我看到这个时,我想知道,“这不是 NumberFormatInfo 负责的吗?”但是,事实证明货币管理与日元和欧元无关。相反,这是微软表示“当前状态”的方式。换句话说,BindingSource 跟踪其 List 中哪个对象是当前对象。在内部,BindingSource 使用 CurrencyManager,它保存对列表的引用并跟踪当前项。

当用户编辑模型名称时,控件会以某种方式修改 BindingSource.Current 对象,然后 BindingSource 会引发 CurrentItemChanged 事件。实际上,单个修改会引发多个事件,如果你想知道是哪些事件,只需将此代码添加到 Form1_Load 中(这是 C# 3.0 语法;在 C# 2.0 中使用匿名委托)

bs.AddingNew          += (s, ev) => Debug.WriteLine("AddingNew");
bs.BindingComplete    += (s, ev) => Debug.WriteLine("BindingComplete");
bs.CurrentChanged     += (s, ev) => Debug.WriteLine("CurrentChanged");
bs.CurrentItemChanged += (s, ev) => Debug.WriteLine("CurrentItemChanged");
bs.DataError          += (s, ev) => Debug.WriteLine("DataError");
bs.DataMemberChanged  += (s, ev) => Debug.WriteLine("DataMemberChanged");
bs.DataSourceChanged  += (s, ev) => Debug.WriteLine("DataSourceChanged");
bs.ListChanged        += (s, ev) => Debug.WriteLine("ListChanged");
bs.PositionChanged    += (s, ev) => Debug.WriteLine("PositionChanged");

但是,控件如何将对其 Text 属性的更改通知 BindingSource 呢?请记住,从控件的角度来看,DataSource 只是一个 Object。另外,我想知道:控件是否专门针对 BindingSource 提供某种特殊支持,或者只要它们实现某个接口,它们就会接受其他类?BindingSource 是否专门针对 DataSet 提供某种特殊支持,或者它只是寻找某些方法/属性?换句话说,数据绑定是基于鸭子类型,还是专门针对某些类或接口处理的?一般来说,数据源期望什么?

嗯,通过使用 Visual Studio 2008 并遵循这些说明,你可以跟踪 .NET Framework 源代码。并且,有一个特殊工具,你可以用它在其他版本的 Visual Studio 中做同样的事情,此外,它下载的是完整的源代码,而不仅仅是你需要的部分。也可以通过使用 Reflector 和 FileDisassembler 插件反汇编代码来检查代码,但这种方法不允许你跟踪代码。

不幸的是,这是一个非常庞大和复杂的代码。经过几个小时,我弄清楚了控件如何通知 BindingSourceText 属性已更改,以及 DataGrid 如何被通知。我将现在解释,但解释是如此复杂,你可能不想听。请随意跳过几段。

长话短说,事实证明

  • 你通过 txtModel.DataBindings.Add() 添加的 Binding 附加了 TextBoxTextChangedValidating 事件的处理程序。
  • 后一个处理程序 (Binding.Target_Validate) 通过几个内部类,BindToObjectReflectPropertyDescriptor 传递新值,其中 ReflectPropertyDescriptor 使用反射实际更改 Airplane 中的值,然后调用其基类 PropertyDescriptor 中的 base.OnValueChanged
  • OnValueChanged 调用一个与指向 BindingSource.ListItem_PropertyChanged 的同一 Airplane 相关联的委托。
  • 此处理程序引发其 ListChanged 事件(带有一个指定更改项索引的 ListChangedEventArgs)。
  • CurrencyManager.List_ListChanged 附加到该事件。此处理程序反过来引发其自己的 ItemChanged 事件,而 BindingSource.CurrencyManager_CurrentItemChanged 附加到该事件。
  • 此事件处理程序仅引发 BindingSource.CurrentItemChanged 事件,该事件调用我们的 WriteLine("CurrentItemChanged") 处理程序。
  • 接下来,CurrencyManager.List_ListChanged 引发其自己的 ListChanged 事件。
  • DataGridView 的内部嵌套类有一个处理程序 (DataGridViewDataConnection.currencyManager_ListChanged),它通过刷新已更改的行来响应该事件。
  • 最后,currencyManager_ListChanged 引发 DataBindingComplete 事件,但无人处理。

呼!这太复杂了。现在,BindingSource 是如何将事件处理程序与 TextBoxDataBindings 集合中 Binding 相关联的 PropertyDescriptor 中的 Airplane 关联起来的呢?嗯,

  • 当你将新的 Binding 添加到 DataBindings 集合时,Binding.SetBindableComponent() 会获得对控件的引用,并且控件有一个 BindingContext,它是一种绑定集合(显然与 DataBindings 集合类似但不同)。所以,
  • Binding(我们称之为“b”)将自己传递给 BindingContext.UpdateBinding()。文档说此方法“将 Binding 与新的 BindingContext 关联”。但这只有在非常间接的情况下才成立。首先,
  • UpdateBinding() 调用 BindingContext.EnsureListManager(),它注意到 BindingDataSource(请记住,这是一个 BindingSource)实现了 ICurrencyManagerProvider,因此它调用 ICurrencyManagerProvider.GetRelatedCurrencyManager(dataMember)(其中 dataMemberBinding 关联,在本例中是一个空字符串)。这就是我正在寻找的神奇部分——DataSource 被视为不仅仅是一个 Object 的部分。在这里,我们看到 .NET 框架寻找一个特殊的接口,而不是使用鸭子类型(基于反射的调用)。反射仅用于学习 Airplane 的属性。
  • 此时,BindingSource 有机会返回其自己的内部 CurrencyManager 对象(我们称之为“c”),该对象连接到 BindingSource
  • 然而,现在我头脑爆炸了,因为 UpdateBinding() 既没有将 b 添加到 BindingContext,也没有将 CurrencyManager 分配给 b。相反,b 通过调用 c.Bindings.Add(b) 被“添加”到 CurrencyManager 中。
  • c.Bindings 的类型是 ListManagerBindingsCollection(一个内部类)。c.Bindings.Add(b) 调用一个方法,该方法调用 b.SetListManager(c)。通过这种方式,BindingSourceCurrencyManager 最终与 Binding b 及其对应的 BindToObject 对象关联。因此,当用户更改 TextBoxText 并离开焦点时,bBindToObject 可以访问 CurrencyManager,通过它获取一个 ReflectPropertyDescriptor(存储在 BindToObject.fieldInfo 中)。反过来,ReflectPropertyDescriptor 是在我们的 Form1_Load() 方法中第一次调用 bs.Add() 期间由 ListBindingHelper.GetListItemProperties() 代表 BindingSource 创建的。这个 ReflectPropertyDescriptor 包含从我们的 AirplaneBindingSource.ListItem_PropertyChanged 的映射,以便 BindingSource 可以收到 Airplane 更改的通知。

我个人觉得这种架构非常不直观。而且,上面这个发现尤其困难,因为 BCL (Base Class Library) 是 JIT 优化的,这意味着一些函数被内联(在调用堆栈中缺失),并且大多数变量在调试器中无法看到。此外,Intellisense 在其中不起作用。此外,大部分代码缺乏注释。但是,我至少能够确定 BindingContext(请记住,每个控件都有一个 BindingContext)对于实现 ICurrencyManagerProviderBindingSource 实现)、IListIListSource 的数据源具有特殊情况代码,因此你可以期望你的数据源必须实现这些接口之一才能充当列表。

至于 BindingSource,它会创建一个类型为 BindingList<T>List 属性,其中 T 是你提供给它的数据类型(例如,BindingList<Airplane>)。它似乎没有为 DataSet 进行特殊处理,尽管某些代码针对某些接口进行了特殊处理,例如

  • 如果 T 实现 INotifyPropertyChangedBindingList 将订阅其 PropertyChanged 事件。
  • BindingSource 似乎与 CurrencyManager 密切配合,后者保存着一个 IList 以及该列表的当前位置。CurrencyManager 具有一些针对实现 IBindingListITypedList 和/或 ICancelAddNew 的列表,以及针对实现 IEditableObject 的列表项的特殊情况代码。

不幸的是,整个绑定架构似乎与自身紧密耦合(即,类之间存在许多引用和关系),因此很难在源代码级别进行跟踪。因此,我建议尝试通过实验和文档来弄清楚。不过,如果能用几个 UML 图来展现这一切,那就太好了。

数据绑定还能做什么?

首先,你可以绑定到 DataSource 中的列表。尝试这个新的 Form1_Load() 处理程序

BindingSource bs = new BindingSource();

private void Form1_Load(object sender, EventArgs e)
{
    Airplane a = new Airplane("Boeing 747", 800);
    bs.DataMember = "Passengers";
    bs.DataSource = a;
    bs.Add(new Passenger("Joe Shmuck"));
    a.Passengers.Add(new Passenger("Jack B. Nimble")); // this happens to work also
    bs.Add(new Passenger("Jane Doe"));
    bs.Add(new Passenger("John Smith"));

    grid.DataSource = bs;
    grid.AutoGenerateColumns = true;
    txtModel.DataBindings.Add("Text", bs, "Name");
    label1.Text = "Name:";
}

与上次不同,这次我们设置了 DataMember 属性。现在,你会得到一个单个 Airplane 的乘客列表,而不是 Airplane 列表

提示:如果你想显示一个 ADO.NET 表,只需将 Airplane 替换为你的 DataSet,并将“Passengers”替换为该 DataSetDataTable 的名称。

请注意,我们可以直接将项目添加到 BindingSourcebs)或 a.Passengers。但是,直接更改 a.Passengers 并不总是有效,如果你将此代码添加到 Form1_Load() 的末尾,你就会发现

a.Passengers.Insert(0, new Passenger("Oops 1"));

EventHandler eh = null;
Application.Idle += (eh = delegate(object s, EventArgs e2) {
    // Window is now visible
    a.Passengers.Insert(0, new Passenger("Oops 2"));
    Application.Idle -= eh;
});

列表中仍然只有四个项目。起初,你会在顶部看到错误 1;当你将鼠标移到网格上时,错误 2会突然出现。它无法正常工作,因为 BindingSource 没有收到更改的通知。如果你必须修改底层列表,可以通过调用 BindingSource.ResetBindings(false) 来刷新屏幕上的列表。

不使用 BindingSource 进行绑定

你可以直接绑定到一个对象,而无需使用 BindingSource。对于此示例(仅此示例),向窗体添加一个 Buttonbutton1)。运行时,它将如下所示

在此示例中,你可以编辑 Airplane.PassengersAirplane.Model

要进行设置,双击 button1,然后用以下代码替换 Form1_Loadbutton1_Click

Airplane a = new Airplane("Boeing 747", 800);

private void Form1_Load(object sender, EventArgs e)
{
    a.Passengers.Add(new Passenger("Joe Shmuck"));
    a.Passengers.Add(new Passenger("Jack B. Nimble"));
    a.Passengers.Add(new Passenger("Jane Doe"));
    a.Passengers.Add(new Passenger("John Smith"));

    grid.DataSource = a;
    grid.DataMember = "Passengers";
    grid.AutoGenerateColumns = true;
    txtModel.DataBindings.Add("Text", a, "Model");
}

private void button1_Click(object sender, EventArgs e)
{
    string msg = string.Format(
        "The last passenger on this {0} is named {1}. Add another passenger?", 
        a.Model, a.Passengers[a.Passengers.Count-1].Name);
    if (MessageBox.Show(msg, "", MessageBoxButtons.YesNo) == DialogResult.Yes) {
        a.Passengers.Add(new Passenger("New Passenger"));
        grid.ResetBindings();
    }
}

button1 演示了两件事

  • 当用户更改网格或文本框时,底层数据源仍会按预期更改。
  • 如果你向底层数据源添加一行,你必须在网格上调用 ResetBindings()(向 BindingSource 添加行时不需要)。

这个例子看起来运行得很好,那为什么还要使用 BindingSource 呢?

  • BindingSource 自动同步显示相同数据的多个控件之间的数据。这个例子之所以有效,是因为两个控件上的数据是独立的。
  • 当你从 BindingSourceList 中添加或删除项时,它会自动刷新绑定的控件。
  • BindingSources 可以串联起来(如下一节所述)。
  • 当你修改模型时,会发生一些奇怪的事情:网格的 CurrentRow.Index 变为零。我不知道为什么,但我怀疑当你使用 BindingSource 时不会发生这种情况。

如果 DataSetDataTable 用作 DataSource,我听说它们提供了一些,但不是全部这些功能(我不知道细节)。

注意:我们不再使用按钮,所以你现在可以删除它。

分层数据绑定

你可以在 ListBoxComboBox 中显示记录中的列表。假设你想在名为“lstPassengers”的新列表中显示选定飞机上的乘客,以便你的窗口看起来像这样

你可以使用以下代码来完成此操作

BindingSource bs = new BindingSource();

private void Form1_Load(object sender, EventArgs e)
{
    // Create some example data.
    Airplane a1, a2, a3;
    bs.Add(a1 = new Airplane("Boeing 747", 800));
    bs.Add(a2 = new Airplane("Airbus A380", 1023));
    bs.Add(a3 = new Airplane("Cessna 162", 67));
    a1.Passengers.Add(new Passenger("Joe Shmuck"));
    a1.Passengers.Add(new Passenger("Jack B. Nimble"));
    a1.Passengers.Add(new Passenger("Jib Jab"));
    a2.Passengers.Add(new Passenger("Jackie Tyler"));
    a2.Passengers.Add(new Passenger("Jane Doe"));
    a3.Passengers.Add(new Passenger("John Smith"));
    
    // Set up data binding
    grid.DataSource = bs;
    grid.AutoGenerateColumns = true;
    lstPassengers.DataSource = bs;
    lstPassengers.DisplayMember = "Passengers.Name";
    txtModel.DataBindings.Add("Text", bs, "Model");
}

在这里,我们告诉 ListBox 使用点分隔的符号填充所有 PassengerName 属性。(我不确定你是否可以在其他情况下使用点分隔的名称)。

但是,如果你想使用数据绑定和 TextBox,以便用户可以更改乘客姓名怎么办?

设计时

运行时

这种数据绑定称为主从。为此,你需要两个 BindingSource,因为一个 BindingSource 只有一个 CurrencyManager,所以它只能跟踪一个“当前记录”。在这里,你需要两个,因为你希望 txtModel 绑定到当前 AirplanetxtName 绑定到当前 Passenger。幸运的是,解决方案很简单,因为你可以将 BindingSource 链接在一起

BindingSource bsA = new BindingSource(); // Airplanes
BindingSource bsP = new BindingSource(); // Passengers

private void Form1_Load(object sender, EventArgs e)
{
    // Create some example data.
    Airplane a1, a2, a3;
    bsA.Add(a1 = new Airplane("Boeing 747", 800));
    bsA.Add(a2 = new Airplane("Airbus A380", 1023));
    bsA.Add(a3 = new Airplane("Cessna 162", 67));
    a1.Passengers.Add(new Passenger("Joe Shmuck"));
    a1.Passengers.Add(new Passenger("Jack B. Nimble"));
    a1.Passengers.Add(new Passenger("Jib Jab"));
    a2.Passengers.Add(new Passenger("Jackie Tyler"));
    a2.Passengers.Add(new Passenger("Jane Doe"));
    a3.Passengers.Add(new Passenger("John Smith"));

    // Set up data binding for the parent Airplanes
    grid.DataSource = bsA;
    grid.AutoGenerateColumns = true;
    txtModel.DataBindings.Add("Text", bsA, "Model");

    // Set up data binding for the child Passengers
    bsP.DataSource = bsA; // chaining bsP to bsA
    bsP.DataMember = "Passengers";
    lstPassengers.DataSource = bsP;
    lstPassengers.DisplayMember = "Name";
    txtName.DataBindings.Add("Text", bsP, "Name");
}

就这样,它完美运行。

DataGridView 可能无法提供添加新行的方式*,即使其 AllowUserToAddRows 属性默认值为 true。这是因为 BindingSource 的列表 (BindingList<Airplane>) 有一个属性,该属性也必须为 true 才能添加行。如果你在 Form1_Load 的末尾添加以下行,DataGridView 将提供一个添加行的界面

((BindingList<Airplane>)bsA.List).AllowNew = true;

同样,你可以让用户删除行

((BindingList<Airplane>)bsA.List).AllowRemove = true;

但是,这种用户界面并不直观。用户必须通过单击行左侧的小矩形来选择整行,然后按下 Delete 键。无法仅使用鼠标或仅使用键盘删除行。

* 我很困惑。第一次运行此示例时,AllowNew 默认值为 false。当天晚些时候,我再次尝试了相同的代码(我以为是),但 AllowNew 默认值为 true。搞不懂。

注意:示例项目在 Form1.cs 中包含此示例。

使用 DataSet 进行分层数据绑定

随着场景变得越来越复杂,你越有可能想要使用数据库,或者至少是一个 DataSet

以下示例与上一个示例类似,但使用 DataSet 而不是 AirplanePassenger 类。我手动构建了 DataSet 的架构,以省去你设置数据库或类型化 DataSet 的麻烦。只需将此方法复制并粘贴到 Form1

DataSet CreateAirplaneSchema()
{
    DataSet ds = new DataSet();
    
    // Create Airplane table
    DataTable airplanes = ds.Tables.Add("Airplane");
    DataColumn a_id = airplanes.Columns.Add("ID", typeof(int));
    airplanes.Columns.Add("Model", typeof(string));
    airplanes.Columns.Add("FuelLeftKg", typeof(int));
    a_id.AutoIncrement = true;
    a_id.AutoIncrementSeed = 1;
    a_id.AutoIncrementStep = 1;

    // Create Passengers table
    DataTable passengers = ds.Tables.Add("Passenger");
    DataColumn p_id = passengers.Columns.Add("ID", typeof(int));
    passengers.Columns.Add("AirplaneID", typeof(int));
    passengers.Columns.Add("Name", typeof(string));
    p_id.AutoIncrement = true;
    p_id.AutoIncrementSeed = 1;
    p_id.AutoIncrementStep = 1;

    // Create parent-child relationship
    DataRelation relation = ds.Relations.Add("Airplane_Passengers", 
        airplanes.Columns["ID"], 
        passengers.Columns["AirplaneID"], true);

    return ds;
}

并使用以下 Form1_Load() 代码(与上一个示例相比,更改的行用 //** 标记)

BindingSource bsA = new BindingSource(); // Airplanes
BindingSource bsP = new BindingSource(); // Passengers

private void Form1_Load(object sender, EventArgs e)
{
    // Create DataSet and connect it to the BindingSources  //**
    DataSet ds = CreateAirplaneSchema();                    //** 
    DataTable airplanes = ds.Tables["Airplane"];            //** 
    DataTable passengers = ds.Tables["Passenger"];          //** 
    bsA.DataSource = ds;                                    //** 
    bsP.DataSource = ds;                                    //** 
    bsA.DataMember = airplanes.TableName;                   //** 
    bsP.DataMember = passengers.TableName;                  //** 
 
    // Create some example data in the DataSet.             //** 
    DataRow a1, a2, a3;                                     //** 
    a1 = airplanes.Rows.Add(null, "Boeing 747", 800);       //** 
    a2 = airplanes.Rows.Add(null, "Airbus A380", 1023);     //** 
    a3 = airplanes.Rows.Add(null, "Cessna 162", 67);        //** 
    passengers.Rows.Add(null, a1["ID"], "Joe Shmuck");      //** 
    passengers.Rows.Add(null, a1["ID"], "Jack B. Nimble");  //** 
    passengers.Rows.Add(null, a1["ID"], "Jib Jab");         //** 
    passengers.Rows.Add(null, a2["ID"], "Jackie Tyler");    //** 
    passengers.Rows.Add(null, a2["ID"], "Jane Doe");        //** 
    passengers.Rows.Add(null, a3["ID"], "John Smith");      //** 

    // Set up data binding for the parent Airplanes
    grid.DataSource = bsA;
    grid.AutoGenerateColumns = true;
    txtModel.DataBindings.Add("Text", bsA, "Model");

    // Set up data binding for the child Passengers
    bsP.DataSource = bsA; // chaining bsP to bsA
    bsP.DataMember = "Airplane_Passengers";                 //** 
    lstPassengers.DataSource = bsP;
    lstPassengers.DisplayMember = "Name";
    txtName.DataBindings.Add("Text", bsP, "Name");
}

当你运行程序时,它的行为与之前相同,只是用户可以默认对列表进行排序、添加新行和删除行

(如果你认为用户不应该能够添加行,只需将 gridAllowUserToAddRows 属性设置为 false。趁你还在设置,你可能还想更改 AllowUserToDeleteRowsAllowUserToOrderColumnsAllowUserToResizeColumnsAllowUserToResizeRows。但我跑题了:这不是 DataGridView 教程。)

当没有当前项时进行绑定

通常,如果你的 DataSource 是一个 DataSetbsP.Current 会指向一个 DataRowView;在前面的对象示例中,它通常指向一个 Passenger。但是,当你创建新行时,它没有乘客,因此 bsP.Currentnull。我没有看到任何关于绑定架构在没有“当前”项时应该如何表现的文档,但至少该架构足够智能,可以清除“名称”文本框。然而,你可能会注意到用户仍然可以更改名称(但没有效果)。

如果你想在没有乘客时禁用文本框,可以为绑定源的 ListChanged 事件添加一个处理程序

...
...
private void Form1_Load(object sender, EventArgs e)
{
    bsP.ListChanged += new ListChangedEventHandler(bsP_ListChanged);   //** 

    ... // same as before
}

void bsP_ListChanged(object sender, ListChangedEventArgs e)            //** 
{                                                                      //** 
    // ListChangedType.Reset indicates that the entire list changed.   //** 
    // ListChanged is also raised when rows/columns are added/removed. //** 
    if (e.ListChangedType == ListChangedType.Reset)                    //** 
        txtName.Enabled = bsP.Current != null;                         //** 
}                                                                      //** 

我不知道这是否是最简单的解决方案,但它运行良好。

筛选

在“真实世界”的应用程序中,可能需要显示数百行。如果你想根据用户提供的某些条件筛选列表怎么办?

BindingSource 提供了一个“Filter”属性,允许你指定一个布尔表达式来控制绑定控件上显示的行。但是,BindingSource 本身并评估此表达式;它只是将其传递给底层 List,该 List 必须实现 IBindingListView。在我们的基于对象的示例中,Airplane 列表是 BindingList<Airplane>Passenger 列表是 List<Passenger>。这些类都不实现 IBindingListView,因此该示例无法使用筛选(尽管可以使用开源库 BindingListView 添加筛选和排序支持。)

但是,你可以筛选 DataTableDataViewDataTable 本身不实现 IBindingListView,但其 DefaultView,由 IListSource.GetList() 返回,实现了)。

为了演示基于 DataSet 的筛选,我创建了一个新的 FormForm2,基于上面的分层 DataSet 示例。然后,我添加了两个新的 TextBoxtxtAirplaneFiltertxtPassengerFilter(和一些标签),得到了这个

接下来,我为 TextBoxes 添加了以下 TextChanged 事件处理程序

void txtAirplaneFilter_TextChanged(object sender, EventArgs e)
{
    try {
        bsA.Filter = txtAirplaneFilter.Text;
        txtAirplaneFilter.BackColor = SystemColors.Window;
    } catch(InvalidExpressionException) {
        txtAirplaneFilter.BackColor = Color.Pink;
    }
}

private void txtPassengerFilter_TextChanged(object sender, EventArgs e)
{
    try {
        bsP.Filter = txtPassengerFilter.Text;
        txtPassengerFilter.BackColor = SystemColors.Window;
    } catch(InvalidExpressionException) {
        txtPassengerFilter.BackColor = Color.Pink;
    }
}

这是它在实际应用中的样子

注意:示例项目在 Form2.cs 中包含此示例。

如屏幕截图所示,DataSet 支持 SQL 风格的筛选表达式语法。如果表达式无法理解,TextBox 将具有粉色背景。顺便说一句,将筛选器设置为空字符串会清除筛选器,这几乎等同于你在 BindingSource 上调用 RemoveFilter()。而且,DataSet.CaseSensitive 属性控制字符串测试是否区分大小写。

提示:如果你有两个列表绑定到不同的 BindingSource,但每个 BindingSource 都附加到同一个 DataTable,那么这两个列表共享同一个筛选器(至少我听说过是这样)。要给它们独立的筛选器,创建两个附加到 DataTable(通过 Table 属性)的 DataView,并将每个 BindingSourceDataSource 设置为不同的 DataView

子字符串筛选器

通常,你不会让用户输入完整的筛选表达式。相反,你可能会显示包含用户输入子字符串的所有记录。这该如何实现?如果能使用委托作为筛选器,那将很棒,但 DataView 不支持它。你必须使用筛选字符串提供的运算符,而最接近子字符串搜索的是“LIKE”运算符。筛选字符串的一个明显选择是

// assuming bs is your BindingSource and txt is a TextBox
bs.Filter = string.Format("Name like '*{0}*'", txt.Text);

不幸的是,这可能无法按用户预期的方式工作,例如,如果用户在他的筛选字符串中放置了撇号。我提供以下转义例程来帮助你

static string EscapeSqlLike(string s_)
{
    StringBuilder s = new StringBuilder(s_);
    for (int i = 0; i < s.Length; i++) {
        if (s[i] == '\'') {
            s.Insert(i++, '\'');
            continue;
        }
        if (s[i] == '[' || s[i] == '*' || s[i] == '?') {
            s.Insert(i++, '[');
            s.Insert(++i, ']');
        }
    }
    return s.ToString();
}

然后,你可以像这样构造一个子字符串过滤器

// assuming bs is your BindingSource and txt is a TextBox
bs.Filter = string.Format("Name like '*{0}*'", EscapeSqlLike(txt.Text));

数据绑定不能做什么?

  • 你无法显示原始数据源中不存在的计算属性。例如,上面的网格有一个 FuelLeftKg 列;我认为不可能在不向底层数据源添加新的 FuelLeftLbs 属性的情况下以磅为单位显示相同的字段。ADO.NET 的 DataColumn 通过其“Expression”属性支持计算属性,但我假设此类列必须是只读的。但是,DataGridView 可以包含你手动设置的“未绑定”列
  • 当用对象列表填充时(如上面几个示例所示),BindingSource.List 是一个 BindingList<T>,它支持排序,也不支持通过 PropertyDescriptor 进行搜索。如果你想支持排序或搜索,一种方法是创建一个派生自 BindingList<T> 的类,并覆盖其名称以“Core”结尾的相关方法和属性(对于排序,请参阅 BindingList.ApplySortCore 的文档以获取详细信息)。然后,将你的自定义列表分配给 bs.DataSource(其中 bs 是你的 BindingSource)。

我不能用数据绑定做什么?

有些事情我仍然不知道怎么做。

  • 也许,我想要一个取消按钮,可以中止对记录的所有更改。或者,也许除非点击“保存”按钮,否则不应保留更改。如何实现?
  • 也许,我想让用户同时编辑多条记录,利用 DataGridViewListBox 和其他可绑定控件的多选功能。例如,我如何让用户选择多行,然后同时设置多架飞机(为相同的字符串)的型号?

相关文章

© . All rights reserved.