C# 和 SQL Server Express 和 DataGridView 在 table 插入后更新

C# and SQL Server Express and DataGridView update after table insert

我是 SQL 服务器数据库的新手,我在 Winforms 应用程序中有一个简单的数据网格视图。 datagridview 绑定到 SQL 服务器数据库。

我需要在按下按钮时更新 datagridview 和后端数据库。

private void btnAdd_Click(object sender, EventArgs e)
{
     var str = txtNewSource.Text;
     var data = this.tBL_SourceTableAdapter.GetData();
     var length = data.Rows.Count;
     this.tBL_SourceTableAdapter.Insert(length + 1, str);
     sourceDataGridView.refresh(); // This does not refresh the data in the form!
 }

我想用刚添加到数据库中的新数据更新绑定的 datagridview。我可以看到数据已添加到数据库中。如果我用 datagridview 关闭 window 并重新打开它,新添加的值是可见的。

如何在插入到绑定数据库后刷新 datagridview 中的数据table?

我愿意接受能够达到预期结果的其他方法。我不允许用户编辑 datagridview 中的值,因为并非 table 中的所有列都对用户可见,我需要保持索引顺序正确。

我尝试过的事情:

我试图在 gridview 中添加一个新行,如下所示:

sourceDataGridView.rows.add(); // I get a runtime error cannot add row to bound datagrid.
sourceDataGridView.rows[n].cells[0].value = str; 

我尝试重置数据网格上的数据源,但这也不起作用。

谢谢。

您不能以这种方式刷新网格视图。您必须清除现有数据网格的行并将其再次与数据源绑定

将数据与其显示方式分开

在现代编程中,有一种趋势是将数据(模型)与数据向操作员显示的方式(视图)分开。

这使您可以自由更改显示而无需更改模型。例如,如果您想要显示较少的列,或者想要以不同的颜色显示负数,或者您想要以 Excel sheet.

的形式显示

同样,您可以在不更改视图的情况下更改模型:如果您想从 CSV 文件或 Json 中获取数据,而不是从数据库中获取数据,甚至可能获取数据来自互联网:你不想改变你根据这些数据做出的看法。

通常您需要适配器来使模型适合显示器。此适配器通常称为 ViewModel。这三项一起缩写为 MVVM。考虑阅读有关此的一些背景信息。

模型和视图分离后,您将有方法:

  • 从存储库中获取数据。存储库是您的数据库,但它可以是任何东西:CSV? Json、XML、互联网或单元测试字典。
  • 将数据写入存储库
  • 显示数据
  • 获取编辑后的数据

也许您需要一种方法来找出哪些已编辑数据已更改,因此需要在您的存储库中进行更新。

您写道您是 SQL 服务器数据库的新手。如果我看看你的问题的其余部分,似乎读取和写入数据库不是你的问题。因此,我不会深入探讨这一点。我将首先写下如何使用普通的旧 SQL 和 DbReaders 访问数据库。如果您已经知道如何操作,则可以跳过本章。

之后,我将解释如何显示获取的数据。

正在访问数据库

您是使用普通旧 SQL 获取数据,还是使用 entity framework?因为您将它隐藏在您的存储库中,所以这对外界来说并不重要。如果您更改获取和更新数据的方式,它不会改变。

唉,你忘了写你在 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;}
     public int StockCount {get; set;}
}


interface IRepository
{
    IEnumerable<Product> FetchProductsToDisplay(...);
    void UpdateProducts(IEnumerable<Product> product);
}

class Repository : IRepository
{
   // TODO: implement
}

如果您使用普通的旧 SQL,那么获取产品将是这样的:

const string sqlTextFetchProducts = @"SELECT TOP ..."
    + @" Id, ProductCode, Name, ..."
    + @" FROM Products;";

确切的 SQL 文本因您使用的数据库管理系统而异。例如 SQLight 使用 Limit 30 而不是 TOP 30.

幸运的是,您将模型与视图分开,并将这些详细信息隐藏在存储库中 class,因此如果您决定使用不同的方法访问数据库,存储库之外的任何内容都不会改变。

您可能还需要 Left Outer Join、GroupBy、Where、Order 等。确切的 SQL 有点超出问题的范围。

需要记住的重要一点是,使用运算符或其他外部源可能提供的某些输入的值来更改 SQL 字符串是非常危险的。如果您从未听说过这个,请阅读 Dangers of SQL injection

Always make your SQL a const string. Use variables to insert operator input.

例如,如果您只想在某个仓库位置展示产品:

const string sqlTextFetchProducts = @"SELECT ... FROM Products;";
    + @" WHERE WareHouseLocationId = @WareHouseLocationId"

好的,让我们实现 FetchProductsToDisplay:

private string DbConnectionString => ...; // gets the database connection string

IEnumerable<Product> FetchProductsToDisplay(int wareHouseLocationId);
{
    const string sqlTextFetchProducts = @"...;";

    using (var dbConnection = new SQLiteConnection(this.DbConnectionString))
    {
        using (var dbCommand = dbConnection.CreateCommand())
        {
            dbCommand.CommandText = sqlTextFetchProducts ;
            dbCommand.Parameters.AddWithValue("@WareHouseLocationId", wareHouseLocationId);
            dbConnection.Open();

            // Execute the query and returns products one by one
            using (SQLiteDataReader dbReader = dbCommand.ExecuteReader())
            {
                while (dbReader.Read())
                {
                    Product fetchedProduct = new Product
                    {
                        Id = dbReader.GetInt64(0),
                        ProductCode = dbReader.GetString(1),
                        ...
                        Price = dbReader.GetDecimal(4),
                        StockCount = dbReader.GetInt32(5),
                    };
                    yield return fetchedProduct;
                }
            }
        }
    }
}

