5

我刚刚从 Prism 4.1 更新到 5,过去可以正常工作的代码现在抛出 InvalidOperationExceptions。我怀疑根本原因是更新的异步 DelegateCommands 没有正确编组到 UI 线程。

我需要能够从任何线程调用 command.RaiseCanExecuteChanged() 并在 UI 线程上引发 CanExecuteChanged 事件。Prism 文档说这就是 RaiseCanExecuteChanged() 方法应该做的。但是,随着 Prism 5 的更新,这不再有效。CanExecuteChanged 事件在非 UI 线程上调用,并且在此非 UI 线程上访问 UI 元素时,我得到下游 InvalidOperationExceptions。

这是提供解决方案提示的 Prism 文档:

DelegateCommand 包括对异步处理程序的支持,并已移至 Prism.Mvvm 可移植类库。DelegateCommand 和 CompositeCommand 都使用 Wea​​kEventHandlerManager 来引发 CanExecuteChanged 事件。WeakEventHandlerManager 必须首先在 UI 线程上构造,以正确获取对 UI 线程的 SynchronizationContext 的引用。

但是,WeakEventHandlerManager 是静态的,所以我无法构造它......

根据 Prism 文档,有谁知道我如何在 UI 线程上构建 WeakEventHandlerManager ?

这是重现问题的失败单元测试:

    [TestMethod]
    public async Task Fails()
    {
        bool canExecute = false;
        var command = new DelegateCommand(() => Console.WriteLine(@"Execute"),
                                          () =>
                                          {
                                              Console.WriteLine(@"CanExecute");
                                              return canExecute;
                                          });
        var button = new Button();
        button.Command = command;

        Assert.IsFalse(button.IsEnabled);

        canExecute = true;

        // Calling RaiseCanExecuteChanged from a threadpool thread kills the test
        // command.RaiseCanExecuteChanged(); works fine...
        await Task.Run(() => command.RaiseCanExecuteChanged());

        Assert.IsTrue(button.IsEnabled);
    }

这是异常堆栈:

测试方法 Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.Fails 抛出异常:System.InvalidOperationException:调用线程无法访问此对象,因为不同的线程拥有它。在 System.Windows.Threading.Dispatcher.VerifyAccess() 在 System.Windows.DependencyObject.GetValue(DependencyProperty dp) 在 System.Windows.Controls.Primitives.ButtonBase.get_Command() 在 System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute () 在 System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(Object sender, EventArgs e) 在 System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged(Object sender, EventArgs e) 在 Microsoft.Practices.Prism.Commands.WeakEventHandlerManager .CallHandler(Object sender, EventHandler eventHandler) 在 Microsoft.Practices.Prism。

4

2 回答 2

5

我不知道您是否还需要答案,但也许有人会观察到同样的错误。

所以问题是,正如您正确提到的,该RaiseCanExecuteChanged()方法并不总是将事件处理程序调用发布到 UI 线程的同步上下文。

如果我们看一下WeakEventHandlerManager实现,我们会看到两件事。首先,这个静态类有一个私有静态字段:

private static readonly SynchronizationContext syncContext = SynchronizationContext.Current;

其次,有一个私有方法,它应该使用这个同步上下文并将事件处理程序调用实际发布到该上下文:

    private static void CallHandler(object sender, EventHandler eventHandler)
    {
        if (eventHandler != null)
        {
            if (syncContext != null)
            {
                syncContext.Post((o) => eventHandler(sender,  EventArgs.Empty), null);
            }
            else
            {
                eventHandler(sender, EventArgs.Empty);
            }
        }
    }

所以,它看起来很不错,但是......

正如我之前所说,这个电话发布“并不总是”发生。例如,“并非总是”意味着这种情况:

  • 您的程序集已在发布配置中构建并启用了优化
  • 您没有将调试器附加到您的程序集

在这种情况下,.NET 框架优化了代码执行,现在很重要的是,它可以syncContext随时初始化静态字段,但在它第一次使用之前。所以,这发生在我们的例子中——这个字段只有在你第一次调用CallHandler()方法时才被初始化(当然间接地,通过调用RaiseCanExecuteChanged())。并且因为您可以从线程池调用此方法,在这种情况下没有同步上下文,因此该字段将被设置为null并且该CallHandler()方法在当前线程上调用事件处理程序,而不是在 UI 线程上。

从我的角度来看,解决方案是黑客攻击或某种代码异味。反正我不喜欢。您应该确保CallHandler()第一次从 UI 线程调用,例如,通过在具有有效 事件订阅RaiseCanExecuteChanged()的实例上调用方法。DelegateCommandCanExecuteChanged

希望这可以帮助。

于 2014-12-10T16:35:31.727 回答
-2

单元测试确保您的功能在任何情况下更改代码后都不会中断,我已经看到了不同的单元测试编写方法

  1. 有些人为代码覆盖率编写单元测试。
  2. 有些人编写单元测试只是为了覆盖他们的功能或业务需求。

不管是什么,单元测试意味着您期望根据您的输入得到一些结果。我建议您避免在单元测试中引用,因为如果您将测试用例更改为 other ,UI components您的测试用例将不起作用,并且 and修饰符也不是必需的。如果你愿意,你应该使用and inside 。Prism 5 支持这一点,您可以在 codeplex 中查看源代码。ButtoncontrolasyncawaitasyncawaitDelegateCommand

每当您调用RaiseCanExecuteChanged它时,它都会触发CanExecute附加到您的委托DelegateCommand并尝试禁用/启用 UI 控件。UI 控件在 UI 线程中,但您RaiseCanExecuteChanged在 Worker 线程中。通常这会破坏您的代码。

我的建议是编写测试用例以期望低于输出

  1. CanExecute如果方法返回,Execute 方法应该触发true
  2. CanExecute如果方法返回,则不应触发执行方法false

    [TestMethod]
    public void Fails()
    {
        bool isExecuted = false;
        bool canExecute = false;
        var command = new DelegateCommand(() => 
                                          {
                                             Console.WriteLine(@"Execute");
                                             isExecuted = true;
                                          }
                                          () =>
                                          {
                                              Console.WriteLine(@"CanExecute");
                                              return canExecute;
                                          });
    
        // assert before execute
        Assert.IsFalse(IsExecuted);
        command.RaiseCanExecuteChanged();
        Assert.IsFalse(IsExecuted);
    
        canExecute = true;
        Assert.IsFalse(IsExecuted);
        command.RaiseCanExecuteChanged();
        Assert.IsTrue(IsExecuted);
    }
    

单元测试总是做断言来验证输出,所以你不需要为你的测试方法标记asyncawait

于 2014-09-09T19:23:10.450 回答