动态属性 - 运行时创建的数据库






4.12/5 (11投票s)
本文介绍如何创建一个数据结构,该结构允许用户在运行时以易于管理的方式添加额外的数据点,同时允许数据插入和验证。

引言
像大多数项目一样,这个项目是学习过程中信息的结晶,当时我正在努力寻找一个答案来满足老板让子系统更简单、更好、更快、更灵活的愿望。本项目将介绍一个鲜为人知的 .NET 类 PropertyDescriptor
,以及如何利用它来发挥你的优势。
在项目过程中,我们将探讨构建一个可以在运行时更改的轻量级灵活数据库需要什么。本项目将仅限于基础知识。上图中的一些项目并未实现,但它们被用作您可能希望自己实现的项目占位符。选择数据库应用程序是为了演示如何使用 PropertyDescriptor
。这个 .NET 类的原理非常适合在运行时动态更改数据结构。
背景
我的要求之一是使系统灵活。 .NET 中最灵活的数据结构之一是 DataSet
。您可以添加表、列、行和约束来模拟内存中的数据库。上述所有项目都可以运行时添加,以增加所提供数据的意义。例如,在上图添加另一列以包含电话号码可以提供海量额外信息。
另一个要求是使子系统更好。如果我使用 DataSet
作为子系统的底层结构,我将如何保存数据?第一个要求是灵活。如果我在运行时更改表的结构并添加数据,我将如何将数据保存到真正的数据库,如 SQL Server、Oracle、Access 等?我将需要一个通用表或将其存储为 XML 数据类型、字符串等。我将如何搜索数据?使用 DataSet
违反了我第二个要求的原则,也不能满足易于维护或扩展的要求。
设计架构
由于本项目旨在模拟数据库,因此我选择了一个看起来像数据库结构的数据结构,即包含表、列、行和单元格。下图显示了数据结构以及将用于解决方案的更常见的方法和属性。

处理这种数据结构会给 DataGrid
绑定以进行数据输入带来问题。如果我们将 DataGrid
的 DataSource
绑定到 RowCollection
,我们会得到所有的行而没有单元格。如果我们将它绑定到 CellCollection
,我们会得到所有的单元格而没有行。最终,我们需要来自所有行的所有单元格。
第二次尝试,我们可以通过遍历我们的 ColumnCollection
和 RowCollection
来创建一个包含所有列和行的 DataSet
。我们如何将数据在数据结构和 DataSet
之间来回传递?我们再次问自己,这是否容易、快速、更好、灵活?答案是明确的……不。
那么我们剩下什么了?将此数据结构粘合在一起的“罗塞塔石碑”是 PropertyDescriptor
。在帮助文件中,PropertyDescriptor
被描述为“提供对类属性的抽象”。哇,这个描述还有很大的改进空间。那提供的例子呢?这是帮助文件中提供的内容:
// Creates a new collection and assigns it the properties for button1.
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(button1);
// Sets an PropertyDescriptor to the specific property.
System.ComponentModel.PropertyDescriptor myProperty = properties.Find("Text", false);
// Prints the property and the property description.
textBox1.Text = myProperty.DisplayName+ '\n' ;
textBox1.Text += myProperty.Description + '\n';
textBox1.Text += myProperty.Category + '\n';
在这个形式下,这个例子并不能解决我们的问题。这只是查找现有的 PropertyDescriptor
并显示一些关于它的信息。然而,查看定义给了我们一个线索。我们将要处理类上的一个属性。问题就变成了哪个类以及哪个属性。
由于我们需要所有行,我们的 DataSource
将是 Table.RowCollection
。现在我们需要为 DataGrid
添加列。列名和单个数据单元格通常来自集合中项的属性。在我们的例子中,这意味着一个 Row
。查看 Row
类,有哪些属性可用于获取单个数据点?不幸的是,数据存储在另一个集合 CellCollection
中。我们如何从与 Row
关联的 CellCollection
中获取单个 Cell
,并提供有意义的 DataGrid
列名?
我们分两部分解决这个问题:创建列,然后填充数据。
DataGrid
由具有有意义名称的列组成。我们为每个表都有一个 ColumnCollection
,其中每个 Column
都有列名 (ColumnName
)、显示名称 (ColumnDisplayName
) 在 DataGrid
中,以及要输入的 数据类型 (ColumnType
)。我们可以通过动态迭代 ColumnCollection
来实现这一点,并创建每一列。
// Create Columns
foreach ( Column column in _table.Columns )
{
gridColumn = CreateColumn( column );
if ( gridColumn is DataGridViewComboBoxColumn )
{
BindComboBox( (DataGridViewComboBoxColumn)gridColumn, column );
}
_rowGrid.Columns.Add( gridColumn );
}
解决方案的第二部分是提供每个 Row
中的一个属性,该属性可以绑定到 DataGrid
列。这就是 PropertyDescriptor
的用武之地。为了提供我们最终需要的功能,我们将创建自己的类 (ColumnPropertyDescriptor
),该类继承自 PropertyDescriptor
。
RowCollection
实现 ITypedList
,它有一个方法 GetItemProperties
,该方法返回集合中的每个 PropertyDescriptor
。这是 DataGrid
将绑定的属性列表。我们必须根据 Column
中的列手动创建此列表。为此,每次我们创建 Column
时,都会创建一个新的 PropertyDescriptor
,并将 Column
作为参数传递。每次删除列时,我们需要找到该列创建的 PropertyDescriptor
并将其从列表中删除。

