EF Core - Disposable DbContext and Attach() - or - DbContext as member - or - Disconnected Entities

.net c# datagrid entity-framework-core wpf

Question

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
1
8/25/2017 7:16:38 AM

Accepted Answer

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();
    }
}
1
8/24/2017 3:48:28 PM

Popular Answer

I don't like to have a DbContext opened every time when CollectionChanged 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).



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow