如何将 DataGridView 中的编辑更新为 table

How to update edits in DataGridView to table

我已将 DataGridView 绑定到 .Net 5.0 WinForms 项目中的 SQL 服务器 table。显示数据效果很好。

我想在移动到 DataGridView 中的另一行后立即更新数据库的版本。但是我还没有找到一种方法来做到这一点。 提出的解决方案 here seems not to work with an OleDbDataAdapter. The Update method does not accept a DataRow. Examples in DOCS 需要一个我尽量避免的数据集。其他示例使用按钮来保存更改。

数据加载如下:

var dataAdapter = new OleDbDataAdapter(sqlQueryString, connString);
var dataTable = new DataTable();
dataAdapter.Fill(dataTable);                // fill data table from SQL server
var bindingSource = new BindingSource();
bindingSource.PositionChanged += new System.EventHandler(bindingSource_PositionChanged);
bindingSource.DataSource = dataTable;       // connect binding source to data table
dataGridView.DataSource = bindingSource;    // connect DataGridView to binding source

为了更新,我终于尝试了这个:

private void bindingSource_PositionChanged(object sender, EventArgs e)
{
    DataRow dataRow = ((DataRowView)((BindingSource)sender).Current).Row;
    if (dataRow.RowState == DataRowState.Modified)  // this is successful
    {
        dataAdapter.Update(dataRow);    // compile error
    }
}

我收到编译错误

Cannot convert from 'System.Data.DataRow' to 'System.Data.DataRow[]'.

如有任何提示,我们将不胜感激。

你可以使用foreach循环。

private void AddInfo()
{
    // flag so we know if there was one dupe
    bool updated = false;

    // go through every row
    foreach (DataGridViewRow row in dgv_Purchase.Rows)
    {
        // check if there already is a row with the same id
        if (row.Cells["Pro_ID"].ToString() == txt_ProID.Text)
        {
            // update your row
            row.Cells["Purchase_Qty"] = txt_Qty.Text;

            updated = true;
            break; // no need to go any further
        }
    }

    // if not found, so it's a new one
    if (!updated)
    {
        int index = dgv_Purchase.Rows.Add();

        dgv_Purchase.Rows[index].Cells["Purchase_Qty"].Value = txt_Qty.Text;
    }
}

MVVM

在现代编程中,有将模型与视图分离的趋势。这种分离使得更改数据显示方式变得更加容易,而无需更改模型。您还可以更改模型的某些部分而无需更改显示。无需启动表单程序就可以更轻松地重用模型和对其进行单元测试。

在 WPF 中,模型和视图之间的这种分离几乎是强制执行的。使用 winforms 时,您必须注意不要将它们混合超过需要。

为了将这两者分开,需要适配器代码将您的模型粘合到您的视图。此适配器代码通常称为视图模型。这三个的缩写通常称为 MVVM。考虑熟悉一下 MVVM 的思想。

在数据源中使用 BindingList

如果您想将模型与视图分开,您需要方法来获取必须从数据库中显示的数据,以及更新项目的数据。

我不知道您将在 DataGridView 中显示什么,但我们假设它是一系列产品,如下所示:

class Product
{
    public int Id {get; set;}
    public string ProductCode {get; set;}
    public string Name {get; set;}
    public string Description {get; set;}
    public decimal Price {get; set;}
    ...
}

您将有方法获取必须显示的产品,并更新一个产品,或者一次更新多个产品:

IEnumerable<Product> FetchProductsToDisplay(...)
{
    // TODO: fetch the products from the database.
}

void UpdateProduct(Product product) {...}
void UpdateProducts(IEnumerable<Product> products) {...}

实现超出了这个问题的范围。顺便说一下,你有没有注意到,因为我把获取和更新数据放在不同的过程中,所以我隐藏了产品的保存位置?它可以在 SQL 服务器中,但如果您愿意,它也可以是 CSV 或 XML 文件,甚至是字典,这对于单元测试来说可能很方便。

此外:您可以在不使用表单的情况下对这些方法进行单元测试。

您使用 visual studio 设计器添加了列并定义了哪个列应显示哪个产品 属性。您也可以使用 属性 DataGridViewColumn.DataPropertyName

在构造函数中完成此操作
public MyForm()
{
    InitializeComponents();

    this.columnProductCode.DataPropertyName = nameof(Product.ProductCode);
    this.columnName.DataPropertyName = nameof(Product.Name);
    ...
}

您无需为无论如何都不会显示的属性设置 DataPropertyName。

现在要显示产品,只需将产品分配给 DataGridView 的数据源即可:

var productsToDisplay = this.FetchProductsToDisplay(...);
this.dataGridView1.DataSource = productsToDisplay.ToList();

这将显示产品。但是,操作员所做的更改:添加/删除/编辑行不会更新。如果您需要此功能,则产品需要放入一个实现 IBindingList 的对象,例如(惊喜!)BindingList<Product>:

private BindingList<Product> DisplayedProducts
{
    get => (BindingList<Product>)this.dataGridView1.DataSource;
    set => this.dataGridView1.DataSource = value;
}

初始化 DataGridView:

private void DisplayProducts()
{
    this.DisplayedProducts = new BindingList<Product>(this.FetchProductsToDisplay().ToList());
}

现在,只要操作员对 DataGridView 进行任何更改:添加/删除行,或更改行中的显示值,这些更改都会反映在 DisplayedProducts

例如,如果操作员单击 Apply Now 表示他已完成对产品的编辑:

private void OnButtonApplyNow_Clicked(object sender, ...)
{
    var products = this.DisplayedProducts;
    // find out which Products are Added / Removed / Changed
    this.ProcessEditedProducts(products);
}

当然,您可以通过编程方式添加/删除/更改显示的产品:

void AddProductsToDisplay()
{
    Product product = this.DisplayedProducts.AddNew();
    this.FillNewProduct(product);
}

回到你的问题

问问自己:位置一改变就更新数据库是否明智?

如果操作员开始打字,然后记得他可以复制粘贴项目,他将停止打字,转到其他控件进行复制,然后继续通过粘贴编辑单元格。也许他会去其他行查看信息以决定在单元格中放入什么。

另一种情况:产品A和产品B的描述需要调换。考虑为此所需的操作员操作。什么时候更新数据库比较明智?您何时确定操作员对新数据感到满意?

因此,一旦编辑了一行就更新数据库是不明智的。操作员应明确表明他已完成编辑。

private void OnButtonOk_Clicked(object sender, ...)
{
    var products = this.DisplayedProducts;
    // find out which Products are Added / Removed / Changed
    this.ProcessEditedProducts(products);
}

进一步改进

一旦您使用数据源将数据(模型)与其显示方式(视图)分开,就可以很容易地访问显示在当前行或选定行中的产品:

Product CurrentProduct => (Product) this.dataGridView1.CurrentRow?.DataBoundItem;

IEnumerable<Product> SelectedProducts = this.dataGridView1.SelectedRows
    .Cast<DataGridViewRow>()
    .Select(row => row.DataBoundItem)
    .Cast<Product>();

我终于找到了遗漏的 2 行:

private SqlCommandBuilder commandBuilder;   // on UserControl level
commandBuilder = new SqlCommandBuilder(dataAdapter);    // when loading data

有一本书帮助了我:Michael Schmalz,C# 数据库基础,O'Reilly

奇怪的是 SqlDataAdapter 的 DOCS 参考没有提到 SqlCommandBuilder。

感谢所有为新贡献者花费宝贵时间的人。