Prism 5 DelegateCommandBase.RaiseCanExecuteChanged抛出InvalidOperationException

Jan*_*dic 5 c# prism prism-5

我刚刚从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?

这是一个失败的单元测试,可以重现问题:

    [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);
    }
Run Code Online (Sandbox Code Playgroud)

这是异常堆栈:

测试方法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(对象发件人,EventArgs e)上System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged(对象发件人,EventArgs e)上Microsoft.Practices.Prism.Commands.WeakEventHandlerManager .CallHandler(对象发件人,事件处理程序事件处理程序)在Microsoft.Practices.Prism.Commands.WeakEventHandlerManager.CallWeakReferenceHandlers(对象发件人,List`1处理程序)在Microsoft.Practices.Prism.Commands.DelegateCommandBase.OnCanExecuteChanged()在Microsoft.Practices.Prism .Commands.DelegateCommandBase.RaiseCanExecuteChanged()在Calypso.Pharos.Commands.Test.PatientSessionCommandsTests <> c__DisplayClass10.b__e()中PatientSessionCommandsTests.cs:线71在SYSTE m.Threading.Tasks.Task.InnerInvoke()在System.Threading.Tasks.Task.Execute()---从先前位置栈跟踪其中抛出异常---在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()

dym*_*oid 5

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

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

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

private static readonly SynchronizationContext syncContext = SynchronizationContext.Current;
Run Code Online (Sandbox Code Playgroud)

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

    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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

正如我之前所说,这次电话会议发布"并不总是"."并非总是"意味着,例如,这种情况:

  • 您的程序集已在发布配置中构建,并且已打开优化
  • 您没有将调试器附加到程序集

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

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

希望这可以帮助.


Vim*_* CK -2

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

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

不管是什么,单元测试意味着您期望根据您的输入得到一些结果。我建议您避免在单元测试中引用,因为如果您将 更改为其他,UI components您的测试用例将无法工作,而且也不需要and修饰符。如果你愿意的话,你应该在里面仍然使用and 。Prism 5 支持此功能,您可以在 codeplex 中查看源代码。ButtoncontrolasyncawaitasyncawaitDelegateCommand

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

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

  1. CanExecute如果方法返回,则应触发执行方法true
  2. CanExecute如果方法返回,则不应触发 Excute 方法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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

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