1

My WPF application consists of a main TabControl which renders multiple plugins, loaded via MEF. Technically, the ICollectionView of the plugins-collection is bound to the ItemsSource-property of the main TabControl. The IsSynchronizedWithCurrentItem-property is also set to True to keep track of the currently selected tab item.

To initialize the plugins, I subscribe the CurrentChanged-event of the ICollectionView to initialize the selected plugin via lazy loading. Since lots of the initialization tasks are async, I also declared the event handler as async.

private ICollectionView pluginsCv;

public ICollectionView PluginsCv
{
    get
    {
        if (pluginsCv == null)
        {
            pluginsCv = CollectionViewSource.GetDefaultView(Plugins);
            pluginsCv.CurrentChanged += async (sender, args) =>
            {
                await LoadCurrentSection();
            };
        }
        return pluginsCv;
    }
}

Tasks of the LoadCurrentSection-method:

  • Check if data was already loaded before.
  • If not, initialize the currently selected plugin, inter alia, reading data via EntityFramework.

LoadCurrentSection-method:

private async Task LoadCurrentSection()
{
    // If no tab is selected, select the first one
    if (PluginsCv.CurrentItem == null)
    {
        // Comment for stackoverflow: Another raise of the CurrentChanged-event does not seem to be the cause. Tried to prevent another execution of LoadCurrentSection via a bool flag, an stayed in this method (no return statement). Same result.
        PluginsCv.MoveCurrentToFirst();
        return;
    }

    // Get the ViewModel of the currently selected section/plugin
    var pluginViewModel = ((IPlugin)PluginsCv.CurrentItem).PluginElement.DataContext as ISettlementScheduleSection;

    // Initialize currently selected section/plugin
    if (pluginViewModel != null && !pluginViewModel.DataLoaded)
        await pluginViewModel.LoadData();
}

LoadData-method of the plugin:

public override async Task LoadData()
{
    // ...
    Costs = await costsService.GetCosts(SubProjectId, UnitTypeId);
    // ...
}

Every plugin holds it's own instances of service-classes for crud operations. Those disposable service classes hold and instance of the db-Context.

Little example:

public class CostsService : ServiceBase
{
    public CostsService()
        : base()
    { }

    public CostsService(MyDbContext db)
    {
        this.Context = db ?? throw new ArgumentNullException(nameof(db));
    }

    public async Task<IEnumerable<CostsDefinition>> GetCosts(Guid subProjectId, Guid unitTypeId)
    {
        return await Context.CostsDefinitions.Where(cd => cd.SubProjectId == subProjectId
                                                       && cd.UnitTypeId == unitTypeId)
                                             .OrderBy(cd => cd.Year).ThenBy(cd => cd.Month)
                                             .ToListAsync();
    }
}

The db-context (Context-property) is not static and is also not shared between the plugins.


The Problem: Sometimes, but not always, this way to initialize the plugins causes a System.NotSupportedException: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe. WPF seems to raise that event multiple times. To me it is unclear in what scenarios it gets raised. When the ICollectionView is first bound to the TabControl and the first item gets automatically selected? Also when I load and insert more plugins into the source collection? ...

Frequency of the bug: In this case, timing seems to play are role. The frequency of that bug changes when running without debugging or under different network conditions.

Workaround attempt: I tried to filter additional/unnecessary calls of the CurrentChanged-handler via a bool flag, but it didn't help in all cases. I just reduced the frequency a bit. I also tried to delegate every access to PluginsCv to the Dispatcher, but it didn't help. Also, I wrote debug information when pluginsCv is null. And I got that information only once, so the CurrentChanged-event must also be subscribed only once.


Exception details:

Source of exception: "EntityFramework"

