I have a WPF application, which follows MVVM.
To make UI responsive, I'm using TPL for executing long-running commands and BusyIndicator to display to user, that application is busy now.
In one of the view models I have this command:
public ICommand RefreshOrdersCommand { get; private set; }
public OrdersEditorVM()
{
this.Orders = new ObservableCollection<OrderVM>();
this.RefreshOrdersCommand = new RelayCommand(HandleRefreshOrders);
HandleRefreshOrders();
}
private void HandleRefreshOrders()
{
var task = Task.Factory.StartNew(() => RefreshOrders());
task.ContinueWith(t => RefreshOrdersCompleted(t.Result),
CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
task.ContinueWith(t => this.LogAggregateException(t.Exception, Resources.OrdersEditorVM_OrdersLoading, Resources.OrdersEditorVM_OrdersLoadingFaulted),
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext());
}
private Order[] RefreshOrders()
{
IsBusy = true;
System.Diagnostics.Debug.WriteLine("Start refresh.");
try
{
var orders = // building a query with Entity Framework DB Context API
return orders
.ToArray();
}
finally
{
IsBusy = false;
System.Diagnostics.Debug.WriteLine("Stop refresh.");
}
}
private void RefreshOrdersCompleted(Order[] orders)
{
Orders.RelpaceContent(orders.Select(o => new OrderVM(this, o)));
if (Orders.Count > 0)
{
Orders[0].IsSelected = true;
}
}
IsBusy
property is bound with BusyIndicator.IsBusy
, and RefreshOrdersCommand
property is bound with a button on a toolbar, which is placed inside BusyIndicator
in the view of this view model.
The problem.
If the user clicks on button not frequently, everything works fine: BusyIndicator
hides toolbar with the button, data becomes loaded, BusyIndicator
disappears. In the output window I can see pairs of lines:
Start refresh. Stop refresh.
But if the user clicks on button very frequently, look like BusyIndicator
doesn't hide toolbar in time, and two background threads attempt to execute RefreshOrders
method, which causes exceptions (and it is OK, because EF DbContext
isn't thread-safe). In the output window I see this picture:
Start refresh. Start refresh.
What am I doing wrong?
I've looked into BusyIndicator
's code. I have no idea, what can be wrong there: setting IsBusy
just makes two calls to VisualStateManager.GoToState
, which, in turn, just makes visible a rectangle, which hides BusyIndicator
's content:
<VisualState x:Name="Visible">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.001" Storyboard.TargetName="busycontent" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.001" Storyboard.TargetName="overlay" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
...and disables content:
<VisualState x:Name="Busy">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.001" Storyboard.TargetName="content" Storyboard.TargetProperty="(Control.IsEnabled)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<sys:Boolean>False</sys:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
Any ideas?
Update. The question isn't about how to prevent command from reentrance. I'm wondering about mechanism, which allows to press button twice.
Here's how it works (from my point):
ViewModel.IsBusy
is bound withBusyIndicator.IsBusy
. The binding is synchronous.- Setter of
BusyIndicator.IsBusy
callsVisualStateManager.GoToState
twice. One of these calls makes visible a rectangle, which overlapsBusyIndicator
's content (the button, in my case). - So, as I understand, I can't physically achieve the button after I've set
ViewModel.IsBusy
, because all of these things happen on the same (UI) thread.
But how the button becomes pressed twice??