Prism 5 DelegateCommandBase.RaiseCanExecuteChanged抛出InvalidOperationException

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

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

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

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

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

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

这是一个失败的unit testing,可以重现问题:

[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); } 

这是exception堆栈:

测试方法Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.Fails抛出exception:System.InvalidOperationException:调用线程无法访问此对象,因为另一个线程拥有它。 System.Windows.Conters.Brton.ButtonBase.UpdateCanExecute中的System.Windows.Controls.Primitives.ButtonBase.get_Command()处的System.Windows.Threading.Dispatcher.VerifyAccess()处于System.Windows.DependencyObject.GetValue(DependencyProperty dp)处。 ()位于Microsoft.Practices.Prism.Commands.WeakEventHandlerManager的System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged(Object sender,EventArgs e)的System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(Object sender,EventArgs e)中。 Microsoft.Practices.Prism上Microsoft.Practices.Prism.Commands.DelegateCommandBase.OnCanExecuteChanged()的Microsoft.Practices.Prism.Commands.WeakEventHandlerManager.CallWeakReferenceHandlers(Object sender,List`1处理程序)中的.CallHandler(Object sender,EventHandler eventHandler)在PatientSessionCommandsTests.cs中的Calypso.Pharos.Commands.Test.PatientSessionCommandsTests。 c__DisplayClass10.b__e()的.Commands.DelegateCommandBase.RaiseCanExecuteChanged():Syste的71行 System.Threading.Tasks.Task.Execute()中的m.Threading.Tasks.Task.InnerInvoke()—抛出异常的前一个位置的堆栈跟踪结束—在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (任务任务)位于Patient.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务)的System.Runtime.CompilerServices.TaskAwaiter.GetResult(),位于PatientSessionCommandsTests.cs中的Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.d__12.MoveNext() :第71行—从抛出异常的上一个位置开始的堆栈跟踪结束—在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务任务)处,系统中的System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务) .Runtime.CompilerServices.TaskAwaiter.GetResult()

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

所以问题是,正如你正确提到的那样, 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()间接调用RaiseCanExecuteChanged() ,此字段才会被初始化。 并且因为您可以从线程池调用此方法,在这种情况下没有同步上下文,因此该字段将只设置为null并且CallHandler()方法在当前线程上调用事件处理程序,但不在UI线程上调用。

从我的观点来看,解决方案就是黑客或某种代码味道。 反正我也不喜欢它。 您应该确保首次从UI线程调用CallHandler() ,例如,通过在具有有效CanExecuteChanged事件订阅的DelegateCommand实例上调用RaiseCanExecuteChanged()方法。

希望这可以帮助。

unit testing确保在任何条件下代码更改后您的function都没有中断,我已经看到了unit testing编写的不同方法

  1. 有些人为代码覆盖编写unit testing。
  2. 有些人只编写unit testing来涵盖他们的function或业务需求。

无论是什么,unit testing意味着您期望根据您的输入获得一些结果。 我建议你不要在unit testing中引用UI components ,因为如果将Button更改为其他control ,则测试用例不起作用,也不需要asyncawait修饰符。 如果需要,您应该使用async并在DelegateCommand await 。 Prism 5支持这一点,你可以检查codeplex中的源代码。

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

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

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

     [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); } 

unit testing总是执行断言来validation输出,因此您不需要标记asyncawait测试方法