我有一个跟随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.IsBusyBusyIndicator.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标志将被重置,命令将再次响应。当前,这假定对RefreshOrdersCompletedLogAggregateException的调用不会冒泡异常,否则,如果抛出异常,则不会重置该标志。可以将标志重置移到这些调用之前,或者在Annon中尝试/最后尝试。方法声明。

    关于c# - BusyIndi​​cator允许触发RelayCommand两次,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/13989684/

    10-11 15:06