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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (40投票s)

2006年1月26日

CPOL

4分钟阅读

viewsIcon

575825

downloadIcon

11251

将 DataGridView 中已更改的行自动保存到数据库似乎是一项基本任务,但实现起来却充满挑战。在此处阅读为什么最直观的方法会失败以及如何使其正常工作。

引言

SQL Enterprise Manager 多年来一直在这样做:每当用户更改表中的某一行时,它都会自动写回数据库表。为用户提供相同的功能实现起来很棘手,因为 DataSetBindingSource 之间的交互,这在 .NET 帮助文档中几乎没有记载。本文研究了一些直观的解决方案,并解释了它们为何不起作用。对涉及的事件的详细分析导致了最终解决方案,该解决方案出人意料地简单,正如任何好的解决方案都应该的那样。

背景

通常,用户需要显式保存他们的工作,就像在 Word 中保存文档一样。这种方法使用 BindingNavigator 的保存按钮,使用 DataRowViews 可以立即正常工作。但是,如果 DataRow 中的更改应该立即更新到数据库,那么显式保存可能会让用户感到麻烦。实现自动保存应该很容易!只需使用一个检测到行内容已更改的事件,使用 TableAdapterUpdate 方法,然后就可以了。不幸的是,如果您尝试这样做,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 之前被对数据库的行更新打断。任何与行更改相关的事件都不能用于将行保存到数据库。因此,解决方案必须是使用在行复制后触发的事件,并且该事件不应与行更改相关!好吧,那么让我们只使用 BindingSourcePositionChanged 事件。当用户导航到下一行时,它会触发。因此,挑战在于记住最后一行是哪一行,检查它是否被修改,并在需要时更新数据库。不要忘记在 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

使用代码

在运行示例应用程序之前,请打开解决方案资源管理器来更改 NorthwindConnectionStringDataSource 应指向您的 SQL Server 和 Northwind 数据库。

应用程序运行后,更改区域的名称并移到另一行。这将把区域名称保存到数据库。在数据库中检查或关闭并重新启动应用程序以查看更改是否真的已存储。不要忘记将区域名称改回其原始值。

结论

早期 ADO.NET 版本中存在同样的问题。我没有尝试过,但描述的方法也应该适用于早期版本,只需使用 CurrencyManager 的事件即可。

历史

  • 2006 年 1 月 27 日:原始发布。
© . All rights reserved.