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

C# 中的 Access 数据库编辑器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.03/5 (34投票s)

2002年3月2日

16分钟阅读

viewsIcon

411408

downloadIcon

24578

本项目介绍了一个用 C# 编写的 Access 数据库编辑器

引言

本项目提出了一个用 C# 编写的 Access 数据库编辑器。该解决方案包含两个项目,其中一个项目 GenericOLEDB dll 通过使用 C# 中可用的 Ole Db 类提供对数据库的访问。正如标题所示,dll 代码是完全通用的,所有特定于 Access 的主要数据库代码都包含在调用该代码的项目文件中。主项目和 GenericOLEDB 项目的消费者包含两个文件,主文件负责数据库的特定创建和打开以及不需要进一步信息的数据库操作,第二个文件提供了一个允许插入或删除数据行的编辑器。

本文将采用以下格式。首先将介绍代码的详细信息,从 GenericOLEDB 项目和 OLE DB 代码开始,然后转向演示的控件代码,涉及网格和列表的使用以及如何访问其中的数据。最后一部分将探讨该程序的功能和局限性。

本项目是使用 Developer Studio.NET beta2 和 Developer Studio.NET Professional 在 win2000 和 XP Home 上开发的

GenericOLEDB 项目

GenericOLEDB 项目是一个动态链接库,其中包含一个 C# 类。这个类是一个包装类,包含了程序的 OLE DB 功能。该类的目的是通用地提供一个可重用的数据库接口,该接口可用于任何所需的数据库技术。因此,该类是通过手动创建的,而不是通过 Developer Studio Seven Wizard 生成的。

该类被编写成遵循与使用向导生成时相同的开发格式。这意味着访问技术是使用 OleDbDataAdapter 作为数据库的主要桥梁,以及四个使用相同 OleDbConnection 对象与数据库通信的 OleDbCommand 对象。

这与向导生成的代码设置相同,因为适配器有一个用于更新、插入、删除和选择命令的命令变量,这些命令通过以下方式在代码中设置:

oleDbDataAdapter.DeleteCommand = oleDbDeleteCommand; 
oleDbDataAdapter.InsertCommand = oleDbInsertCommand; 
oleDbDataAdapter.SelectCommand = oleDbSelectCommand; 
oleDbDataAdapter.UpdateCommand = oleDbUpdateCommand; 

一旦命令在 OleDbDataAdapter 上设置好,连接就会在命令上设置。

oleDbSelectCommand.Connection = oleDbConnection; 
oleDbDeleteCommand.Connection = oleDbConnection; 
oleDbUpdateCommand.Connection = oleDbConnection; 
oleDbInsertCommand.Connection = oleDbConnection; 

GenericOLEDB dll 中的主要代码是 ExecuteCommand 函数,它执行当前选定的命令。命令在 dll 中通过跟踪最后设置的命令字符串来选择,该字符串被认为是需要执行的命令。如果待执行的命令是 select 命令,则该函数简单地返回 true,因为当调用 GetReader 函数时,命令的主要部分将被执行,该函数调用 select 命令上的 ExecuteReader 函数,无论调用哪个操作。

事务

这使得我们来到了 ExecuteCommand 函数中需要直接更新数据库的部分。这对于插入、删除和更新命令是必需的。事务只是锁定数据库一部分区域的一种方法,以便在不破坏数据库完整性的情况下执行写入或删除操作。任何试图访问由事务覆盖的数据库部分的操作都将被阻止,直到事务完成。事务有两种方式完成:一种是通过执行提交,将更改写入数据库;另一种是通过执行回滚,将数据库重置回事务开始前的状态。

C# 中的事务按自顶向下的方式工作,首先必须从 OleDbConnection 中获取事务,这是顶层。

/// get the transaction object from the connection 
OleDbTransaction trans = oleDbConnection.BeginTransaction(); 

然后,这是允许我们为特定命令创建子事务的父事务。这是通过调用 OleDbTransaction begin 函数来完成的。在这种情况下,我们将使用 DeleteCommand

case COMMAND.DELETE: oleDbDeleteCommand.Transaction = trans.Begin(); break; 

从上面可以看出,每个 OleDbCommand 对象都包含一个 Transaction 成员,它必须连接到与 OleDbCommand 对象相同的 OleDbConnection。如果不是这样,将会抛出异常,数据库操作将不会按预期工作。

一旦使用 OleDbCommand::ExecuteNonQuery 函数执行了命令(该函数用于执行不返回记录集的命令)。如果 ExecuteNonQuery 函数成功,则可以将事务提交到数据库。

case COMMAND.DELETE: oleDbDeleteCommand.Transaction.Commit(); break; 
		

然而,如果 ExecuteNonQuery 函数失败,则需要将事务回滚到其以前的状态。

case COMMAND.DELETE: oleDbDeleteCommand.Transaction.Rollback(); break; 

