Prism 5中DelegateCommandBase.RaiseCanExecuteChanged会抛出InvalidOperationException异常。

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都使用WeakEventHandlerManager来引发CanExecuteChanged事件。必须在UI线程上首先构造WeakEventHandlerManager以正确获取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.Commands.WeakEventHandlerManager.CallWeakReferenceHandlers(Object sender, List`1 handlers)中 在Microsoft.Practices.Prism.Commands.DelegateCommandBase.OnCanExecuteChanged()中 在Microsoft.Practices.Prism.Commands.DelegateCommandBase.RaiseCanExecuteChanged()中 在Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.<>c__DisplayClass10.b__e()中,在PatientSessionCommandsTests.cs第71行 在System.Threading.Tasks.Task.InnerInvoke()中 在System.Threading.Tasks.Task.Execute()中 --- 异常堆栈跟踪结束于前一次引发异常的位置 --- 在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)中 在System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)中 在System.Runtime.CompilerServices.TaskAwaiter.GetResult()中 在Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.d__12.MoveNext()中,在PatientSessionCommandsTests.cs第71行 --- 异常堆栈跟踪结束于前一次引发异常的位置 --- 在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)中 在System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)中 在System.Runtime.CompilerServices.TaskAwaiter.GetResult()中

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线程上调用事件处理程序。
我认为解决这个问题的方法是一种hack或某种代码味道。无论如何,我都不喜欢它。您应该确保首次从UI线程调用CallHandler(),例如通过在具有有效CanExecuteChanged事件订阅的DelegateCommand实例上调用RaiseCanExecuteChanged()方法。
希望这可以帮助到您。

-2

单元测试确保您的功能在任何情况下进行代码更改后不会出现故障,我见过不同的单元测试编写方法

  1. 有些人为了代码覆盖率而编写单元测试。
  2. 有些人只编写单元测试以涵盖其功能或业务需求。

无论如何,单元测试意味着您期望基于输入获得某些结果。我建议您避免在单元测试中引用 UI 组件,因为如果您将 Button 更改为其他 control,则您的测试用例将无法工作,同时不需要使用 asyncawait 修饰符。如果您想要,仍然应在 DelegateCommand 中使用 asyncawait。Prism 5 支持此功能,您可以在 codeplex 中查看源代码。

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

我的建议是编写测试用例以期望以下输出。
执行方法应在CanExecute方法返回true时触发。 如果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.IsFalse(IsExecuted); command.RaiseCanExecuteChanged(); Assert.IsFalse(IsExecuted);
canExecute = true; Assert.IsFalse(IsExecuted); command.RaiseCanExecuteChanged(); Assert.IsTrue(IsExecuted); }

单元测试总是进行断言以验证输出,因此您不需要在测试方法中标记asyncawait


感谢您的回复,但我的问题不是关于单元测试的。异常发生在应用程序运行时。我认为单元测试会有所帮助,以帮助澄清我遇到的问题,因为单元测试表现出完全相同的行为。问题在于Prism DelegateCommand.RaiseCanExecuteChanged 不会转换为UI线程,即使方法文档说“在UI线程上引发CanExecuteChanged,以便每个命令调用者都可以重新查询以检查命令是否可以执行。” - Jana Mandic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接