用于显示、编辑和自动更新多对多关系的基类 Windows 窗体 – 第二部分






2.46/5 (3投票s)
2005年10月11日
7分钟阅读

36395

666
在这一部分,我添加了退出时更新数据库的例程,并且因为我觉得非常慷慨,我还添加了一个从 Excel 数据表中导入数据的例程。
引言
在这一部分,我添加了退出时更新数据库的例程,并且因为我觉得非常慷慨,我还添加了一个从 Excel 数据表中导入数据的例程。
目前我只添加了 `UpdateDatabase` 例程,并从窗体关闭例程中调用它。稍后我们会在进行更改、添加和删除内容时使用它。
如果您编辑了一个记录然后退出,这个例程会起作用。它会更新数据库。但是请注意,更改不会反映到另一个选项卡。例如,如果我将 _Ms Smith_ 改为 _Mrs Taylor_,那么它只在你进行更改的选项卡中生效。如果你去到另一个选项卡,它就不会被更改。这是因为一个关系(例如 AB)不会更新另一个(BA)——这需要修复,我们将在稍后进行。
你也可以让它添加记录,但你必须弄清楚正确的 ID 号,否则它会崩溃。这同样需要修复。删除记录在某种程度上也能工作,但我们需要对它的操作有更多的控制。所有这些都将在第三部分解决。
更新 Access 数据库的例程基于 `DataAdapter.Update` 调用。为了使用它,你必须设置好所有的 SQL 语句并仔细定义参数。老实说,我记不清这一切到底是如何工作的了。我记得我花了许多许多小时的摸索才让它正常工作,你必须小心地精确指定所有内容。
private void UpdateDatabase()
{
string updateSQLA = "Update A set [AID]=?, [AName]=? where [AID]=?";
string insertSQLA = "Insert into A ([AID], [AName]) values (?,?)";
string deleteSQLA = "Delete * from A where [AID]=?";
//
//the @ is to make sure that it is treated as is,
//no confusion with C# keywords
daA.UpdateCommand = new OleDbCommand(updateSQLA, conn);
daA.UpdateCommand.Parameters.Add("@AID", OleDbType.Integer, 10, "AID");
daA.UpdateCommand.Parameters.Add("@AName",
OleDbType.VarWChar, 50, "AName");
OleDbParameter AID1 = daA.UpdateCommand.Parameters.Add("@AID",
OleDbType.Integer, 10, "AID");
AID1.SourceVersion = DataRowVersion.Original;
daA.InsertCommand = new OleDbCommand(insertSQLA, conn);
daA.InsertCommand.Parameters.Add("@AID", OleDbType.Integer, 10, "AID");
daA.InsertCommand.Parameters.Add("@AName", OleDbType.VarWChar, 50, "AName");
daA.DeleteCommand = new OleDbCommand(deleteSQLA, conn);
OleDbParameter AID2 = daA.DeleteCommand.Parameters.Add("@AID",
OleDbType.Integer, 10, "AID");
AID2.SourceVersion = DataRowVersion.Original;
string updateSQLB = "Update B set [BID]=?, [BName]=? where [BID]=?";
string insertSQLB = "Insert into B ([BID], [BName]) values (?,?)";
string deleteSQLB = "Delete * from B where [BID]=?";
//
daB.UpdateCommand = new OleDbCommand(updateSQLB, conn);
daB.UpdateCommand.Parameters.Add("@BID", OleDbType.Integer, 10, "BID");
daB.UpdateCommand.Parameters.Add("@BName",
OleDbType.VarWChar, 50, "BName");
OleDbParameter BID1 = daB.UpdateCommand.Parameters.Add("@BID",
OleDbType.Integer, 10, "BID");
BID1.SourceVersion = DataRowVersion.Original;
daB.InsertCommand = new OleDbCommand(insertSQLB, conn);
daB.InsertCommand.Parameters.Add("@BID", OleDbType.Integer, 10, "BID");
daB.InsertCommand.Parameters.Add("@BName",
OleDbType.VarWChar, 50, "BName");
daB.DeleteCommand = new OleDbCommand(deleteSQLB, conn);
OleDbParameter BID2 = daB.DeleteCommand.Parameters.Add("@BID",
OleDbType.Integer, 10, "BID");
BID2.SourceVersion = DataRowVersion.Original;
//now need to update ParticipantGroup table in real database
//it doesn't matter which I use, AB or BA because
//the info should! be the same really? and probably isn't
//Let's try AB
string updateSQLAB = "Update AB set [ABID]=?, [AID]=?," +
" [BID]=? where [AID]=? and [BID]=?";
string insertSQLAB = "Insert into AB ([ABID], [AID]," +
" [BID]) values (?,?,?)";
daAB.UpdateCommand = new OleDbCommand(updateSQLAB, conn);
daAB.UpdateCommand.Parameters.Add("@ABID",
OleDbType.Integer, 10, "ABID");
daAB.UpdateCommand.Parameters.Add("@AID",
OleDbType.Integer, 10, "AID");
daAB.UpdateCommand.Parameters.Add("@BID",
OleDbType.Integer, 10, "BID");
OleDbParameter ABID1 = daAB.UpdateCommand.Parameters.Add("@AID",
OleDbType.Integer, 10, "AID");
ABID1.SourceVersion = DataRowVersion.Original;
OleDbParameter ABID2 = daAB.UpdateCommand.Parameters.Add("@BID",
OleDbType.Integer, 10, "BID");
ABID2.SourceVersion = DataRowVersion.Original;
daAB.InsertCommand = new OleDbCommand(insertSQLAB, conn);
daAB.InsertCommand.Parameters.Add("@ABID",
OleDbType.Integer, 10, "ABID");
daAB.InsertCommand.Parameters.Add("@AID",
OleDbType.Integer, 10, "AID");
daAB.InsertCommand.Parameters.Add("@BID",
OleDbType.Integer, 10, "BID");
//For testing
//DataRow[] dr = ds.Tables["AB"].Select(null,null,
// DataViewRowState.Deleted);
//foreach (DataRow d in dr) MessageBox.Show(d["BID",
// DataRowVersion.Original].ToString()+"|"+d["AID",
// DataRowVersion.Original].ToString()+"|"+d["ABID",
// DataRowVersion.Original].ToString());
string deleteSQLAB = "Delete * from AB where [AID]=? and [BID]=?";
daAB.DeleteCommand = new OleDbCommand(deleteSQLAB, conn);
OleDbParameter ABID3 = daAB.DeleteCommand.Parameters.Add("@AID",
OleDbType.Integer, 10, "AID");
ABID3.SourceVersion = DataRowVersion.Original;
OleDbParameter ABID4 = daAB.DeleteCommand.Parameters.Add("@BID",
OleDbType.Integer, 10, "BID");
ABID4.SourceVersion = DataRowVersion.Original;
int g = 0, h = 0, i = 0;
try //AB first (important)
{
i = daAB.Update(ds, "AB");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
try
{
g = daA.Update(ds, "A");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
try
{
h = daB.Update(ds, "B");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
//for testing
MessageBox.Show(g.ToString()+" A and "+h.ToString()+
" B and "+ i.ToString()+" AB records updated");
}
从 Excel 电子表格导入数据
为了让你用更多数据来测试这个系统,我包含了我在学校的实际系统中用于导入学生和班级数据的例程。当然,我已经为这个基础程序做了调整。你可能认为我把 A 和 B 搞反了,但可以把它看作 A 是老师(或班级),B 是学生。这样我认为就说得通了。如果你不喜欢这样,可以随意调换。
要访问此功能,请从“文件”菜单调用它,并在应用程序的 _debug_ 文件夹中找到名为 _AoB.xls_ 的文件。
要使用 Excel 相关功能,你必须在 COM 选项卡中添加对 Microsoft.Excel 相关库的引用。我在 Beta 2.0 下进行此操作时遇到了一些问题,所以我通过使用 1.1 下的我的主程序套件中的 interop DLLs 并添加对它们的引用来进行了调整。我认为你还需要添加 `using System.Reflection
。我希望如果你只下载 Zip 文件,一切都能正常工作,但如果你在自己的应用程序中使用这些例程,可能会出现程序集问题,例如一个错误提示说 Excel 未识别等。尝试确保 interop DLLs 被正确引用。这也是一个我不太理解的领域,但最终还是摸索出了工作方法。
导入例程首先询问你是否要保存现有数据。如果你选择“是”,它会将数据保存到电子表格中,然后显示出来,你必须手动保存。你也可以将数据写出到 XML 文件,但我还没有实现这一点(至少在这个应用程序中;如果你想要想法,可以给我发邮件,我会从另一个应用程序中找出代码)。
下一个问题是是否要清除表。如果你不清除,系统将在导入数据时不触动现有数据。这样你就可以例如导入一个现有的班级列表,只添加少量新内容,而不会影响现有数据。
一个主要问题是如何选择文件然后导入。当类导入 Excel 工作表时,它会根据看到的数据类型来决定列的类型。如果它看到一个数字,它会将该列设置为数字类型;如果是字符串,则设置为字符串类型。但是,如果我有一个类,如 7.1,这是七年级第一组,我希望它成为字符串,因为列表中后面可能出现 7.A。由于它将其设置为数字,所以当遇到 7.A 时,例程就会失败。尽管我尝试了很多技巧,但仍未解决这个问题。我能解决的唯一方法是确保我的 Excel 工作表的前几行数据是字符串。我认为有可能在数据集中手动创建一个表并指定数据类型,但我不知道如何使用 Excel 例程做到这一点?
[在使用 Access 数据库和字符串时,我学到一个小技巧是记住使用 `mystring.Replace("'", "''");` 类型的操作将单引号转换为双单引号。日期也很麻烦,尤其是如果你不是美国风格的话。]
例程经常需要找到 ID 号的最大值。这些是我使用的数字,而不是 Access 自动生成的自动编号——我不用那些,因为事情很容易脱节。我对实际的 Access 数据库执行 `ExecuteScalar` 来获取 ID 的最大值。例如:表 A 中的 AID 等(不是 Access 自动生成的 ID)。
然后我将它递增,这样我就能确保事物能够正确匹配。然而,如果在我完成处理和重新保存之前,其他人进行更新,那么我就完蛋了。这些例程可能需要为多用户使用进行一些工作。
这也解释了为什么自动编号是危险的。当添加和删除时,数据集中的 ID 号(当然,数据集与实际的 Access 数据库断开连接)很容易与 Access 保存记录时生成的 ID 号不一致。如果你添加三行并删除中间的一行,然后将这两行保存到 Access,自动编号将不知道丢失的数字,然后就会崩溃。所以你必须管理所有的数字并重新调整一切。想象一下添加一百条记录然后删除第一条。想想为了在保存数据集表之前将所有记录和相关记录重新同步,你需要做多少额外的处理。
当你对空表执行 `max` 操作时,也会发生一些奇怪的事情——我(认为)它会失败!我先进行计数,只有在计数大于 0 时,我才执行 `max`。这并不是太大的开销,但这样做更干净。
我还做了一些检查,看看我是否已经有现有的 AName 或 BName 或 AID-BID 组合。如果我已经有了,那么我就忽略该记录,然后处理下一条。这是为了避免重复。如果 John 属于 Mr Moore,那么我只需要一条这样的记录。
另外请注意,我正在修改实际的 Access 数据库。最后,我将它重新读入数据集并重新显示。请注意,我的列添加例程只需要在会话中执行一次——因此使用了布尔标志 `DoneThisOnce`。如果没有这个标志,我会在 `DataGridView` 中得到重复添加的列。
另外请注意,当你导入并退出时,更新数据库例程不会记录任何更新,因为所有这些都在 `ImportFromExcel` 例程中已经处理好了。