ExecuteNonQuery 函数的成功或失败通过其返回值进行检查。正值表示已成功在数据库上执行了操作,而负值或异常表示未能成功在数据库上执行操作。一旦子事务已提交或回滚了数据库上的操作,就必须对父事务或 OleDbConnection 对象执行相同的操作。

CSHARPOLEDB3 项目

项目的主文件是 Form1.cs 文件,它控制数据输入和响应,并将项目绑定到 Access 类型数据库。Access 数据库的初始值已硬编码在窗体类的构造函数中。

textBox1.Text = "Microsoft.Jet.OLEDB.4.0"; 
textBox2.Text = "Admin"; textBox3.Text = ""; 
Mode = "ReadWrite"; 

OpenFileDialog

这些是具有仅需通过浏览按钮添加数据库文件路径的 Access 数据库的默认设置,该按钮初始化一个从工具箱添加到项目中的 OpenFileDialog,然后在代码中进行初始化。

	openFileDialog1.InitialDirectory = Directory.GetCurrentDirectory(); 
	openFileDialog1.Filter = "Access Files ( *.mdb )| *.mdb"; 
	openFileDialog1.FilterIndex = 1; 
	if( openFileDialog1.ShowDialog() == DialogResult.OK ) 
	{ 
		DatabaseName = openFileDialog1.FileName; 
		textBox4.Text = DatabaseName; 
	} 

您可以看到在使用 OpenFileDialog 之前有一些操作要执行。 InitialDirectory 成员通过使用静态 Directory.GetCurrentDirectory 函数设置为当前目录。然后将过滤器设置为 Access 类型文件。这是通过使用一个由 "|" 分隔成两部分的长字符串完成的。第一部分是要打开的文件的描述,第二部分是用于搜索目录文件的字符串,在这种情况下是 "*.mdb"。由于 OpenFileDialog 实际上不会打开文件,它可以指向任何所需的不同文件类型,这就是为什么在显示对话框之前需要将 FilterIndex 属性设置为所需的过滤器。

完成后,就可以显示对话框,它控制到将要选择的数据库的所有必需的路径查找。只要用户找到文件并按下 OK 按钮,一切都会正常,因为 OpenFileDialog 将返回 DialogResult.OK。接下来要做的就是存储 OpenFileDialog FileName 属性,它将返回文件的完整路径和名称。

Select 按钮随后会显示一个带有两个编辑字段的对话框,第一个字段是您想从表中选择的字段。默认情况下设置为 "*",这将返回所选表的所有字段。第二个编辑字段是输入所需表名的空间。这是一个普通的编辑字段,用户必须知道字段中数据库表之一的名称。这是程序的要求,尽管一个改进的想法是获取数据库参数的数据并显示数据库中此时的表列表。

当选择了正确的数据库后,按下“打开数据库”按钮,该按钮创建并获取相应框中的参数,然后创建一个 GenericOLEDB 对象并调用带参数的 open。

dbAccess = new GenericOLEDBClass(); 
dbAccess.Open( Provider, UserID, Password, DatabaseName, Mode ); 

然后使用在 select 对话框中输入的表来设置 Select 命令。通过调用 dbAccess.ExecuteCommand 来执行命令,尽管由于这是第一个生成的命令,该函数此时只会返回 true。select 函数直到在 DisplayList 函数中调用 dbAccess.ExecuteReader 函数时才被调用,并将 dbAccess.Reader 变量用作参数。

在 ListBox 中显示数据

DisplayList 函数完全按照其名称所示进行操作,它清除列表框中的任何当前数据,然后根据读取器返回的字段数设置若干列。

/// build the headers first
for( int i=0; i<nCount; i++ )
{
	ColumnHeader header = new ColumnHeader();
	header.Width = 100;
	header.Text = reader.GetName( i );
	listView1.Columns.Add( header );

	/// Store the header names in a collection
	stringCol.Add( reader.GetName( i ) );
} 

创建列后,代码需要做的就是将数据添加到列表中。

/// now add the data
ListViewItem lvItem = new ListViewItem();

while( reader.Read() == true )
{
	lvItem = listView1.Items.Add( reader.GetValue( 0 ).ToString() );
	for( int i=1; i<nCount; i++ )
	{
		lvItem.SubItems.Add( reader.GetValue( i ).ToString() );
	}
}

数据通过创建新的 ListViewItem 进入表中,顾名思义,它控制在 Listbox 中显示的项目的显示属性和数据。尽管从上面的代码可以看出, ListViewItem 的实际使用有点复杂,因为行中的 ListViewItem 应被视为行的第一个单元格,同时也是所有其他单元格可访问的容器。这是通过 ListViewItem 类的 SubItems 成员实现的,该成员是类型

ListViewItem.ListViewSubItemCollection

