问题
I'm not sure about how to correctly use the DbContext for Entities bound to a WPF DataGrid?
How do I correctly "re-attach" and save changes to the database for all the entities that were loaded to the datagrid during UserControl load?
I was using a DbContext as a member variable and ObservableCollection as DataSource for Datagrids. So everything was fine so far, no need to search for errors in the code below. Just to show what I have done so far.
// Old code - working perfectly as desired
private TestMenuDataContext _Db;
public ObservableCollection<Vendor> Vendors { get; set; }
private void ucGeneralSettings_Loaded(object sender, RoutedEventArgs e) {
//Do not load your data at design time.
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) {
_Db = new TestMenuDataContext();
_Db.Database.EnsureCreated();
Vendors = new ObservableCollection<Vendor>(_Db.Vendors);
Vendors.CollectionChanged += Vendors_CollectionChanged;
vendorDataGrid.ItemsSource = Vendors;
}
}
private void Vendors_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
_Db.Vendors.AddRange(e.NewItems.Cast<Vendor>());
foreach (var vendor in e.NewItems.Cast<Vendor>()) {
vendor.TimeStamp = DateTime.Now;
vendor.UserName = Environment.UserName;
}
break;
case NotifyCollectionChangedAction.Remove:
_Db.Vendors.RemoveRange(e.OldItems.Cast<Vendor>());
break;
}
}
private void SaveSettingsButton_Click(object sender, RoutedEventArgs e) {
var queryDeletedUsedVendor = _Db.TestMenu.Where(t => !Vendors.Any(v => v.Name== t.Vendor));
if (queryDeletedUsedVendor.Any()) {
_AppManager.AddStatusMessage($"Saving settings not possible. Vendor {queryDeletedUsedVendor.FirstOrDefault().Vendor} deleted but it is in use in the Test Menu!", State.Error);
return;
}
try {
_Db.SaveChanges();
_AppManager.AddStatusMessage("Settings saved", State.Ok);
}
catch (Exception ex) {
_AppManager.AddStatusMessage($"Saving data failed {ex.Message}", State.Error);
}
// fire delegate event to inform MainWindow
onDatabaseUpdated?.Invoke(this);
}
private void ucGeneralSettings_Unloaded(object sender, RoutedEventArgs e) {
if (_Db != null)
_Db.Dispose();
}
BUT, currently starting with MVVM and search how to correctly integrate EF Core. Now I have read several times:
Your DbContext lifetime should be limited to the transaction you are running.
E.g. here:c# entity framework: correct use of DBContext class inside your repository class
So taking this into account and changed the saving code to:
// new code
using (TestMenuDataContext db = new TestMenuDataContext())
{
foreach (Vendor v in Vendors) {
var test = db.Vendors.Attach(v);
bool isAlreadyInside = db.Vendors.Any(v2 => v2.Id == v.Id);
if (!isAlreadyInside)
db.Vendors.Add(v);
}
db.SaveChanges();
Do I really need to loop over all entities, attach every single entity and check manually for deleted or added entities? I don't like to have a DbContext opened every time when CollectionChanged event appears. I can't believe it should be this complicated... So currently I would prefer to go with the DbContext as member variable as used before...
If I'm googling correct the not implemented disconnected entities aren't intended to be used in a WPF app with DB-Server connection, they are meant to be used in n-tier environment. So this is not the topic to search for, correct?
Do I need disconnected entities?
Disconnected Entities on MSDN
回答1:
Side Note:
in MVVM ObservableCollection.CollectionChanged
is supposed to inform View about changes in Model, resp ViewModel. I would not recommend to let View modify ObservableCollection
and then use CollectionChanged
to reflect the changes in ViewModel. Try to keep ViewModel -> View notification flow, not the other direction.* Every change is done in ViewModel and reflected in the View.
First approach:
basically split your application logic and your data access, which is exactly what viewmodel is for.
public class YourPageViewModel
{
private readonly ObservableCollection<VendorItemVm> _deletedVendors = new ObservableCollection<VendorItemVm>();
public List<VendorItemVm> Vendors { get; } = new List<VendorItemVm>();
void Add()
{
Vendors.Add(new VendorItemVm
{
IsNew = true,
Id = new Guid(),
UserName = "New Vendor",
});
}
void Remove(VendorItemVm vendor)
{
Vendors.Remove(vendor);
_deletedVendors.Add(vendor);
}
async Task Load()
{
using(var db = new DbContext())
{
var vendors = db.Vendors.AsNoTracking().ToList();
foreach(var entity in vendors)
{
Vendors.Add(new VendorItemVm
{
Id = entity.Id,
Name = entity.Name,
});
}
}
}
async Task Save()
{
using (var db = new DbContext())
{
//convert viewmodels to entities
var newVendorsEntities = Vendors
.Where(v => v.IsNew)
.Select(v => new Vendor
{
Id = v.Id,
UserName = v.UserName,
TimeSpan = DateTime.Now,
})
.ToArray();
//add new entities
foreach (var vm in Vendors.Where(v => v.IsNew))
{
var entity = new Vendor
{
Id = vm.Id,
UserName = vm.UserName,
TimeSpan = DateTime.Now,
};
db.Vendors.Add(vendor);
}
//delete removed entities:
foreach(var vm in _deletedVendors)
{
var entity = new Vendor { Id = vm.Id };
db.Vendors.Attach(entity);
db.Ventors.Remove(entity);
db.Vendors.Add(vendor);
}
await db.SaveChangesAsync();
//reset change tracking
foreach (var vm in Vendors) vm.IsNew = false;
_deletedVendors.Clear();
}
}
}
Second approach:
In the previevious example we have basically implemented our own primitive Unit of Work pattern. However, DbContext is already implementation of UoW and change tracking pattern.
We will create instance of DBContext, but we will use it only for tracking Added/Removed entities:
public class YourPageViewModel
{
MyDbContext _myUoW;
public ObservableCollection<Vendor> Vendors { get; } = new ObservableCollection<Vendor>();
void Add()
{
var entity = new Vendor
{
Id = new Guid(),
UserName = "New Vendor",
};
Vendors.Add(entity)
_myUoW.Vendors.Add(entity);
}
void Remove(VendorItemVm vendor)
{
Vendors.Remove(vendor);
_myUoW.Vendors.Attach(entity);
_myUoW.Vendors.Add(entity);
}
async Task Load()
{
using(var db = new MyDbContext())
{
Vendors = db.Vendors.AsNoTracking.ToList();
foreach(var entity in vendors) Vendors.Add(entity);
}
_myUoW = new MyDbContext();
//if you want to track also changes to each vendor entity, use _myUoW to select the entities, so they will be tracked.
//In that case you don't need to attach it to remove
}
async Task Save()
{
//add new entities and delete removed entities
_myUoW.SaveChanges();
//reset change tracking
_myUoW.Dispose();
_myUoW = new MyDbContext();
}
}
回答2:
I don't like to have a
DbContext
opened every time whenCollectionChanged
event appears.
Then don't. Create a single TestMenuDataContext
in your view model and use this one as you did before.
So currently I would prefer to go with the
DbContext
as member variable as used before.
There is nothing stopping you from doing so, is it? Apparently, you do want a single TestMenuDataContext
per instance of your view model in this case. Just create a TestMenuDataContext
once in your view model, for example in its constructor, and use this one in your CollectionChanged
event handler. Or create the context in your save method.
The optimal lifetime of a DbContext
may certainly vary depending on your requirements. In general you should use short-lived contexts, but in this case it seems like you do want (and should use) the same context for all changes made to the entity objects in your DataGrid
.
The other option would of course be to create the context and attache your entities when the save button is pressed (and not every time the in-memory collection is modified).
来源:https://stackoverflow.com/questions/45862050/ef-core-disposable-dbcontext-and-attach-or-dbcontext-as-member-or-di