Stacktrace:

   at System.Data.Entity.Internal.ThrowingMonitor.EnsureNotEntered()
   at System.Data.Entity.Core.Objects.ObjectQuery`1.System.Data.Entity.Infrastructure.IDbAsyncEnumerable<T>.GetAsyncEnumerator()
   at System.Data.Entity.Internal.Linq.InternalQuery`1.GetAsyncEnumerator()
   at System.Data.Entity.Infrastructure.DbQuery`1.System.Data.Entity.Infrastructure.IDbAsyncEnumerable<TResult>.GetAsyncEnumerator()
   at System.Data.Entity.Infrastructure.IDbAsyncEnumerableExtensions.ForEachAsync[T](IDbAsyncEnumerable`1 source, Action`1 action, CancellationToken cancellationToken)
   at System.Data.Entity.Infrastructure.IDbAsyncEnumerableExtensions.ToListAsync[T](IDbAsyncEnumerable`1 source, CancellationToken cancellationToken)
   at System.Data.Entity.Infrastructure.IDbAsyncEnumerableExtensions.ToListAsync[T](IDbAsyncEnumerable`1 source)
   at System.Data.Entity.QueryableExtensions.ToListAsync[TSource](IQueryable`1 source)
   at xyz.desk.app.ccm.business.Service.CostsService.<GetCosts>d__7.MoveNext() in D:\Dev\ThatCrazyPluginProject\desk\app\xyz.desk.app.ccm.business\Service\CostsService.cs:line 89
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at xyz.desk.app.ccm.plugin.costplan.constructionprogress.ViewModel.ContructionProgressCostPlanViewModel.<LoadData>d__58.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at xyz.desk.app.ccm.plugin.settlementscheduleshost.ViewModel.SettlementSchedulesHostViewModel.<LoadCurrentSection>d__67.MoveNext() in D:\Dev\ThatCrazyPluginProject\desk\app\xyz.desk.app.ccm.ui\ViewModel\SettlementSchedulesHostViewModel.cs:line 307
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at xyz.desk.app.ccm.plugin.settlementscheduleshost.ViewModel.SettlementSchedulesHostViewModel.<LoadData>d__65.MoveNext() in D:\Dev\ThatCrazyPluginProject\desk\app\xyz.desk.app.ccm.ui\ViewModel\SettlementSchedulesHostViewModel.cs:line 282
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at xyz.desk.app.ccm.ui.ViewModel.ProjectViewModel.<LoadCurrentSection>d__115.MoveNext() in D:\Dev\ThatCrazyPluginProject\desk\app\xyz.desk.app.ccm.ui\ViewModel\ProjectViewModel.cs:line 763
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at xyz.desk.app.ccm.ui.ViewModel.ProjectViewModel.<<get_PluginsCv>b__21_0>d.MoveNext() in D:\Dev\ThatCrazyPluginProject\desk\app\xyz.desk.app.ccm.ui\ViewModel\ProjectViewModel.cs:line 120

Any ideas to solve this issue?

3
  • 1
    Part of the problem might be that you are storing entity framework context in some field (maybe even in static field) and reusing it, which is generally not a good idea. Also worth disabling tab control while plugin is being initialized (or otherwise check if plugin is being loaded and cancel it before trying to load next one). Commented Apr 18, 2018 at 16:09
  • It matters how you use EF inside LoadCurrentSection(). Post an outline. Commented Apr 19, 2018 at 7:39
  • @Evk I do keep the db-context in a field. They are kept in service classes (see my edit). Every service class has it's own instance of the db-context and every plugin has it's own instances of service classes. When I clear the plugins, or cause a reload, all the instances of service classes get disposed and recreated (and therefore the same happens to the db-contexts). Commented Apr 19, 2018 at 11:22

1 Answer 1

0

Typical race condition.

Lets consider this scenario

Thread1 - Access PluginsCv
PluginsCv is null so register CurrentChanged
Thread2 - Access PluginsCv
PluginsCv is null so register CurrentChanged

Later... CurrentChanged fires

Thread1 - await LoadCurrentSection
Thread2 - await LoadCurrentSection

Thread2 "Wait! Thread1 is doing LoadCurrentSection too and its not done yet! Error! Error!"

Sign up to request clarification or add additional context in comments.

1 Comment

Ok seems like a possible scenario. But now I wrote some debug info into the console every time pluginsCv is null. And this happened only once. So the CurrentChanged-event must also be subscribed only once. Also tried to delegate every access to PluginsCv to the dispatcher. Bit it didn't help.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.