我们接下来会看如何从 list view 中取出数据,但首先我们必须知道如何将其放入列表中。列表中第一个单元格的初始值是通过调用 ListView1.Items.Add 并传入所需参数来添加的。 ListViewItem.Item 成员是一个 ListViewItemCollection,它保存着行的所有单元格。Add 函数创建并返回行的 ListViewItem,该行又包含保存行中其余单元格的子项集合。这意味着实际行的创建首先是将 ListViewItem 添加到视图中,然后将每一列的其余单元格添加到 ListViewItem.SubItem 成员中。

从 ListBox 中获取数据

数据添加到列表框后,就需要将其取回,以便更新数据库。以下代码来自 OnDelete 函数。

ListView.SelectedListViewItemCollection col = listView1.SelectedItems;

IEnumerator colEnum = col.GetEnumerator();

/// move to the first and only
colEnum.MoveNext();

/// get the list view item
ListViewItem item = ( ListViewItem )colEnum.Current;

/// get the collection of subitems
ListViewItem.ListViewSubItemCollection subItemsCol = item.SubItems;

IEnumerator subEnum = subItemsCol.GetEnumerator();
StringEnumerator stringEnum = stringCol.GetEnumerator();

/// build sql code string removed

bool bFirst = true;

OleDbDataReader reader = dbAccess.GetReader;
reader.Read();
int nColumn = 0;
string strType;

while( subEnum.MoveNext() == true && stringEnum.MoveNext() == true )
{
		
	ListViewItem.ListViewSubItem subItem = 
	          ( ListViewItem.ListViewSubItem )subEnum.Current;
	strType = reader.GetDataTypeName( nColumn++ );
		
	/// build sql string code removed
}
		

代码开始时获取列表视图中选定的项目。在此代码中, ListBox 设置为单选,因此只有一个。选定的项目被返回到 ListView.SelectedListViewItemCollection,我们需要以某种形式对其进行遍历。方法是使用 GetEnumerator 函数,从外观上看,该函数随所有集合类的实例提供。 GetEnumerator 函数是从 IEnumerable 接口继承的,因此只要集合继承自此接口, GetEnumerator 函数就会存在。

每当调用 GetEnumerator 函数时,它都会返回一个 IEnumerator 对象,该对象位于要枚举的对象类型的起始位置之前。这就是为什么在对正在枚举的对象执行任何操作之前,必须调用 IEnumerator.MoveNext 函数的原因。 IEnumerator 接口只有三个函数: MoveNext,它移动到枚举中的下一个对象;Reset,它将枚举器重置到枚举中的第一个对象之前;以及 Current,它返回正在枚举的对象。 IEnumerator.Current 的调用必须强制转换为要枚举的类型对象。

ListViewItem item = ( ListViewItem )colEnum.Current;

一旦从集合中提取了 ListViewItem,我们就可以通过获取 ListViewItem.SubItem 成员的 ListViewItem.ListViewSubItemCollection 来获取 ListViewItem SubItems。集合中的每个 SubItem 本身都是一个 ListViewSubItem,这意味着当我们循环遍历子项以从 ListViewSubItemCollection.Current 成员获取每个单元格数据时,可以使用 ListBox 列标题所在字符串集合的 StringEnumerator 来构建 SQL 语句,并使用 ListViewSubItem.Text 成员变量返回每个单元格的值。

编辑框对话框

插入和更新命令通过 EditBox 对话框访问,这是一个简单的对话框,允许编辑和插入单行代码。 EditBox 是通过其构造函数调用的,该构造函数接受四个参数:第一个是 IWin32Window 接口,它用作调用 ShowDialog 后对话框的父项。第二个参数是当前的 OleDbDataReader,第三个是将在其上执行更新或插入命令的表名,最后一个参数是一个 bool bInsert,它指示 EditBox 是用于插入还是更新表中的记录。

在 DataGrid 中显示数据

ListBox 一样, DataGrid 也是在运行时创建的,只是这次信息直接从传递给构造函数的 OleDbDataReader 中获取。在 EditBox 用于插入新记录的情况下,只从读取器中获取列信息。但是,在 EditBox 用于编辑现有记录的情况下, EditBox 也会从读取器中获取行信息。

因为我们是在动态设置 DataGrid,所以我们需要创建一个新的 DataSet 和一个新的 DataTable DataSet 本质上是我们正在创建的一个数据库对象。它将作为包含将在 DataGrid 中显示的信息的容器。尽管在此示例中我们只使用了一个 DataTable,但我们可以在 DataSet 中包含任意数量的 DataTables

与在 ListBox 中显示数据一样,我们首先构建表中所需的列集。

