自动将 DataGridView 行保存到 SQL Server 数据库






4.84/5 (40投票s)
将 DataGridView 中已更改的行自动保存到数据库似乎是一项基本任务,但实现起来却充满挑战。在此处阅读为什么最直观的方法会失败以及如何使其正常工作。
引言
SQL Enterprise Manager 多年来一直在这样做:每当用户更改表中的某一行时,它都会自动写回数据库表。为用户提供相同的功能实现起来很棘手,因为 DataSet
和 BindingSource
之间的交互,这在 .NET 帮助文档中几乎没有记载。本文研究了一些直观的解决方案,并解释了它们为何不起作用。对涉及的事件的详细分析导致了最终解决方案,该解决方案出人意料地简单,正如任何好的解决方案都应该的那样。
背景
通常,用户需要显式保存他们的工作,就像在 Word 中保存文档一样。这种方法使用 BindingNavigator
的保存按钮,使用 DataRowViews
可以立即正常工作。但是,如果 DataRow
中的更改应该立即更新到数据库,那么显式保存可能会让用户感到麻烦。实现自动保存应该很容易!只需使用一个检测到行内容已更改的事件,使用 TableAdapter
的 Update
方法,然后就可以了。不幸的是,如果您尝试这样做,ADO.NET 会遇到一些奇怪的内部错误。
让我们仔细看看一些直观的解决方案(如果您赶时间,可以跳到 解决方案)。
DataGridView 事件
DataGridView
将是检测 DataGridView
中行已更改的最明显选择。但是 DataGridView
主要关注单元格,显示其内容,用户交互并将更改后的数据写回 DatSet.DataTable.DataRow
。诸如 DataGridView_RowValidated
之类的事件会出于所有可能的原因触发,而不一定是用户更改了数据。
会有 DataGridView_CellEndEdit
事件指示更改。但是此时使用 TableAdapter.Update()
会搞乱 ADO.NET。数据库的更新将在从 DataView
复制到 DataTable
的中间发生。这两种活动都会改变 DataRow
的状态。在复制过程中间进行更新会阻止复制操作正常完成(我猜 ADO.NET 不支持重入)。
BindingSource 事件
DataGridView
的数据绑定是在 BindingSource
中完成的,这是检测单元格内容何时更改的正确位置。
private void BindingSource_CurrentItemChanged(
object sender, EventArgs e)
{
DataRow ThisDataRow =
((DataRowView)((BindingSource)sender).Current).Row;
if (ThisDataRow.RowState==DataRowState.Modified) {
TableAdapter.Update(ThisDataRow);
}
}
如果您尝试此代码,它将起作用,但只对第一个更改的记录起作用!在更新第二行时,您会收到一个奇怪的错误消息,基本上该行似乎是空的。当您通过调试器检查时,在更新之前,该行包含有意义的数据,并且仅在运行时错误之后,它似乎为空。更新甚至成功将第二条记录写入了数据库。
DataTable 事件
如果 BidingSource
不起作用,那么使用 DataSet.DataTable
的事件怎么样?毕竟,对 DataRow
的任何更改都应该写回数据库,无论谁做的。代码可能看起来像这样
void Table_RowChanged
(object sender, DataRowChangeEventArgs e)
{
if (e.Row.RowState == DataRowState.Modified)
{
TableAdapter.Update(e.Row);
}
}
这次,您将立即遇到运行时错误。在 Update
尝试再次更改 DataRow
的状态之前,ADO.NET 尚未完成更改 DataRow
。
解决方案
似乎 ADO.NET 不希望在完全将更改从 DatRowView
复制到 DataTable
之前被对数据库的行更新打断。任何与行更改相关的事件都不能用于将行保存到数据库。因此,解决方案必须是使用在行复制后触发的事件,并且该事件不应与行更改相关!好吧,那么让我们只使用 BindingSource
的 PositionChanged
事件。当用户导航到下一行时,它会触发。因此,挑战在于记住最后一行是哪一行,检查它是否被修改,并在需要时更新数据库。不要忘记在 Form
关闭时执行相同的操作,因为当 Form
关闭时,PositionChanged
事件不会触发。
public partial class MainForm: Form {
public MainForm() {
InitializeComponent();
}
private void MainForm_Load(
object sender, EventArgs e)
{
this.regionTableAdapter.Fill(
this.northwindDataSet.Region);
// resize the column once, but allow the
// users to change it.
this.regionDataGridView.AutoResizeColumns(
DataGridViewAutoSizeColumnsMode.AllCells);
}
//tracks for PositionChanged event last row
private DataRow LastDataRow = null;
/// <SUMMARY>
/// Checks if there is a row with changes and
/// writes it to the database
/// </SUMMARY>
private void UpdateRowToDatabase() {
if (LastDataRow!=null) {
if (LastDataRow.RowState==
DataRowState.Modified) {
regionTableAdapter.Update(LastDataRow);
}
}
}
private void regionBindingSource_PositionChanged(
object sender, EventArgs e)
{
// if the user moves to a new row, check if the
// last row was changed
BindingSource thisBindingSource =
(BindingSource)sender;
DataRow ThisDataRow=
((DataRowView)thisBindingSource.Current).Row;
if (ThisDataRow==LastDataRow) {
// we need to avoid to write a datarow to the
// database when it is still processed. Otherwise
// we get a problem with the event handling of
//the DataTable.
throw new ApplicationException("It seems the" +
" PositionChanged event was fired twice for" +
" the same row");
}
UpdateRowToDatabase();
// track the current row for next
// PositionChanged event
LastDataRow = ThisDataRow;
}
private void MainForm_FormClosed(
object sender, FormClosedEventArgs e)
{
UpdateRowToDatabase();
}
}
事件分析
作为奖励,请查找用户更改 DataGridView
中单元格内容时涉及的事件跟踪。
DataGridView_CellBeginEdit
CellEditMode: False
DataGridView_CellValidating
CellEditMode: True
DataTable_ColumnChanging
RowState: Unchanged; HasVersion 'DCOP'
DataTable_ColumnChanged
RowState: Unchanged; HasVersion 'DCOP'
DataGridView_CellValidated
CellEditMode: True
DataGridView_CellEndEdit
CellEditMode: False
DataGridView_RowValidating
CellEditMode: False
DataTable_RowChanging
RowState: Unchanged; HasVersion 'DCOP'
BindingSource_CurrentItemChanged
RowState: Modified ; HasVersion 'DCO '
BindingSource_ListChanged
RowState: Modified ; HasVersion 'DCO '
DataTable_RowChanged
RowState: Modified ; HasVersion 'DCO '
DataGridView_RowValidated
CellEditMode: False
DataGridView_Validating
CellEditMode: False
DataGridView_Validated
CellEditMode: False
DataRow Versions:
D: Default
C: Current
O: Old
P: Proposed
使用代码
在运行示例应用程序之前,请打开解决方案资源管理器来更改 NorthwindConnectionString
。DataSource
应指向您的 SQL Server 和 Northwind 数据库。
应用程序运行后,更改区域的名称并移到另一行。这将把区域名称保存到数据库。在数据库中检查或关闭并重新启动应用程序以查看更改是否真的已存储。不要忘记将区域名称改回其原始值。
结论
早期 ADO.NET 版本中存在同样的问题。我没有尝试过,但描述的方法也应该适用于早期版本,只需使用 CurrencyManager
的事件即可。
历史
- 2006 年 1 月 27 日:原始发布。