这里有几件有趣的事情。

Return IEnumerable

我 return 一个 IEnumerable:如果我的调用者只使用前几项,将所有获取的数据转换为产品是没有用的。

Product firstProduct = this.FetchProducts(...).Take(25).ToList();

为此创建一个特殊的 SQL 可能更有效,但对于这个示例,您可以看到,您不需要将所有获取的数据转换为产品。

使用参数

SQL 文本不变。参数有一个前缀 @,以区别于文字文本。这只是约定,您可以更改它,但这可以很容易地找到参数。

参数的值一个一个地添加,例如,如果您只想要 WareHouseLocation 10 的产品,其 StockCount 至少为 2,最高价格为 25 欧元,您可以更改 SQL 这样它包含 @WareHouseLocation, @StockCount, @Price 并且您添加:

IEnumerable<Product> FetchProductsToDisplay(
    int wareHouseLocationId,
    int minimumStockCount,
    decimal maximumPrice)
{
    using(...)
    ...

   dbCommand.Parameters.AddWithValue("@WareHouseLocationId", wareHouseLocationId);
dbCommand.Parameters.AddWithValue("@StockCount", minimumStockCount);
dbCommand.Parameters.AddWithValue("@Price", maximumPrice);
...

将获取的数据转换为产品 执行查询后,您使用 DbReader 将获取的数据逐一放入 Products。

while (dbReader.Read())

Return只要有未读取的提取数据就为真。

Id = dbReader.GetInt64(0),
ProductCode = dbReader.GetString(1),
...

您的 SQL 文本 Select Id, ProductCode, ... From ... 中提取的项目有一个索引,Id 的索引为 0,ProductCode 的索引为 1,等等。使用正确的 dbReader.Get... 转换提取的项目进入正确的类型。

将 dbReader 中获取的数据转换为您的 class 的确切方法可能因数据库管理系统而异,但我想您会明白要点。

当然,您还需要一种更新产品的方法。这如果相当相似,但是您将使用 `

而不是 ExecuteReader
public void UpdateProductPrice(int productId, decimal price)
{
    const string sqlText = "UPDATE " + tableNameProducts
        + " SET Price = @Price"
        + " WHERE Id = @Id;";

    using (SQLiteCommand dbCommand = this.DbConnection.CreateCommand())
    {
        dbCommand.CommandText = sqlText;
        dbCommand.Parameters.AddWithValue("@Id", productId);
        dbCommand.Parameters.AddWithValue("@Price", productPrice);
        dbCommand.ExecuteNonQuery();
    }
}

由您来实施 void UpdateProduct(Product product)

进入 ViewModel!

展示产品

现在我们有了获取必须显示的产品的方法,我们可以尝试显示获取的产品。虽然您可以通过直接编辑 DataGridViewCells 来使用它,但使用起来更容易 DataGridView.DataSource:

使用 visual studio 设计器,您添加了 DataGridView 及其列。使用 属性 DataGridView.DataPropertyName 定义哪个列应显示哪个产品 属性。这也可以使用 visual studio 设计器来完成,但您也可以在构造函数中这样做:

public MyForm()
{
    InitializeComponent();

    this.columnProductId.DataPropertyName = nameof(Product.Id);
    this.columnProductName.DataPropertyName = nameof(Product.Name);
    ...
    this.columnProductPrice.DataPropertyName = nameof(Product.Price);
}

此方法的优点是,如果您将来决定更改 Product 属性的标识符,如果您忘记在此处更改它们,编译器会检查它们。当然:visual studio 会自动更改此处的标识符。如果您使用设计器,则不会这样做。

现在要展示的产品是一条线:

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

这将根据您在设计器中使用的视图规范显示产品:如果您想要价格的特殊格式,或者低库存的红色背景,模型中没有任何变化,也没有视图模型。

private IRepository Repository {get;} = new Repository();

private IEnumerable<Product> FetchProductsToDisplay()
{
    return this.Repository.FetchProductsToDisplay(...);
}

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

还有宾果游戏!所有产品都以您在视图中定义的格式显示。操作员所做的所有更改:添加/删除/更改显示的产品都会在 BindingList 中自动更新。

例如:如果操作员指示他已完成更改产品,他可以按“确定”或“立即应用”按钮:

private void OnButtonApplyNow_Clicked(object sender, ...)
{
    Collection<Product> editedProducts = this.Displayedproducts();

    // find out which Products are changed and save them in the repository
    this.ProcessEditedProducts(editedProducts);
}

现在剩下的唯一挑战是:如何找出编辑了哪些展示产品。由于操作员不会每秒按几次确定按钮,我只是从数据库中获取原始数据,并将它们与编辑后的数据进行比较,以决定是否需要更新。

我不会只更新所有内容,因为其他人可能以您可能决定不更新的方式更改了数据。例如,如果您的产品有 属性 IsObsolete,那么更改价格可能并不明智。

结论

通过将 ModelView 分开,View 已成为一堆单一的线性方法。大部分工作都是在模型中完成的。此模型可以在不使用 WinForms 的情况下进行单元测试。

您可以轻松更改数据的显示方式,而无需更改模型。如果较低的 StockCount 需要不同的背景颜色,则模型不会更改。 如果您想使用 WPF 而不是 Winforms,或者如果您决定通过 Internet 和 windows 服务访问您的数据,则无需更改模型。