BusyIndicator 允许两次触发 RelayCommand [英] BusyIndicator allows to fire RelayCommand twice
问题描述
我有一个 WPF 应用程序,它遵循 MVVM.
I have a WPF application, which follows MVVM.
为了使 UI 具有响应性,我使用 TPL 来执行长时间运行的命令和 BusyIndicator 显示给用户,该应用程序现在正忙.
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
属性与BusyIndicator.IsBusy
绑定,而RefreshOrdersCommand
属性与工具栏上的按钮绑定,该按钮位于BusyIndicator
在这个视图模型的视图中.
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.
问题.
如果用户不经常点击按钮,一切正常:BusyIndicator
隐藏带有按钮的工具栏,数据被加载,BusyIndicator
消失.在输出窗口中,我可以看到成对的行:
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:
开始刷新.停止刷新.
但是如果用户非常点击按钮,看起来BusyIndicator
没有及时隐藏工具栏,两个后台线程试图执行RefreshOrders
方法,这会导致异常(这没关系,因为 EF DbContext
不是线程安全的).在输出窗口中,我看到了这张图片:
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:
开始刷新.开始刷新.
我做错了什么?
我查看了 BusyIndicator
的代码.我不知道,那里可能有什么问题:设置 IsBusy
只会对 VisualStateManager.GoToState
进行两次调用,而后者只会使一个矩形可见,该矩形隐藏 BusyIndicator
的内容:
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>
...并禁用内容:
<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>
有什么想法吗?
更新.问题不在于如何防止命令重新进入.我想知道允许按两次按钮的机制.
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
与BusyIndicator.IsBusy
绑定.绑定是同步的.BusyIndicator.IsBusy
的设置器调用VisualStateManager.GoToState
两次.其中一个调用使一个矩形可见,该矩形与BusyIndicator
的内容(在我的例子中是按钮)重叠.- 因此,据我所知,在设置
ViewModel.IsBusy
后,我无法实际实现按钮,因为所有这些事情都发生在同一个 (UI) 线程上.
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??
推荐答案
我不会尝试依赖忙碌指示器来控制有效的程序流.命令执行设置 IsBusy
,如果 IsBusy
已经是 True
,它本身不应该运行.
I wouldn't try to rely on a busy indicator to control valid program flow. The command execution sets IsBusy
, and it should itself not run if IsBusy
is already True
.
private void HandleRefreshOrders()
{
if (IsBusy)
return;
...
您也可以将按钮的 Enabled
状态绑定到 IsBusy
,但我认为核心解决方案是防止您的命令意外重新进入.启动一个工人也会增加一些复杂性.我将状态设置移动到 HandleRefreshOrders
然后处理 ContinueWith
实现中的状态重置.(可能需要一些额外的建议/阅读以确保它是线程安全的.)
You can also bind the button's Enabled
state to IsBusy
as well, but I think the core solution is to guard your commands from unintended re-entrance. Kicking off a worker will also add some complexity. I'd move the setting of the status to the HandleRefreshOrders
then handle the resetting of the state in the ContinueWith
implementation. (Some additional advice/reading may be needed to ensure it's thread-safe.)
*澄清移动 IsBusy
以避免双重执行:
* To clarify moving the IsBusy
to avoid double-execution:
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());
}
然后从 RefreshOrders
方法中删除 IsBusy
引用.单击一次按钮后,将设置 IsBusy
.如果用户快速点击,第二次触发命令的尝试将跳过.任务将在后台线程上执行,一旦完成,IsBusy
标志将被重置,命令将再次响应.当前假设对 RefreshOrdersCompleted
和 LogAggregateException
的调用不会冒泡异常,否则如果抛出异常,则不会重置该标志.可以在这些调用之前移动标志重置,或者在 annon 中使用 try/finally.方法声明.
Then remove the IsBusy
references from the RefreshOrders
method. Once you click on the button once, IsBusy
will be set. If the user rapid-clicks, the second attempt to trigger the command will skip out. The Task will execute on a background thread, and once it is complete, the IsBusy
flag will be reset and the command will respond once again. This currently assumes that calls to RefreshOrdersCompleted
and LogAggregateException
don't bubble exceptions, otherwise if they throw, the flag will not be reset. Can move the flag reset ahead of these calls, or use a try/finally within the annon. method declaration.
这篇关于BusyIndicator 允许两次触发 RelayCommand的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!