BusyIndi​​cator 允许两次触发 RelayCommand [英] BusyIndicator allows to fire RelayCommand twice

查看:35
本文介绍了BusyIndi​​cator 允许两次触发 RelayCommand的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个 WPF 应用程序,它遵循 MVVM.

I have a WPF application, which follows MVVM.

为了使 UI 具有响应性,我使用 TPL 来执行长时间运行的命令和 BusyIndi​​cator 显示给用户,该应用程序现在正忙.

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 属性与BusyIndi​​cator.IsBusy 绑定,而RefreshOrdersCommand 属性与工具栏上的按钮绑定,该按钮位于BusyIndi​​cator 在这个视图模型的视图中.

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.

问题.

如果用户不经常点击按钮,一切正常:BusyIndi​​cator 隐藏带有按钮的工具栏,数据被加载,BusyIndi​​cator 消失.在输出窗口中,我可以看到成对的行:

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:

开始刷新.停止刷新.

但是如果用户非常点击按钮,看起来BusyIndi​​cator没有及时隐藏工具栏,两个后台线程试图执行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:

开始刷新.开始刷新.

我做错了什么?

我查看了 BusyIndi​​cator 的代码.我不知道,那里可能有什么问题:设置 IsBusy 只会对 VisualStateManager.GoToState 进行两次调用,而后者只会使一个矩形可见,该矩形隐藏 BusyIndi​​cator 的内容:

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.IsBusyBusyIndi​​cator.IsBusy 绑定.绑定是同步的.
  • BusyIndi​​cator.IsBusy 的设置器调用 VisualStateManager.GoToState 两次.其中一个调用使一个矩形可见,该矩形与 BusyIndi​​cator 的内容(在我的例子中是按钮)重叠.
  • 因此,据我所知,在设置 ViewModel.IsBusy 后,我无法实际实现按钮,因为所有这些事情都发生在同一个 (UI) 线程上.
  • ViewModel.IsBusy is bound with BusyIndicator.IsBusy. The binding is synchronous.
  • Setter of BusyIndicator.IsBusy calls VisualStateManager.GoToState twice. One of these calls makes visible a rectangle, which overlaps BusyIndicator'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 标志将被重置,命令将再次响应.当前假设对 RefreshOrdersCompletedLogAggregateException 的调用不会冒泡异常,否则如果抛出异常,则不会重置该标志.可以在这些调用之前移动标志重置,或者在 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.

这篇关于BusyIndi​​cator 允许两次触发 RelayCommand的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