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?