我有一个跟随MVVM的WPF应用程序。
为了使UI响应,我正在使用TPL执行长时间运行的命令,并使用BusyIndicator显示给用户,该应用程序现在很忙。
在其中一种 View 模型中,我具有以下命令:
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
属性与BusyIndicator.IsBusy
绑定(bind),而RefreshOrdersCommand
属性与工具栏上的按钮绑定(bind),该工具栏在此 View 模型的 View 中位于BusyIndicator
内。问题。
如果用户不经常单击按钮,则一切正常:
BusyIndicator
用按钮隐藏工具栏,加载数据,BusyIndicator
消失。在输出窗口中,我可以看到几行:但是,如果用户非常频繁地单击按钮,看起来
BusyIndicator
不会及时隐藏工具栏,并且两个后台线程尝试执行RefreshOrders
方法,这会导致异常(并且可以,因为EF DbContext
并不是线程安全的) 。在输出窗口中,我看到以下图片:我究竟做错了什么?
我研究了
BusyIndicator
的代码。我不知道那里有什么问题:设置IsBusy
只会对VisualStateManager.GoToState
进行两次调用,而这又只会使一个可见的矩形可见,从而隐藏了BusyIndicator
的内容: <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>
...并禁用内容:
<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>
有任何想法吗?
更新。
问题不在于如何防止命令再次进入。我想知道机制,它允许按两次按钮。
这是它的工作原理(从我的角度来看):
ViewModel.IsBusy
与BusyIndicator.IsBusy
绑定(bind)。绑定(bind)是同步的。 BusyIndicator.IsBusy
的二传手两次调用VisualStateManager.GoToState
。这些调用之一使矩形可见,该矩形与BusyIndicator
的内容(在本例中为按钮)重叠。 ViewModel.IsBusy
后,我实际上无法实现该按钮,因为所有这些事情都发生在同一(UI)线程上。 但是如何两次按下按钮呢?
最佳答案
我不会尝试依靠繁忙的指示器来控制有效的程序流。命令执行将设置IsBusy
,如果IsBusy
已经是True
,则它本身不应运行。
private void HandleRefreshOrders()
{
if (IsBusy)
return;
...
您还可以将按钮的
Enabled
状态也绑定(bind)到IsBusy
,但是我认为核心解决方案是防止命令意外进入。开工也将增加一些复杂性。我将状态设置移到HandleRefreshOrders
,然后在ContinueWith
实现中处理状态的重置。 (可能需要一些其他建议/阅读以确保其是线程安全的。)*编辑:为澄清移动
IsBusy
以避免再次执行:private void HandleRefreshOrders()
{
if (IsBusy)
return;
IsBusy = true;
var task = Task.Factory.StartNew(() => RefreshOrders());
task.ContinueWith(t =>
{
RefreshOrdersCompleted(t.Result);
IsBusy = false;
},
CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
task.ContinueWith(t =>
{
this.LogAggregateException(t.Exception, Resources.OrdersEditorVM_OrdersLoading, Resources.OrdersEditorVM_OrdersLoadingFaulted);
IsBusy = false;
},
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext());
}
然后从
IsBusy
方法中删除RefreshOrders
引用。单击一次按钮后,将设置IsBusy
。如果用户快速单击,则第二次触发命令的尝试将跳过。该任务将在后台线程上执行,完成后,IsBusy
标志将被重置,命令将再次响应。当前,这假定对RefreshOrdersCompleted
和LogAggregateException
的调用不会冒泡异常,否则,如果抛出异常,则不会重置该标志。可以将标志重置移到这些调用之前,或者在Annon中尝试/最后尝试。方法声明。关于c# - BusyIndicator允许触发RelayCommand两次,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/13989684/