>我正在使用VS 2019,带有Win表单和实体框架核心5.0的c#应用程序
在我的应用程序中,我有一个System.Windows.Forms.DataGridView,用于显示和更新MySQL数据库中的数据。 通过使用 System.Windows.Forms.BindingSource myBindingSource 将数据绑定到 DataGridView,并使用
myDbContext.SomeEntities.Load();
myBindingSource.DataSource = myDbContext.SomeEntities.Local.ToBindingList();
这确实正确显示数据,一旦我更改了网格中的一些数据并调用myDbContext.SaveChanges(),它就会将数据保存到数据库中。
因此,只要应用程序独立运行,它就可以正常工作。
但是,我想要实现的是,每当我的应用外部的任何其他操作更改数据时,包含网格的表单都会刷新数据。因此,如果对数据的任何更新发生在我的应用程序外部,我希望这些更改立即在打开的窗体中可见,而无需用户关闭并重新打开包含 DataGridView 的窗体。 我知道,当然,我需要为这些变化提供一个触发器。这可能是计时器或外部信号。目前它是一个计时器。
在计时器中,我做了一个
foreach( var rec in (BindingList<SomeEntities>)this.DataSource)
{
DbContext.Entry(rec).Reload();
}
之后我做了一个
CurrencyManager cm = (CurrencyManager)((myDataGridView).BindingContext)[(ctrl as DataGridView).DataSource];
if (cm != null) cm.Refresh();
这适用于现有记录的外部更新。 但是,如果插入或删除了记录,则会失败。在外部插入时,新记录在现有 BindingList 中根本不是已知的,因此不会刷新;从外部删除记录时,重新加载失败(因为它不再存在于数据库中)。 对于正在发生的事情,两者都是可以理解的。
不仅刷新现有实体,而且刷新集合 myDbContext.SomeEntities 内容的正确方法是什么。
在寻找答案时,我经常阅读"使用较短的 DbContext 生存期"。可以理解,但我确实需要 DbContext 来调用 myDbContext.SaveChanges() 以保存网格中所做的任何更改。我吗?还是有别的办法?如果 DbContext 仅在加载网格期间使用,如何使用常规数据绑定将其用作网格的数据源?
在 EntityFramework 6 中,有
_myObjectContext.RefreshAsync(RefreshMode.StoreWins, GetAll())
不知道这是否会有所帮助,因为我没有尝试使用 EntityFramework 6,但在 EF core 中,无论如何都没有与此等效的。那么有什么建议吗?
我认为在这种情况下 - 当您一直打开数据网格时(据我所知),这应该更多地手动完成。
例如,使用 MVVM,我将创建一个具有一些可观察视图项集合的 ViewModel,例如:
假设这是您的数据库模型:
public class DbItem
{
public int Id {get;set;}
public string Name {get;set;}
}
现在,我将创建一些要在视图模型中使用的数据:
public class ItemData: INotifyPropertyChanged
{
public ItemData(DbItem item)
{
id = item.Id;
name = item.Name; //notice that I use here backup field
}
public bool Modified {get; private set;}
int id;
public int Id
{
get { return id; }
set
{
if(id != value)
{
id = value;
NotifyPropertyChanged();
}
}
}
string name;
public string Name
{
get { return name; }
set
{
if(name != value)
{
name = value;
NotifyPropertyChanged();
}
}
}
}
在这里,我使用了INotifyPropertyChanged,但这一切都归结为您的需求。您可以将字段Modified
更新为 true,或者每次记录更改时,只需在数据库中更新它(SQL UPDATE/INSERT)
现在在我的视图模型中,我会做:
public class ViewModel
{
public ObservableCollection<ItemData> DataSource {get; set;}
}
我怀疑您是否可以在WinForms中使用ObservableCollection,就像在WPF中使用一样,因此我认为您可以创建一些绑定集合,而不是这样做。
无论如何,现在当您从数据库中读取数据时,您应该将它们转换为您的项目:
public class ViewModel
{
public void ReadData()
{
DataSource.Clear();
List<DbItem> dbItems = service.GetDataFromDatabaseWithNoTracking();
foreach(var item in dbItems)
{
DataSource.Add(new ItemData(item));
}
}
}
现在,当您需要更新某些内容时,只需:
public class ViewModel
{
public void UpdateData(ItemData data)
{
//if data.Modified...
DbItem db = new DbItem();
db.Id = data.Id;
db.Name = data.Name;
service.UpdateItem(db);
}
}
在 EF 中:
public void UpdateItem(DbItem item)
{
var entry = dbContext.Entry(item);
dbContext.Save();
}
归根结底,您将不会跟踪数据库中的记录。您应该手动执行此操作。
您如何看待此解决方案?
我不确定这是否是一个不错的界面。假设操作员正在愉快地编辑一行,突然您决定重新加载表:他的所有更改都丢失了? 如果运算符只是将一个值从 4 更改为 5,而其他人只是将相同的值从 4 更改为 3,我们应该保留哪一个?如果其他人决定删除一行怎么办,因为他认为
所以我建议不要进行自动刷新,为此添加一个按钮。类似于浏览器中的重新加载按钮。添加 F5 按钮开始刷新,您的界面与大多数可能显示过时数据的 Windows 应用程序兼容。
但是,这样做的缺点是,如果运算符编辑了一个值,其他人也编辑了,甚至删除了,您应该决定要做什么。我的建议是:询问接线员:
- 如果操作员编辑了其他人也编辑的行:我们应该保留哪一行?
- 如果操作员编辑了其他人删除的行?
- 如果操作员删除了其他人刚刚更改的行?
假设您的数据网格视图显示Products
:
class Product
{
public int Id {get; set;}
public string Name {get; set;}
public decimal Price {get; set;}
public int Stock {get; set;}
...
}
使用 Visual Studio 设计器,你已添加 DataGridView 和一些列。在构造函数中,可以将属性分配给列:
public MyForm() : Form
{
InitializeComponents()
this.columnId.DataPropertyName = nameof(Product.Id);
this.columnName.DataPropertyName = nameof(Product.Name);
this.columnPrice.DataPropertyName = nameof(Product.Price);
...
}
您还需要一种方法来获取必须从数据库中显示的产品,然后再次重新获取它们,以查看哪些产品已更改。当然,您隐藏了它们来自数据库。
IEnumerable<Product> FetchProductsToDisplay()
{
... // Fetch the data from the database; out-of-scope of this question
}
若要显示提取的产品,请使用数据源和绑定列表:
BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.dataGridView1.DataSource;
set => this.dataGridView1.DataSource = value;
}
最初,您展示产品:
void OnFormLoading(object sender, ...)
{
this.DisplayedProducts = this.FetchProductsToDisplay()
}
因此,现在操作员可以愉快地编辑现有产品,也许可以添加一些新产品或删除一些产品。过了一会儿,他想用其他人输入的数据刷新产品:
private void OnButtonRefresh_Clicked(object sender, ...)
{
this.UpdateProducts();
}
private void UpdateProducts()
{
IEnumerable<Product> dbProducts = this.FetchProductsToDisplay();
IEnumerable<Product> editedProducts = this.DisplayedProducts();
我们必须将数据库中的产品与 DataGridView 中的产品进行比较。按 ID 匹配产品:相同的 ID,期望相同的产品。
- Id 为零,因此仅在 editedProducts 中,它已由操作员添加,但尚未添加到数据库中。
- 非零 Id 仅在编辑的产品中:已被其他人删除
- Id 在 dbProducts 和 editeProducts 中,但值不相等:由操作员或其他人编辑
一个困难的:
- Id 仅在 dbProducts 中:它已被其他人添加,或由操作员删除。
为了能够向操作员询问正确的问题,似乎我们还需要在操作员开始编辑之前显示的产品。
private IEnumerable<Product> OriginalProducts => ...
因此,现在我们能够检测操作员和/或在数据库中添加/删除/更改了哪些产品:
Id in originalProducts editedProducts dbProducts
yes yes yes compare values to detect edits
yes yes no someone else deleted
yes no yes operator deleted.
yes no no both operator and someone else deleted
no no yes someone else added
no yes no operator added
no yes yes both operator and someone else added
检测更改的过程:
private void DetectChanges(IEnumerable<Product> originalProducts,
IEnumerable<Product> editedProducts,
IEnumerable<Product> dbProducts)
{
// do a full outer join on these three sequences:
var originalDictionary = originalProducts.ToDictionary(product => product.Id);
var dbDictionary = dbProducts.ToDictionary(product => product.Id);
// some Ids in editedProducts have value zero:
var addedProducts = editedProducts.Where(product => product.Id == 0);
var editedDictionary = editedProducts.Where(product => product.Id != 0)
.ToDictionary(product => product.Id);
var allUsedIds = originalDictionary.Keys
.Concat(dbDictionary.Keys)
.Distinct();
注意:所有带有 Id != 0 的编辑产品在上次获取时已经存在于数据库中,因此它们的 ID 已经在 originalDictionary 中。我使用"不同"删除了重复的 ID
foreach (int id in allUsedIds)
{
bool idInDb = dbDictionary.TryGetValue(id, out Product dbValue)
bool idInOriginal = originalDictionary.TryGetValue(id, out Product originalValue);
bool idInEdited = editedDictionary.TryGetValue(id, out Product editedValue);
使用上面的表格和值 idInDb/idInOriginal/idInEdited 来了解该值是否已添加或删除/更改。
有时,只需在 DataGridView 中添加/编辑/更改值就足够了;有时您必须询问操作员。
建议:
其他人所做的更改:
- 如果由其他人添加:只需将其添加到 DataGridView 中即可
- 如果由其他人删除,而不是由操作员编辑:只需从 DataGridView 中删除
- 如果被其他人删除,由操作员编辑:询问操作员该怎么做
操作员所做的更改:
如果由运算符添加 (Id == 0):插入数据库
如果被运算符删除,请将其从数据库中删除
如果由操作员而不是其他人编辑:更新数据库
操作员和其他人所做的更改:询问操作员要保留哪一个。更新数据库或数据网格视图。
检测产品变化:使用IE质量比较器
若要检测更改,需要一个按值进行比较的相等比较器:
public class ProductComparer : EqualityComparer<Product>
{
public static IEqualityComparer<Product> ByValue {get} = new ProductComparer();
public override bool Equals(Product x, Product y)
{
if (x == null) return y == null; // true if both null
if (y == null) return false; // because x not null
if (Object.ReferenceEquals(x, y) return true; // same object
if (x.GetType() != y.GetType()) return false; // different types
return x.Id == y.Id
&& x.Name == y.Name // or use StringComparer.CurrentCultureIgnoreCase
&& x.Price == y.Price
&& ...
}
public override int GetHashCode(Product x)
{
if (x == null) return 87742398;
// for fast hash code: use Id only.
// almost all products are not edited, so with same Id have same values
return x.Id.GetHashCode();
}
}
用法: IEqualityComparer productValueComparer = ProductComparer.ByValue;
检测哪些产品在哪个字典中后:
if (idInDb && idInEdited && )
{
// one of the Products in the DataGridView already exists in the database
// did the operator edit it?
bool operatorChange = !productValueComparer.Equal(originalValue, editedValue);
bool dbChange = !productValueComparer.Equal(originalValue, dbValue);
if (!productValueComparer.Equal(operatorValue, dbValue);)
{
// operator edited the Product; someone change it in the database
// ask operator which value to keep
...
}
else
{
// operator edited the Product; the same value is already in the database
// nothing to do.
}
因此,对于每个 Id,检查它是否已在数据库中,检查它是否仍在数据库中,并检查操作员是否将其保存在 dataGridView 中。还要检查更改了哪些值,以检测是否需要添加/删除/更新数据库或 DataGridView,或者您可能不需要执行任何操作。