I've just updated from Prism 4.1 to 5 and code that used to work fine now throws InvalidOperationExceptions. I suspect that the root cause is that the updated async DelegateCommands don't marshall to the UI thread properly.
I need to be able to call command.RaiseCanExecuteChanged() from any thread and for that to raise the CanExecuteChanged event on the UI thread. The Prism documentation says that that's what the RaiseCanExecuteChanged() method is supposed to do. However, with the Prism 5 update, that no longer works. The CanExecuteChanged event gets called on a non-UI thread and I get downstream InvalidOperationExceptions as UI elements are accessed on this non-UI thread.
Here's the Prism documentation that provides a hint of a solution:
DelegateCommand includes support for async handlers and has been moved to the Prism.Mvvm portable class library. DelegateCommand and CompositeCommand both use the WeakEventHandlerManager to raise the CanExecuteChanged event. The WeakEventHandlerManager must be first constructed on the UI thread to properly acquire a reference to the UI thread’s SynchronizationContext.
However, the WeakEventHandlerManager is static, so I can't construct it...
Does anyone know how I might go about constructing the WeakEventHandlerManager on the UI thread, per the Prism docs?
Here's a failing unit test that reproduces the problem:
[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);
}
And here's the exception stack:
Test method Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.Fails threw exception: System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it. at System.Windows.Threading.Dispatcher.VerifyAccess() at System.Windows.DependencyObject.GetValue(DependencyProperty dp) at System.Windows.Controls.Primitives.ButtonBase.get_Command() at System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() at System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(Object sender, EventArgs e) at System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged(Object sender, EventArgs e) at Microsoft.Practices.Prism.Commands.WeakEventHandlerManager.CallHandler(Object sender, EventHandler eventHandler) at Microsoft.Practices.Prism.Commands.WeakEventHandlerManager.CallWeakReferenceHandlers(Object sender, List`1 handlers) at Microsoft.Practices.Prism.Commands.DelegateCommandBase.OnCanExecuteChanged() at Microsoft.Practices.Prism.Commands.DelegateCommandBase.RaiseCanExecuteChanged() at Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.<>c__DisplayClass10.b__e() in PatientSessionCommandsTests.cs: line 71 at System.Threading.Tasks.Task.InnerInvoke() at System.Threading.Tasks.Task.Execute() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.d__12.MoveNext() in PatientSessionCommandsTests.cs: line 71 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult()