BusyIndi​​cator允许两次触发RelayCommand

时间:2012-12-21 11:52:27

标签: c# wpf mvvm task-parallel-library busyindicator

我有一个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.IsBusyBusyIndicator.IsBusy绑定。绑定是同步的。
  • 设置BusyIndicator.IsBusy两次调用VisualStateManager.GoToState。其中一个调用显示一个矩形,它与BusyIndicator的内容重叠(在我的情况下,按钮)。
  • 所以,据我所知,在我设置ViewModel.IsBusy之后,我无法实际完成按钮,因为所有这些都发生在同一个(UI)线程上。

但按钮如何被按两次?

1 个答案:

答案 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标志将被重置,命令将再次响应。目前假设对RefreshOrdersCompletedLogAggregateException的调用不会出现异常,否则如果它们抛出,则不会重置该标志。可以在这些调用之前移动标志重置,或者在annon中使用try / finally。方法声明。