如何手动创建类型化DataTable






4.97/5 (47投票s)
当您重写 GetRowType 时,还需要重写 NewRowFromBuilder()。
引言
我写这篇文章是因为网上关于使用 DataTable.GetRowType()
方法的信息不多,而且我找到的代码示例要么是完全错误的,要么是不完整的。此外,似乎没有任何自动化工具可以仅创建类型化的 DataTable
——相反,有用于创建类型化 DataSet
的工具。最后,我不得不创建一个类型化 DataSet
来弄清楚我在手动创建类型化 DataTable
时哪里做错了。因此,这是一篇关于我所学内容的初学者文章,目的是提供一个示例和正确的信息作为他人的资源。我没有提供创建类型化 DataTable
的工具,这也许是未来文章的内容。
什么是类型化 DataTable?
类型化 DataTable
允许您创建一个特定的 DataTable
,其中已初始化了所需的列、约束等。类型化 DataTable
通常还使用类型化 DataRow
,这允许您通过属性名访问字段。因此,而不是
DataTable personTable=new DataTable();
personTable.Columns.Add(new DataColumn("LastName"));
personTable.Columns.Add(new DataColumn("FirstName"));
DataRow row=personTable.NewRow();
row["LastName"]="Clifton";
row["FirstName"]="Marc";
使用类型化 DataTable
会看起来像这样
PersonTable personTable=new PersonTable();
PersonRow row=personTable.GetNewRow();
row.LastName="Clifton";
row.FirstName="Marc";
类型化 DataTable
的优势与类型化 DataSet
的优势相同:您拥有类型化的 DataTable
和 DataRow
,并且您使用属性而不是字符串来设置/获取行中的值。此外,通过使用类型化的 DataRow
,字段值(在 DataRow
中是 object
类型)可以在属性 getter 中被自动强制转换为正确的类型。这提高了代码的可读性,并消除了不当构造和字段名拼写错误的可能性。
创建类型化 DataTable
要创建类型化 DataTable
,请创建自己的派生自 DataTable
的类。例如
public class PersonTable : DataTable
{
}
有两个方法需要重写:GetRowType()
和 NewRowFromBuilder()
。这篇文章的重点在于,我花了大约四个小时才弄清楚需要重写第二个方法。
protected override Type GetRowType()
{
return typeof(PersonRow);
}
protected override DataRow NewRowFromBuilder(DataRowBuilder builder)
{
return new PersonRow(builder);
}
第二个方法至关重要。如果不提供它,在尝试创建新行时会遇到关于“数组类型不匹配”的异常。我花了几个小时才弄明白这一点!
创建类型化 DataRow
接下来,您需要一个类型化 DataRow
来定义上面引用的 PersonRow
类型。
public class PersonRow : DataRow
{
}
构造函数
考虑到上面调用的 NewRowFromBuilde
r,构造函数参数是显而易见的,但不太明显的是,构造函数必须标记为 protected
或 internal
,因为 DataRow
构造函数被标记为 internal
。
public class PersonRow : DataRow
{
internal PersonRow(DataRowBuilder builder) : base(builder)
{
}
}
填写详细信息
接下来,我将展示类型化 DataTable
和 DataRow
的基本知识。这些方法和属性的目的是利用类型化 DataRow
来避免在需要 DataTable
的代码中进行类型转换。
PersonTable 方法
构造函数
在构造函数中,我们可以添加定义表的列和约束。
public class PersonTable : DataTable
{
public PersonTable()
{
Columns.Add(new DataColumn("LastName", typeof(string)));
Columns.Add(new DataColumn("FirstName", typeof(string)));
}
}
以上是一个简单的示例,并没有说明如何创建主键、设置字段约束等。
索引器
您可以实现一个返回类型化 DataRow
的索引器
public PersonRow this[int idx]
{
get { return (PersonRow)Rows[idx]; }
}
索引器是在类型化 DataTable
上实现的,因为我们无法重写 Rows
属性上的索引器。边界检查可以留给 .NET 框架的 Rows
属性。非类型化 DataRow
的典型用法看起来像这样
DataRow row=someTable.Rows[n];
而类型化 DataRow
的索引器看起来像这样
PersonRow row=personTable[n];
不理想,因为它看起来像我在索引一个表数组。另一种方法是实现一个属性,也许命名为 PersonRows
;然而,这将需要实现一个 PersonRowsCollection
并将 Rows
集合复制到类型化集合中,这在每次索引 Rows
集合时很可能会对性能产生重大影响。这甚至更不理想!
Add
Add
方法应该接受类型化的 DataRow
。这可以防止我们将行添加到另一个表中。如果您尝试使用非类型化 DataTable
执行此操作,会在运行时出现错误。类型化 Add
方法的优点是您会收到编译器错误,而不是运行时错误。
public void Add(PersonRow row)
{
Rows.Add(row);
}
移除
类型化的 Remove
方法具有与上面类型化 Add
方法相同的优点
public void Remove(PersonRow row)
{
Rows.Remove(row);
}
GetNewRow
这里,如果我们尝试使用 DataTable.NewRow()
方法,就会出现冲突,因为唯一不同的是返回类型,而不是方法签名(参数)。因此,我们可以写
public new PersonRow NewRow()
{
PersonRow row = (PersonRow)NewRow();
return row;
}
然而,我个人反对使用“new
”关键字来覆盖基类的行为。因此,我更喜欢完全不同的方法名
public PersonRow GetNewRow()
{
PersonRow row = (PersonRow)NewRow();
return row;
}
PersonRow 属性
类型化的 DataRow
应包含 PersonTable
构造函数中定义的列的属性
public string LastName
{
get {return (string)base["LastName"];}
set {base["LastName"]=value;}
}
public string FirstName
{
get {return (string)base["FirstName"];}
set {base["FirstName"]=value;}
}
这里的优点是我们可以使用属性名(任何拼写错误都会导致编译器错误),可以使用 Intellisense,并且可以在应用程序中而不是在这里进行类型转换。此外,如果需要,我们还可以添加验证和属性更改事件。这里也是处理 DBNull
到 null 的转换以及从 null 到 DBNull
的转换的好地方,如果我们使用可空类型,我们可以为属性 getter/setter 添加更智能的功能。
PersonRow 构造函数
您可能希望在构造函数中初始化字段
public class PersonRow : DataRow
{
internal PersonRow(DataRowBuilder builder) : base(builder)
{
LastName=String.Empty;
FirstName=String.Empty;
}
}
行事件
如有必要,您可能希望实现类型化的行事件。常见的行事件是
ColumnChanged
ColumnChanging
RowChanged
RowChanging
RowDeleted
RowDeleting
我们将查看其中一个事件 RowChanged
来说明类型化事件。
定义委托
首先,我们需要一个合适类型的委托
public delegate void PersonRowChangedDlgt(PersonTable sender,
PersonRowChangedEventArgs args);
请注意,此委托定义了类型化的参数。
事件
现在我们可以将事件添加到 PersonTable
类中
public event PersonRowChangedDlgt PersonRowChanged;
定义事件参数类
我们还需要一个类型化的事件参数类,因为我们要使用我们的类型化 PersonRow
public class PersonRowChangedEventArgs
{
protected DataRowAction action;
protected PersonRow row;
public DataRowAction Action
{
get { return action; }
}
public PersonRow Row
{
get { return row; }
}
public PersonRowChangedEventArgs(DataRowAction action, PersonRow row)
{
this.action = action;
this.row = row;
}
}
重写 OnRowChanged 方法
我们不需要添加 RowChanged
事件处理程序,而是可以重写 OnRowChanged
方法,并为新方法 OnPersonRowChanged
创建类似的模式。请注意,我们仍然调用基类 DataTable
的 RowChanged
实现。这些方法被添加到 PersonDataTable
类中。
protected override void OnRowChanged(DataRowChangeEventArgs e)
{
base.OnRowChanged(e);
PersonRowChangedEventArgs args =
new PersonRowChangedEventArgs(e.Action, (PersonRow)e.Row);
OnPersonRowChanged(args);
}
protected virtual void OnPersonRowChanged(PersonRowChangedEventArgs args)
{
if (PersonRowChanged != null)
{
PersonRowChanged(this, args);
}
}
请注意,上面的方法是 virtual
的,因为这是 .NET 框架中引发事件的模式,并且遵循此模式是个好习惯。
添加一个类型化事件就需要这么多工作,所以您可以看出,有一个代码生成器会非常有帮助。
使用事件
这是一个简单的示例,说明如何使用类型化的 DataTable
和事件
class Program
{
static void Main(string[] args)
{
PersonTable table = new PersonTable();
table.PersonRowChanged += new PersonRowChangedDlgt(OnPersonRowChanged);
PersonRow row = table.GetNewRow();
table.Add(row);
}
static void OnPersonRowChanged(PersonTable sender, PersonRowChangedEventArgs args)
{
// This is silly example only for the purposes of illustrating using typed events.
// Do not do this in real applications, because you would never use this Changed event
// to validate row fields!
if (args.Row.LastName != String.Empty)
{
throw new ApplicationException("The row did not initialize " +
"to an empty string for the LastName field.");
}
}
}
然而,这说明了类型化 DataTable
和类型化 DataRow
的优点:可读性和编译器对正确用法的检查。
结论
希望这篇文章清楚地说明了如何手动创建类型化 DataTable
。“我发现”(在互联网上其他地方找不到)的“发现”是,当您重写 GetRowType()
时,您还需要重写 NewRowFromBuilder()
。