在每个 RowCollection
中动态创建或删除 ColumnPropertyDescriptor
允许我们为 DataGrid
创建可绑定的属性。
ColumnCollection.cs
public new void Add( Column column )
{
if ( column != null )
{
this._table.Rows.AddPropertyDescriptor( column );
base.Add( column );
}
}
public new bool Remove( Column column )
{
if ( column != null )
{
base.Remove( column );
this._table.Rows.RemovePropertyDescriptor( column );
}
}
每次 DataGrid
希望显示一个单元格时,它都会调用 PropertyDescriptor
的 GetValue
方法,并传入当前对象(一个 Row
对象)。以给定的行为输入,我们可以迭代 CellCollection
,查找具有与此 PropertyDescriptor
中的 Column
匹配的 Column
的单元格。找到匹配项后,我们就找到了要显示的。一个附加方法 SetValue
接受两个参数:Row
和要存储的值。
通过创建具有实现 GetValue
和 SetValue
方法的 ColumnPropertyDescriptor
类,以及一个将 ColumnPropertyDescriptor
对象添加到 RowCollection
和从中删除的方法,以及一个创建 DataGrid
列的方法,我们即将完成本文。
数据库项目旨在可移植到其他项目。除了本文上文提到的特定位置之外,其他类的内部代码都实现了正常的日常代码。集合类可以更新为包含根据 IBindingList
接口定义的排序、搜索、过滤等功能。
运行演示
启动应用程序,然后转到“文件”->“打开”并选择两个 SQL 文件(Address.sql 和 States)。
加载这两个文件后,单击 Address
表,然后单击“定义”选项卡,将状态数据类型更改为“外键”。然后屏幕底部会显示两个附加字段。一个说“表”,另一个说“列”。将表设置为“States”,将列设置为“Abbreviation”,然后单击返回到“数据”选项卡,您将看到状态字段现在是一个 ComboBox
。
现在让我们添加一些新字段。返回 Address
的“数据”选项卡。添加两个新列,使其看起来如下:

当您返回“数据”选项卡时,您将看到两个新列,您可以在其中添加数据。
下一步该做什么
在本文列出的代码片段的一些地方设置断点是真正理解应用程序流程的好起点。请注意,在 ColumnPropertyDescriptor
中设置断点有助于理解,但不利于运行应用程序。您将陷入一个无休止的循环,试图显示数据。您必须删除/禁用断点才能使应用程序运行。
为简单起见,DataAccess
类中的 SqlScriptReader
包含一些硬编码的结构来读取 SQL 数据文件。这需要修改才能读取其他数据文件。
虽然应用程序不允许保存数据,但数据可以轻松地存储在 XML 文件中、写回到 SQL 文本文件中,甚至写入到真正的数据库中。这些我留给读者作为练习,因为它们超出了本项目的范围。
其他建议是实现“定义”选项卡上的其他一些字段并添加其他数据类型。本文提出的数据库项目允许您实现自己的 Column
类,添加任何新的用于数据掩码或其他验证例程的属性,并通过正常的 UI 实践将它们连接起来。
最终想法
我已在生产环境中使用了此功能,拥有多达 10,000 行数据和约 15 列,未出现性能下降。与任何技术演示一样,性能和功能总是有改进的空间。这些我留给读者,因为它们超出了本文的范围。
历史
- 2007 年 3 月 19 日:初次发布