我刚刚从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事件。 WeakEventHandlerManager必须是第一个 在UI线程上构造以正确获取对UI的引用 thread的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扔了 exception:System.InvalidOperationException:调用线程 无法访问此对象,因为其他线程拥有它。 在System.Windows.Threading.Dispatcher.VerifyAccess()处于System.Windows.DependencyObject.GetValue(DependencyProperty dp)at at System.Windows.Controls.Primitives.ButtonBase.get_Command()at System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute()at System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(对象发件人,EventArgs的) 在 System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged(对象发件人,EventArgs的) 在 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行 System.Threading.Tasks.Task.InnerInvoke()at System.Threading.Tasks.Task.Execute() ---从抛出异常的先前位置开始的堆栈跟踪结束--- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务task) 在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务task) 在System.Runtime.CompilerServices.TaskAwaiter.GetResult()中 Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.d__12.MoveNext() 在PatientSessionCommandsTests.cs中:第71行 ---从抛出异常的先前位置开始的堆栈跟踪结束--- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务task) 在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务task) 在System.Runtime.CompilerServices.TaskAwaiter.GetResult()
答案 0 :(得分: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线程上。
从我的观点来看,解决这个问题的方法是黑客或某种代码味道。反正我也不喜欢它。您应该确保首次从UI线程调用CallHandler()
,例如,通过在RaiseCanExecuteChanged()
实例上调用具有有效DelegateCommand
事件订阅的CanExecuteChanged
方法
希望这有帮助。
答案 1 :(得分:-2)
单元测试确保在任何条件下代码更改后您的功能都没有中断,我看到了单元测试编写的不同方法
无论是什么,单元测试意味着您期望根据您的输入获得一些结果。我建议您不要在单元测试中引用UI components
,因为如果您将Button
更改为其他control
,async
和await
,则您的测试用例将无效async
修饰符不是必需的。如果需要,您仍应在await
内使用DelegateCommand
和RaiseCanExecuteChanged
。 Prism 5支持这一点,你可以检查codeplex中的源代码。
每当您致电CanExecute
时,它都会触发附加到DelegateCommand
的{{1}}委托,并尝试禁用/启用UI控件。 UI控件位于UI线程中,但您的RaiseCanExecuteChanged
位于Worker线程中。通常这会破坏您的代码。
我的建议是编写测试用例以期望低于输出
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 before execute
Assert.IsFalse(IsExecuted);
command.RaiseCanExecuteChanged();
Assert.IsFalse(IsExecuted);
canExecute = true;
Assert.IsFalse(IsExecuted);
command.RaiseCanExecuteChanged();
Assert.IsTrue(IsExecuted);
}
单元测试始终执行断言以验证输出,因此您无需为测试方法标记async
和await