详细数据绑定教程
通过几个简单的示例,演示了多种 Windows Forms 数据绑定功能。
- 下载源代码 - 29.8 KB
(这是一个 Visual Studio 2008 项目。它包含本文中的两个“主从”示例。)
引言
Windows Forms 数据绑定的文档相当稀疏。它究竟是如何工作的?你能用它做多少事情?很多人知道如何使用数据绑定,但可能很少有人真正理解它。我仅仅是弄清楚如何使用它就已经很费劲了,所以我开始调查它,希望理解这个神秘的“野兽”。
我相信“数据绑定”传统上指的是控件和数据库行或表之间的自动同步。在 .NET Framework(和 Compact Framework 2.0)中,你仍然可以这样做,但这个概念已经扩展到其他场景,以至于你可以将任何控件的几乎任何属性绑定到几乎任何对象。
System.Windows.Forms.BindingSource
是 .NET Framework 2.0 中的新功能。我的印象是 Microsoft 希望我们使用 BindingSource
而不是旧的类,例如 CurrencyManager
和 BindingContext
,所以本文只会帮助你使用 BindingSource
。
数据绑定可以使用反射,因此你不仅限于 ADO.NET DataSet
中的数据库表和行;相反,几乎任何具有属性的对象都可以工作。例如,如果你的选项存储在一个普通的 .NET 对象中,数据绑定将有助于实现一个“选项”对话框。
这不是 ADO.NET/DataSet
或 DataGridView
的教程,但请参阅相关文章。
注意:本文假定你精通 C#(也可能精通 ADO.NET),但对数据绑定一无所知。
免责声明:我稍微谈论了 .NET Compact Framework 对数据绑定的支持,但我不知道这里描述的一切是否都可以在该平台上实现。
目录
数据绑定 API 简介
请注意以下几点:
Control.DataBindings
集合包含Binding
对象,每个对象都有一个类型为Object
的DataSource
属性。ListBox
、DataGridView
等的DataSource
属性类型为Object
。BindingSource
类也有一个类型为Object
的DataSource
。
那么,这些对象必须是什么呢?我发现关于这个主题的文档相当令人困惑,这就是为什么会编写此类教程的原因。在实际应用中,你可能会使用 BindingSource
对象作为列表控件和 Binding
的 DataSource
属性。如果你使用数据库,你的 BindingSource
的 DataSource
属性通常是一个 DataSet
;否则,它可能是你的应用程序中某个类的对象。
数据绑定似乎有多种不同的方法,但我无法在任何地方找到详细说明。因此,我做了一些实验,学习了几种可以做的事情。
让我们从典型情况开始:你将一个 BindingSource
对象分配给控件的 DataSource
。你可以将 BindingSource
视为一个“二合一”数据源。它具有
- 一个名为
Current
对象的单个对象。Control
的属性可以绑定到Current
的属性。 - 一个实现
IList
的List
对象。该列表应包含与Current
对象类型相同的对象。List
是一个只读属性,它要么返回BindingSource
的“内部列表”(如果未设置DataMember
字符串),要么返回在设置DataMember
时使用的“外部列表”。Current
始终是List
的成员(或null
)。当你将DataSource
设置为单个(非列表)对象时,你的List
只包含该一个项。
数据绑定在不同类型的控件之间工作方式不同。
ComboBox
和ListBox
通过它们的DataSource
和DisplayMember
属性绑定到List
。你通常将DataSource
设置为BindingSource
,并将DisplayMember
设置为Current
对象的属性之一的名称。DataGrid
和DataGridView
通过它们的DataSource
属性绑定到List
。DataGrid
和DataGridView
没有DisplayMember
属性,因为它们可以显示数据源中的多个属性(每列一个),而不仅仅是一个。DataGridView
还有一个名为DataMember
的附加属性,它似乎类似于BindingSource
的DataMember
属性。通常情况下,你不会将网格的DataMember
设置为任何值,除非其DataSource
不是BindingSource
(如果你使用BindingSource
,则应设置BindingSource.DataMember
。)- “简单”控件,例如
TextBox
、Button
、CheckBox
等,通过Control.DataBindings
集合绑定到Current
对象的单个属性。实际上,即使列表控件也有DataBindings
集合,但它使用不多。DataBindings
列表可以在设计器中“(数据绑定)”节点下的“(高级)”行下更改。
提示:在本教程中,我们经常会为 TextBox
的“Text
”属性添加绑定。你可以添加到 DataBindings
的其他常见绑定包括
CheckBox
和RadioButton
的“Checked
”属性。ComboBox
、ListBox
或ListView
的“SelectedIndex
”属性。ComboBox
或ListBox
的“SelectedValue
”属性。- 任何控件的“
Enable
”或“Visible
”属性。 - 各种控件的“
Text
”属性。
提示:在桌面端,Microsoft 鼓励你使用 DataGridView
,它是 DataGrid
的更强大“升级”版。然而,在 .NET Compact Framework 中,只有 DataGrid
可用。
提示:ListView
和 TreeView
的内容无法数据绑定(只能绑定“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
,用户可以在其中更改当前选定 Airplane
的 Model
名称。如何实现?
设计器方法
通常在 Visual Studio 设计器中设置数据绑定。如果你想跟着做,创建一个新的 Windows Forms 项目,并创建一个包含上述 Airplane
和 Passenger
类的 C# 代码文件。然后,在设计器中,将一个 DataGridView
(命名为“grid
”)和一个 TextBox
(命名为“txtModel
”)放在你的 Form1
上。如果你选择你的 DataGridView
,右上角有一个小箭头,你可以点击它找到一个配置框。从那里,你可以找到“数据源配置向导”(要到达它,点击“选择数据源”,“添加项目数据源”)。
注意:在项目构建之前,向导不会看到 Airplane
和 Passenger
。
你可以告诉这个向导从一个“Object
”获取数据,在第二页上,你可以从树中选择 Airplane
。向导将在组件托盘中创建一个 BindingSource
并将其 DataSource
设置为 typeof(Airplane)
向导还为 Airplane
的三个属性创建了三列(DataGridViewTextBoxColumn
对象)。然而,没有乘客列表的列;我猜向导知道你不能在单个单元格中显示复杂对象的列表。(你可以在单元格中显示字符串下拉列表,但我不会在本文中讨论。)
在 TextBox
的属性中,打开“(数据绑定)”节点,点击“(高级)”行,然后点击“...”。选择 Text
属性,然后打开“绑定”列表,你可以在其中选择 airplaneBindingSource
的 Model
属性。
然后,点击确定。现在,你只需将一些飞机添加到列表中。这通过将 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
,当前 Airplane
的 Model
不会更新,至少不会立即更新(未来的事件倾向于以某种方式触发更新)。一种解决方法是更改底层数据(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 插件反汇编代码来检查代码,但这种方法不允许你跟踪代码。
不幸的是,这是一个非常庞大和复杂的代码。经过几个小时,我弄清楚了控件如何通知 BindingSource
其 Text
属性已更改,以及 DataGrid
如何被通知。我将现在解释,但解释是如此复杂,你可能不想听。请随意跳过几段。
长话短说,事实证明
- 你通过
txtModel.DataBindings.Add()
添加的Binding
附加了TextBox
的TextChanged
和Validating
事件的处理程序。 - 后一个处理程序 (
Binding.Target_Validate
) 通过几个内部类,BindToObject
和ReflectPropertyDescriptor
传递新值,其中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
是如何将事件处理程序与 TextBox
的 DataBindings
集合中 Binding
相关联的 PropertyDescriptor
中的 Airplane
关联起来的呢?嗯,
- 当你将新的
Binding
添加到DataBindings
集合时,Binding.SetBindableComponent()
会获得对控件的引用,并且控件有一个BindingContext
,它是一种绑定集合(显然与DataBindings
集合类似但不同)。所以, Binding
(我们称之为“b
”)将自己传递给BindingContext.UpdateBinding()
。文档说此方法“将Binding
与新的BindingContext
关联”。但这只有在非常间接的情况下才成立。首先,UpdateBinding()
调用BindingContext.EnsureListManager()
,它注意到Binding
的DataSource
(请记住,这是一个BindingSource
)实现了ICurrencyManagerProvider
,因此它调用ICurrencyManagerProvider.GetRelatedCurrencyManager(dataMember)
(其中dataMember
与Binding
关联,在本例中是一个空字符串)。这就是我正在寻找的神奇部分——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)
。通过这种方式,BindingSource
的CurrencyManager
最终与Binding
b
及其对应的BindToObject
对象关联。因此,当用户更改TextBox
的Text
并离开焦点时,b
的BindToObject
可以访问CurrencyManager
,通过它获取一个ReflectPropertyDescriptor
(存储在BindToObject.fieldInfo
中)。反过来,ReflectPropertyDescriptor
是在我们的Form1_Load()
方法中第一次调用bs.Add()
期间由ListBindingHelper.GetListItemProperties()
代表BindingSource
创建的。这个ReflectPropertyDescriptor
包含从我们的Airplane
到BindingSource.ListItem_PropertyChanged
的映射,以便BindingSource
可以收到Airplane
更改的通知。
我个人觉得这种架构非常不直观。而且,上面这个发现尤其困难,因为 BCL (Base Class Library) 是 JIT 优化的,这意味着一些函数被内联(在调用堆栈中缺失),并且大多数变量在调试器中无法看到。此外,Intellisense 在其中不起作用。此外,大部分代码缺乏注释。但是,我至少能够确定 BindingContext
(请记住,每个控件都有一个 BindingContext
)对于实现 ICurrencyManagerProvider
(BindingSource
实现)、IList
和 IListSource
的数据源具有特殊情况代码,因此你可以期望你的数据源必须实现这些接口之一才能充当列表。
至于 BindingSource
,它会创建一个类型为 BindingList<T>
的 List
属性,其中 T
是你提供给它的数据类型(例如,BindingList<Airplane>
)。它似乎没有为 DataSet
进行特殊处理,尽管某些代码针对某些接口进行了特殊处理,例如
- 如果
T
实现INotifyPropertyChanged
,BindingList
将订阅其PropertyChanged
事件。 BindingSource
似乎与CurrencyManager
密切配合,后者保存着一个IList
以及该列表的当前位置。CurrencyManager
具有一些针对实现IBindingList
、ITypedList
和/或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”替换为该 DataSet
中 DataTable
的名称。
请注意,我们可以直接将项目添加到 BindingSource
(bs
)或 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
。对于此示例(仅此示例),向窗体添加一个 Button
(button1
)。运行时,它将如下所示
在此示例中,你可以编辑 Airplane.Passengers
和 Airplane.Model
。
要进行设置,双击 button1,然后用以下代码替换 Form1_Load
和 button1_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
自动同步显示相同数据的多个控件之间的数据。这个例子之所以有效,是因为两个控件上的数据是独立的。- 当你从
BindingSource
的List
中添加或删除项时,它会自动刷新绑定的控件。 BindingSource
s 可以串联起来(如下一节所述)。- 当你修改模型时,会发生一些奇怪的事情:网格的
CurrentRow.Index
变为零。我不知道为什么,但我怀疑当你使用BindingSource
时不会发生这种情况。
如果 DataSet
或 DataTable
用作 DataSource
,我听说它们提供了一些,但不是全部这些功能(我不知道细节)。
注意:我们不再使用按钮,所以你现在可以删除它。
分层数据绑定
你可以在 ListBox
或 ComboBox
中显示记录中的列表。假设你想在名为“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
使用点分隔的符号填充所有 Passenger
的 Name
属性。(我不确定你是否可以在其他情况下使用点分隔的名称)。
但是,如果你想使用数据绑定和 TextBox
,以便用户可以更改乘客姓名怎么办?
这种数据绑定称为主从。为此,你需要两个 BindingSource
,因为一个 BindingSource
只有一个 CurrencyManager
,所以它只能跟踪一个“当前记录”。在这里,你需要两个,因为你希望 txtModel
绑定到当前 Airplane
,txtName
绑定到当前 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
而不是 Airplane
和 Passenger
类。我手动构建了 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");
}
当你运行程序时,它的行为与之前相同,只是用户可以默认对列表进行排序、添加新行和删除行
(如果你认为用户不应该能够添加行,只需将 grid
的 AllowUserToAddRows
属性设置为 false
。趁你还在设置,你可能还想更改 AllowUserToDeleteRows
、AllowUserToOrderColumns
、AllowUserToResizeColumns
和 AllowUserToResizeRows
。但我跑题了:这不是 DataGridView
教程。)
当没有当前项时进行绑定
通常,如果你的 DataSource
是一个 DataSet
,bsP.Current
会指向一个 DataRowView
;在前面的对象示例中,它通常指向一个 Passenger
。但是,当你创建新行时,它没有乘客,因此 bsP.Current
为 null
。我没有看到任何关于绑定架构在没有“当前”项时应该如何表现的文档,但至少该架构足够智能,可以清除“名称”文本框。然而,你可能会注意到用户仍然可以更改名称(但没有效果)。
如果你想在没有乘客时禁用文本框,可以为绑定源的 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 添加筛选和排序支持。)
但是,你可以筛选 DataTable
和 DataView
(DataTable
本身不实现 IBindingListView
,但其 DefaultView
,由 IListSource.GetList()
返回,实现了)。
为了演示基于 DataSet
的筛选,我创建了一个新的 Form
,Form2
,基于上面的分层 DataSet
示例。然后,我添加了两个新的 TextBox
,txtAirplaneFilter
和 txtPassengerFilter
(和一些标签),得到了这个
接下来,我为 TextBox
es 添加了以下 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
,并将每个 BindingSource
的 DataSource
设置为不同的 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
)。
我不能用数据绑定做什么?
有些事情我仍然不知道怎么做。
- 也许,我想要一个取消按钮,可以中止对记录的所有更改。或者,也许除非点击“保存”按钮,否则不应保留更改。如何实现?
- 也许,我想让用户同时编辑多条记录,利用
DataGridView
、ListBox
和其他可绑定控件的多选功能。例如,我如何让用户选择多行,然后同时设置多架飞机(为相同的字符串)的型号?
相关文章
- MSDN 论坛:Windows Forms 数据控件和数据绑定(请在此处查找
DataGridView
常见问题) - 使用 DataGridView 控件呈现数据(详细教程)
- ADO.NET 初学者指南
- 管理 @@IDENTITY 危机(处理 ADO.NET 和 SQL Server 中的 AutoIncrement 和 Identity 列)
- 面向对象程序员的 ADO.NET
- 数据绑定 TreeViews 一和 二
- 什么是 BindingSource,为什么我需要它?
- 操作 DataGridView 控件的 101 种方法
- 实现 IBindingList 和 ITypedList 集合的复杂数据绑定
- .NET / C# Windows Forms 中的数据绑定
- 在 ADO.NET 中使用 DataSet
- Windows Forms DataGridView 和 DataGrid 控件之间的区别