我有一个WPF应用程序,它遵循MVVM。
为了使UI响应,我使用TPL执行长时间运行的命令,并BusyIndicator向用户显示该应用程序现在正忙。
在其中一个视图模型中,我有这个命令:
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
绑定,RefreshOrdersCommand
属性与工具栏上的按钮绑定,该按钮位于此视图模型视图中的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
绑定。绑定是同步的。BusyIndicator.IsBusy
两次调用VisualStateManager.GoToState
。其中一个调用显示一个矩形,它与BusyIndicator
的内容重叠(在我的情况下,按钮)。ViewModel.IsBusy
之后,我无法实际完成按钮,因为所有这些都发生在同一个(UI)线程上。但按钮如何被按两次?
答案 0 :(得分:3)
我不会试图依靠繁忙的指标来控制有效的程序流程。命令执行设置IsBusy
,如果IsBusy
已经True
,它本身就不会运行。
private void HandleRefreshOrders()
{
if (IsBusy)
return;
...
您也可以将按钮的Enabled
状态绑定到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中使用try / finally。方法声明。