DataColumn dataCol = dataTable.Columns.Add( reader.GetName( i ), reader.GetFieldType( i ) );
if( reader.GetSchemaTable() != null )
{
	DataTable tempTable = reader.GetSchemaTable();

	DataColumnCollection colCollect = tempTable.Columns;

	IEnumerator colEnum = colCollect.GetEnumerator();

	colEnum.MoveNext();
	for( int count=0; count<i; count++ )
	{
		colEnum.MoveNext();
	}

	DataColumn tempCol = ( DataColumn )colEnum.Current;

	if( tempCol.Unique == true )
	{
		dataCol.Unique = true;
	}
	/// this is technically incorrect AutoIncrement fields
	/// are being returned with this as false
	if( tempCol.AutoIncrement == true )
	{
		dataCol.ReadOnly = true;
		dataCol.AutoIncrement = true;
	}
	
	dataCol.MaxLength = tempCol.MaxLength;

}		

首先,通过调用 DataTable.Columns.Add 函数将 Column 添加到 DataTable 对象。这会在表中创建列并返回 DataColumn 对象,以便我们可以对其指定进一步的选项。然后,我们在调用 OleDbDataReader.GetSchemaTable 时检查读取器是否会返回有效的 SchemaTable,因为如果调用 OleDbDataReader.GetSchemaTable 返回 null 值,我们就无法对其进行任何操作。一旦我们断言 Schema Table 有效,我们就再次调用 OleDbDataReader.GetSchemaTable 函数并将结果存储在 DataTable 对象中。从 DataTable 对象中,可以从 Columns 成员中提取列并存储到 DataColumnCollection 对象中。 DataColumnCollection 的使用方式与其他集合类似,即 IEnumerator 接口等于 IEnumerator.GetEnumerator 函数的返回值。然后通过列遍历枚举器,直到它等于我们所需的列号。然后我们测试该列的各种属性。

这里需要注意的是,最初的想法是 DataColumn.AutoIncrement 的测试并不像我预期的那样工作,因为即使该列实际上是自动增量列,它也总是为 false。我以为可以通过检查自动增量列更新的值来解决这个问题。这也没起作用,因为如果存在自动增量值,则所有列都包含自动增量列更新的值。这就是为什么演示项目指定用户手动指示哪些字段应被忽略以进行编辑或插入的原因,因为代码无法正确确定哪些字段(如果有的话)应自动从 sql 语句中排除。

这就给了我们一组列,但到目前为止还没有数据。添加数据的方式有点奇怪,因为它打破了获取枚举器并遍历它的例程,回到了更传统(至少从 C++ 的角度来看)的想法,即获取一个数组并填充值。

/// add only one row to the table
DataRow dataRow = dataTable.NewRow();

if( bInsert == false )
{
	object[] itemArray = dataRow.ItemArray;
	for( int i=0; i<nCount; i++ )
	{
		itemArray[ i ] = reader.GetValue( i ).ToString();
	}

	dataRow.ItemArray = itemArray;
}		

上面的代码显示了 EditBox 如何将数据添加到行中,以便在 DataGrid 中显示。行通过 DataTable.NewRow 函数从 DataTable 中返回,该函数在表中创建一个具有正确或与表模式相同的新行。通过获取 DataTable ItemArray 并遍历返回的数组,用来自读取器的字符串填充每个项来设置行的信息。最后一行确保 DataRow.ItemArray 包含我们想要的数据。

一旦数据被编辑,数据的保存就与编辑或插入无关,并且完全是反向的,即我们钻取到 ItemArray,然后通过返回的值从数组中复制数据来构建一个 sql 字符串。

使用数据库编辑器

在编辑所有数据库方面,编辑器有一个主要缺陷,那就是数据库本身(无论是 Access 还是任何其他数据库)在两个或多个表之间维护关系。一个例子是 Microsoft 示例数据库 Nwind.mdb。如果打开该数据库并选择 Suppliers 表,编辑器将正常显示数据,只要排除 SupplierID 字段,就可以编辑和插入表中的字段。当尝试删除操作时,问题就会出现,它会抛出一个异常,说“由于表‘Products’包含相关记录,因此无法删除或更改该记录。”出现此错误的原因是数据库本身在维护关系,而不是纯粹的逻辑或关系关系。

另一个改进的领域是编辑器处理类型的方式,目前仅包括对基本类型的支持,而且说实话,示例数据库只使用字符串来表示数据,这意味着如果编辑器用于访问包含不同数据类型的数据库表,那么将需要进行一些代码修改。

结论

GenericOLEDB 应该允许使用提供的代码访问任何 Access 数据库,而且由于它是一个单独的 dll,因此很容易使用它来访问其他数据库。这可能需要对 GenericOLEDB dll 中的连接字符串进行一些小的更改,但仅此而已。其余代码显示了如何使用该 dll 来处理 Access 数据库,尽管提供的大部分代码应该可以重新用于任何数据库类型,因为大部分代码都处理读取器,而与数据库类型无关。

© . All rights reserved.