将DataTable绑定到类的辅助类






4.55/5 (21投票s)
在没有 System.Windows.Forms 命名空间的情况下绑定到 DataTable。
引言
.NET Framework 提供了许多有用的类来支持数据绑定。但不幸的是,这些类都与 System.Windows.Forms
(SWF)命名空间相关联。这是因为数据绑定的典型目的是将控件与(通常是业务层的)容器关联起来。然而,有时我想在不引入 SWF 命名空间的情况下使用数据绑定。例如,控制台应用程序、单元测试和服务的场景。本文旨在介绍一个简单的表绑定辅助类,该类可以在没有 SWF 命名空间的情况下使用。
什么是数据绑定?
数据绑定连接属性更改事件,以便在(通常)属性值更改时,两个或多个类可以自动同步。数据绑定是一种优雅的方式,可以将表示层与业务层解耦,使用事件在这一边界上传递数据。此外,如果使用一个中介(通常称为控制器)来连接这两个层之间的事件,表示层和业务层就不需要相互了解。同样,数据绑定可用于将数据访问层的表示(可能以 DataTable
的形式)与业务层(可能希望将单个行映射到类实例)解耦。
数据绑定有两个术语:简单绑定和复杂绑定。简单数据绑定通常与容器类和控件一起使用,其中容器的属性与控件的属性之间存在一对一的关系。当容器的属性值改变时,控件的值(通常显示在 UI 上)会自动更新。同样,当控件的可编辑属性改变时,容器也会更新。
复杂数据绑定是指将控件绑定到表。为了实现这一点,数据绑定机制需要有一个行游标的概念,以便与显示值的控件(双向)更新与特定行相关的值。并且行游标经常需要在用户导航列表类型控件(如 DataGridView
)时自动更新。因此称为“复杂数据绑定”。通过监视 DataTable
中的事件和列表类型控件中的事件,可以创建包含列表控件和离散控件的 UI,这些 UI 可以自动跟踪选定的行,在表更改时处理 UI 更新,依此类推。
.NET 数据绑定的历史
.NET 1.1 中的数据绑定最初是使用反射来实现的,以检测具有 [propertyName]Changed
签名的事件。例如,一个具有 Text
属性的类会实现 TextChanged
事件。事件处理程序负责检索 Text 属性的值,并使用新值更新任何对象(以及该对象中的相应属性)。双向绑定是通过另外连接一个反向的关联事件来实现的。
这种方法的麻烦在于,事件处理程序无法知道哪个属性发生了更改,这使得编写通用事件处理程序变得困难(但并非不可能)。.NET 2.0 Framework 引入了 INotifyPropertyChanged
接口,要求实现者提供 PropertyChanged
事件。事件签名是标准的“object, args”签名,在这种情况下,“args”的类型是 PropertyChangedEventArgs
,该类的唯一属性是属性名。现在事件处理程序知道了与事件关联的属性名,并且可以轻松编写通用事件处理程序。此外,测试一个类是否提供属性通知更容易——您可以测试 obj is INotifyPropertyChanged
。反之,您无法知道该类将在“哪些”属性上提供通知。在 .NET 1.1 风格中,您可以通过反射(假设 xxxChanged
事件被相应的“xxx”属性正确使用)来做到这一点。
编写通用事件处理程序的更“传统”方式需要创建一个辅助对象,该对象保留属性名并实现事件接收器,以便事件处理程序拥有辅助对象的实例。感到困惑吗?区别在于:对于实现 INotifyPropertyChanged
(因此也实现 PropertyChanged
事件)的类,属性名可能会被“忘记”,因为事件本身包含了属性名。对于传统实现,您需要一个保留属性名并接收 [propertyName]Changed
事件的容器类,以便处理程序可以根据正在接收事件的实例来获取属性名。如果您优化了代码,您实际上不需要属性名,可以获取一次并重复使用 PropertyInfo
实例,但概念是一样的。
以下是一个简单的类(BindableDataElement
),它演示了 INotifyPropertyChanged
接口。我将在本文后面描述的单元测试中使用这个类。请注意,这个类同时实现了旧风格的属性更改事件 TextChanged
和新风格的 PropertyChanged
事件。比较它们之间的区别。如果您想在容器类中支持传统数据绑定,您需要同时实现这两种事件样式。
using System;
using System.ComponentModel;
namespace Clifton.Tools.Data
{
public class BindableDataElement : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public EventHandler TextChanged;
protected string text;
public string Text
{
get { return text; }
set
{
if (text != value)
{
text = value;
OnTextChanged();
}
}
}
public BindableDataElement()
{
}
protected virtual void OnTextChanged()
{
if (TextChanged != null)
{
TextChanged(this, EventArgs.Empty);
}
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Text"));
}
}
}
}
不幸的是,.NET 2.0 的控件类并未实现 INotifyPropertyChanged
接口。但是,对于本文来说,这无关紧要,因为本文的重点是在不使用 System.Windows.Forms
命名空间(因此也不使用控件)的情况下实现表绑定。
关于数据绑定的思考
如前所述,数据绑定通常是在连接业务层和表示层时考虑的。即使是 MSDN 关于 .NET 3.0 中数据绑定的文档也传播了这一概念:数据绑定是建立应用程序 UI 和业务逻辑之间连接的过程。当然,这是在讨论 WPF 中数据绑定上下文的页面上,您将始终在 UI 的上下文中看到对数据绑定的讨论。
然而,数据绑定在非 UI 上下文中也很有用。它是一种将两个类关联起来而无需它们相互了解的有用机制(类似于观察者模式)。它也是一种将数据访问层(DAL)及其更原始的 DataTable
数据表示与业务层对象(使用具体类作为表数据的容器)连接起来的有用机制。
.NET 类
.NET Framework 包含许多有用的数据绑定类。.NET 2.0 中的主要类当然是 BindingSource
类,它实现了十个接口,通过列表管理例程(导航、排序、筛选、更新)和货币管理(行游标、更改通知等)来支持复杂绑定。而在 .NET 1.1 中(以前),控件的 DataSource
属性被初始化为一个 DataTable
或 DataView
,并且必须使用窗体的 CurrencyManager
(BindingContext
的一部分)来管理绑定到表的控件的货币(行游标)。随着 .NET 2.0 的出现,数据绑定和货币管理的功能在 BindingSource
类中公开,这坦率地说更有意义。不幸的是,BindingSource
类仍然是 System.Windows.Forms
命名空间的一部分。
在 .NET 1.1 中,BindingContext
属性是 Form
类的成员,它管理 BindingContext
集合类。数据源是简单(单个属性)还是复杂(实现 IList
或 IBindingList
)决定了 BindingContext
Item
属性返回的对象——分别是 PropertyManager
或 CurrencyManager
。这有点像一种权宜之计,因为 PropertyManager
类的 Position
和 Count
属性没有任何作用。如前一段所述,这种方法(Form
管理 BindingContext
实例,这些实例为您提供了派生自 PropertyManager
和 CurrencyManager
的基类的引用)对于复杂绑定来说现在基本过时了——BindingSource
是一个更好的机制。
无论是哪种实现,其基础都是 Binding
类,它维护控件属性与对象属性或对象列表中的当前对象之间的实际绑定。这个类有趣的地方在于,除了使用属性名之外,您还可以使用点分隔的导航路径。导航路径可以帮助您导航数据集、数据表、类属性,甚至可以导航表之间的关系。这种魔力是通过结合使用反射、数据集导航以及每个导航路径中集合的货币管理来实现的。唯一的规则是,最终属性必须解析为一个简单值属性或由货币管理器管理的集合中的一个列。
TableBindHelper 类
我将在这里提出的这个类绝不是 .NET BindingSource
类的替代品,其丰富性也不及 Binding
类(在导航路径支持方面)。如果您正在将复杂类型(DataTable
、DataView
、DataSet
对象)绑定到控件,我强烈推荐 BindingSource
类。我在这里提出的是一个小型类,我主要需要它来提供一些有限的复杂绑定,在不使用 System.Windows.Forms
命名空间的环境中,例如其他组件的单元测试、控制台应用程序和服务。这里的工作扩展了我之前关于 理解数据绑定 的文章,并且与关于 对象映射 - 行游标 的文章有一些相似之处。
实现
列和实例属性的管理
TableBindHelper
有四个字段,了解它们很有用,因为它们是该类工作方式的基石。
protected DataTable table;
protected int rowIdx;
protected Dictionary<string, ColumnBinder> columnBinders;
protected Dictionary<PropertyBinding, ColumnBinder> propertyBinders;
前两个很明显——我们提供绑定服务的 DataTable
实例,以及一个用于管理“货币”——当前行的字段。(关于“货币”这个词的一个有趣故事——当我第一次查看 .NET 绑定类并看到处理“货币”的属性时,我实在无法理解为什么会有处理货币值的属性。)
真正令人感兴趣的是两个 Dictionary
集合。第一个 columnBinders
,按名称维护了已绑定到类中属性的列的映射。第二个 propertyBinders
,是实例中映射到表中列的属性的映射。
AddColumnBinder
方法将条目添加到这两个字典中,以便在 DataTable
或实例属性发生更改时,TableBindHelper
能够相应地更新值。
public void AddColumnBinder(string columnName, object dest,
string propertyName, bool useLegacyChangeEvent)
{
ColumnBinder cb = new ColumnBinder(this, columnName, dest, propertyName);
columnBinders[columnName] = cb;
PropertyBinding db = new PropertyBinding(dest, propertyName);
propertyBinders[db]=cb;
if ( (dest is INotifyPropertyChanged) && (!useLegacyChangeEvent) )
{
// Create a generic property watcher.
CreatePropertyWatcher(dest, propertyName);
}
else
{
// Create the event sink in the container that knows about the
// the property name.
cb.CreatePropertyWatcher();
}
if (rowIdx < table.Rows.Count)
{
object val = table.Rows[rowIdx][cb.ColumnName];
UpdateTargetWithValue(cb, val);
}
}
前四行设置了字典中的条目。为了测试目的,我决定同时实现 .NET 1.1 和 2.0 的绑定支持,并允许程序员强制进行传统的事件挂钩。
最后,如果存在有效的行索引,则实例属性将使用该行中列的值进行更新。
DataTable 事件挂钩
在构造函数中
public TableBindHelper(DataTable table)
{
this.table = table;
columnBinders = new Dictionary<string, ColumnBinder>();
propertyBinders = new Dictionary<PropertyBinding, ColumnBinder>();
table.ColumnChanged += new DataColumnChangeEventHandler(OnColumnChanged);
table.RowDeleted += new DataRowChangeEventHandler(OnRowDeleted);
table.RowChanged += new DataRowChangeEventHandler(OnRowChanged);
}
挂钩了三个 DataTable
事件。第一个 ColumnChanged
最明显,因为它在列值更改时触发。
protected void OnColumnChanged(object sender, DataColumnChangeEventArgs e)
{
ColumnBinder cb = null;
bool ret = columnBinders.TryGetValue(e.Column.ColumnName, out cb);
if (ret)
{
UpdateTargetWithValue(cb, e.ProposedValue);
}
}
如果该列已绑定到实例属性,则该属性将得到更新。
另外两个事件 RowDeleted
和 RowChanged
负责在由于插入或删除行而导致行索引更改时,将实例与所有绑定的列进行同步。
protected void OnRowDeleted(object sender, DataRowChangeEventArgs e)
{
if (rowIdx >= table.Rows.Count)
{
// Can result in rowIdx set to -1.
rowIdx = table.Rows.Count - 1;
}
if (rowIdx >= 0)
{
UpdateAllDestinationObjects();
}
}
protected void OnRowChanged(object sender, DataRowChangeEventArgs e)
{
if (e.Action == DataRowAction.Add)
{
RowIndex = FindRow(e.Row);
}
}
这两个方法是“愚蠢”的,因为它们不会通过确定当前行索引是否受插入或删除操作的影响来优化实例属性的更新。
当列值改变时
上面,OnColumnChanged
事件处理程序调用 UpdateTargetWithValue
。
protected void UpdateTargetWithValue(ColumnBinder cb, object val)
{
if ( (val == null) || (val==DBNull.Value) )
{
// TODO: We need a more sophisticated way of:
// 1: does the target handle null/DBNull.Value itself?
// 2: specifying the default value associated with a property.
val = String.Empty;
}
cb.PropertyInfo.SetValue(cb.Object, val, null);
}
问题总是,如何处理 null
或 DBNull.Value
?我想我应该将此方法设为虚拟方法,以便您可以覆盖此方法的行为,但我认为您可能只是在这里的代码中进行修复。不幸的是,“修复”对于不同的人来说可能会导致不同的实现。当然,将 val
设置为空字符串不会让数字属性满意,因为类型转换器会尝试将空字符串转换为数字!
所以,根据您的需要在此处添加实现。这还涉及到关于可空类型(nullable types)的决定——如果您的类使用可空类型,您可以避免关于当列值为 DBNull.Value
时应将属性设置为何值的恼人问题。
当实例属性值改变时
反向操作更容易,因为对象类型被用作列的存储机制。
protected void UpdateTablePropertyValue(ColumnBinder cb)
{
if (rowIdx < table.Rows.Count)
{
object val = cb.PropertyInfo.GetValue(cb.Object, null);
if (val == null)
{
val = DBNull.Value;
}
table.Rows[rowIdx][cb.ColumnName] = val;
}
}
您会注意到这里我显式地将 null
值设置为 DBNull.Value
,因此它适合事务处理到数据库。
单元测试
我有几个单元测试来验证传统的和新的 .NET 2.0 绑定过程。单元测试还进行了一些最小的测试,以验证行更改是否得到了妥善处理。当然,单元测试提供了很好的使用示例,所以我将在这里展示整个单元测试套件。请注意,我没有向您展示整个 TableBindHelper
类,哦,不,单元测试要重要得多!顺便说一句,这些不是 NUnit
测试——这些是在我的单元测试引擎上运行的,但将它们转换为 NUnit
测试并不难。
using System;
using System.Data;
using Clifton.Tools.Data;
using Vts.UnitTest;
namespace UnitTests
{
[TestFixture]
public class TableBindHelperTests
{
protected DataTable table;
protected BindableDataElement bdeLastName;
protected BindableDataElement bdeFirstName;
protected TableBindHelper tableBindHelper;
[SetUp]
public void Setup()
{
table = new DataTable();
table.Columns.Add(new DataColumn("LastName", typeof(string)));
table.Columns.Add(new DataColumn("FirstName", typeof(string)));
DataRow row = table.NewRow();
row["LastName"] = "A";
row["FirstName"] = "B";
table.Rows.Add(row);
row = table.NewRow();
row["LastName"] = "C";
row["FirstName"] = "D";
table.Rows.Add(row);
bdeLastName = new BindableDataElement();
bdeFirstName = new BindableDataElement();
tableBindHelper = new TableBindHelper(table);
}
[Test]
public void DataIsUpdatedTest()
{
tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
table.Rows[0]["LastName"] = "AA";
table.Rows[1]["FirstName"] = "BB";
Assertion.Assert(bdeLastName.Text == "AA",
"Destination object did not get updated.");
Assertion.Assert(bdeFirstName.Text == "BB",
"Destination object did not get updated.");
}
[Test]
public void DataIsUpdatedLegacyTest()
{
tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text", true);
tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text", true);
table.Rows[0]["LastName"] = "AA";
table.Rows[1]["FirstName"] = "BB";
Assertion.Assert(bdeLastName.Text == "AA",
"Destination object did not get updated.");
Assertion.Assert(bdeFirstName.Text == "BB",
"Destination object did not get updated.");
}
[Test]
public void RowIsUpdatedTest()
{
tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
bdeLastName.Text = "AA";
bdeFirstName.Text = "BB";
Assertion.Assert(table.Rows[0]["LastName"].ToString()== "AA",
"Table did not get updated.");
Assertion.Assert(table.Rows[0]["FirstName"].ToString() == "BB",
"Table did not get updated.");
}
[Test]
public void RowCursorChangeTest()
{
tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
tableBindHelper.RowIndex = 1;
Assertion.Assert(bdeLastName.Text == "C",
"Destination object did not get updated.");
Assertion.Assert(bdeFirstName.Text == "D",
"Destination object did not get updated.");
}
[Test]
public void InitialValueTest()
{
tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
Assertion.Assert(bdeLastName.Text == "A",
"Destination object did not get updated.");
Assertion.Assert(bdeFirstName.Text == "B",
"Destination object did not get updated.");
}
[Test]
public void DeleteRowTest()
{
tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
tableBindHelper.RowIndex = 1;
table.Rows[1].Delete();
Assertion.Assert(bdeLastName.Text == "A",
"Destination object did not get updated.");
Assertion.Assert(bdeFirstName.Text == "B",
"Destination object did not get updated.");
}
}
}
结论
我希望本文能为那些对与 System.Windows.Forms
命名空间无关的数据绑定感兴趣的人提供一个有用的起点。虽然它绝不是全面的,但它应该为严谨的实现提供一个良好的基础,但希望它本身也能用于类到 DataTable
的轻量级绑定。似乎我每年都会因为各种原因重新审视数据绑定。这是一种非常有用的技术,而且它具有非常“丰富”的能力,能够适应不同的架构,并且能够很好地与命令式和声明式代码协同工作。