【问题标题】:BusyIndicator allows to fire RelayCommand twiceBusyIndi​​cator 允许两次触发 RelayCommand
【发布时间】:2012-12-09 00:09:56
【问题描述】:

我有一个 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) 线程上。

但是按钮是如何被按下两次的??

【问题讨论】:

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


    【解决方案1】:

    我不会尝试依靠繁忙指示器来控制有效的程序流程。命令执行设置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());
    }
    

    然后从RefreshOrders 方法中删除IsBusy 引用。一旦您单击按钮一次,IsBusy 将被设置。如果用户快速点击,第二次触发命令的尝试将跳过。任务将在后台线程上执行,一旦完成,IsBusy 标志将被重置,命令将再次响应。这目前假定对 RefreshOrdersCompletedLogAggregateException 的调用不会冒泡异常,否则如果它们抛出,则不会重置标志。可以在这些调用之前移动标志重置,或者在 annon 中使用 try/finally。方法声明。

    【讨论】:

    • > “我不会尝试依赖繁忙指示器来控制有效的程序流程”。为什么?你能反驳这个吗?我看不出与if (IsBusy) return 有任何关键区别。此外,问题不在于“如何防止双重命令执行”。我不明白,如何实现双击按钮,必须在第一次单击后隐藏。
    • 也许更清楚地解释你的问题,因为你在自相矛盾。 “看起来 BusyIndi​​cator 没有及时隐藏工具栏,两个后台线程尝试执行 RefreshOrders 方法,......开始刷新。开始刷新。我做错了什么?”这看起来像您在问为什么要执行双重命令。或者,您想在按钮上启用双击的问题是什么?在这种情况下,一般选项是在命令上启动 Timer,如果您及时再次单击,则执行双击行为,如果超时则执行单击行为。
    • 好的,根据您的描述,忙碌指示符绑定到IsBusy 属性。但是,您是从任务内部设置此属性。 (异步)处理您的 Click 事件的代码可以很好地完成,并在该任务真正有机会开始之前再次触发。如果您在事件处理程序而不是任务中设置IsBusy,这可能会被阻止,但我怀疑是否有任何保证。 AFAIK 忙碌指示器通过控制其他人来捕获事件来欺骗 UI。在按钮捕获下一次点击之前,它可能不会到位。
    • 当从 UI 线程设置 IsBusy 时,会发生相同的效果(双重触发)。
    • 好的,我已经根据您提供的内容构建了一个快速示例并确认了行为。这个问题是我之前建议的。忙碌指示器拦截用户事件,但只有在 之后它才有机会生效。事实上,您可以在忙碌指示符出现之前对按钮的消息泵进行 2 次点击。您的选择是检测并忽略我上面概述的重新输入,或者将按钮的IsEnabled 绑定到IsBusy 属性。 (使用逆变器)这会转储消息,从而不会导致重复调用。
    猜你喜欢
    • 2015-06-27
    • 2012-09-05
    • 1970-01-01
    • 1970-01-01
    • 2011-04-02
    • 2012-11-28
    • 2013-05-19
    • 2011-10-06
    • 1970-01-01
    相关资源
    最近更新 更多