Prism 5 DelegateCommandBase.RaiseCanExecuteChanged throws InvalidOperationException

I just upgraded from Prism 4.1 to 5, and the code that was used to work fine now calls InvalidOperationExceptions. I suspect that the main reason is that the updated async DelegateCommands do not order the thread thread correctly.

I need to be able to call the .RaiseCanExecuteChanged () command from any thread and for this to raise the CanExecuteChanged event in the UI thread. The Prism documentation says that the RaiseCanExecuteChanged () method should do this. However, with the Prism 5 update, this no longer works. The CanExecuteChanged event is fired on a thread other than the UI, and I get child InvalidOperationExceptions when the user interface elements are available in that thread other than the UI.

Here's the Prism documentation that gives a hint about the solution:

DelegateCommand includes support for asynchronous handlers and is moved to the Prism.Mvvm portable class library. DelegateCommand and CompositeCommand use the WeakEventHandlerManager command to raise the CanExecuteChanged event. The WeakEventHandlerManager must be the first built on the user interface thread in order to correctly get a link to the user interface of the SynchronizationContext theme.

However, WeakEventHandlerManager is static, so I cannot build it ...

Does anyone know how I can build a WeakEventHandlerManager construct in a user interface thread according to Prism docs?

Here the unit test fails, which 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 is the exception stack:

The test method Calypso.Pharos.Commands.Test.PatientSessionCommandsTests.Fails threw an Exception: System.InvalidOperationException: the calling thread cannot access this object because it has a different thread. in System.Windows.Threading.Dispatcher.VerifyAccess () in System.Windows.DependencyObject.GetValue (DependencyProperty dp) in System.Windows.Controls.Primitives.ButtonBase.get_Command () in System.Windows.Controls.Primitives.ButtonBaseExecateate () at System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged (sender of the object, EventArgs e) in System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged (sender of the object, EventArgs e) in Microsoft.Practerm.Practerm.Pr .CallHandler (object sender, EventHandler EventHandler) in Microsoft.Practices.Prism.Commands.WeakEventHandlerManager.CallWeakReferenceHandlers (object sender, List`1 handlers) in Microsoft.Practices.Prism.Commands.DelegateCommandBase.OnCedExices) .Commands.DelegateCommandBase.RaiseCanExecuteChanged () in Calypso.Pharos.Commands .Test.PatientSessionCommandsTests <. > C__DisplayClass10.b__e () in PatientSessionCommandsTests.cs: line 71 in System.Threading.Tasks.Task.InnerInvoke () in System.Threading.Tasks.Task.Execute () --- The end of the stack trace from the previous place where it was selected an exception is on System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (Task task) in System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (Task Task) in System.Runtime.CompilerServices.TaskAwult.alas.taskRaTalTaTaTaTaTar.aTalTaRaTaTaTerAtal.aSeter.alas.taskRaTalTaTaTaTaeret.alat.etasketaalsultasaly.a .PatientSessionCommandsTests.d__12.MoveNext () in PatientSessionCommandsTests.cs: line 71 --- The end of the stack trace from the previous place where the exception was thrown is on System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (task Task) in System.Runter..piler .TaskAwaiter.HandleNonSuccessAndDebuggerNotificat ion (Task) in System.Runtime.CompilerServices.TaskAwaiter.GetResult ()

+5
source share
2 answers

I do not know if you still need an answer, but maybe someone will observe the same error.

So the problem is that, as you rightly said, the RaiseCanExecuteChanged() method does not always send an event handler call to the UI thread synchronization context.

If we look at the implementation of WeakEventHandlerManager , we will see two things. Firstly, this static class has a private static field:

 private static readonly SynchronizationContext syncContext = SynchronizationContext.Current; 

Secondly, there is a private method that should use this synchronization context and actually send calls to the event handler in this context:

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

So it looks good, but ...

As I said, this call message does not always occur. β€œNot always” means, for example, this circumstance:

  • Your build was built in release configuration and with optimization enabled
  • you do not attach a debugger to your assembly

In this situation, the .NET platform optimizes code execution and, importantly, can initialize the syncContext static field at any time, but before it is used for the first time. So, this happens in our case - this field is initialized only when you first call the CallHandler() method (of course, indirectly by calling RaiseCanExecuteChanged() ). And since you can call this method from the thread pool, in this case the synchronization context will not be set, so the field will be only null , and the CallHandler() method CallHandler() call the event handler in the current thread, but not to the User Interface.

The solution to this is, from my point of view, hacking or some kind of code smell. I don’t like it anyway. You just need to make sure that CallHandler() is called for the first time from the user interface thread, for example, by calling the RaiseCanExecuteChanged() method RaiseCanExecuteChanged() DelegateCommand instance that has valid CanExecuteChanged subscriptions.

Hope this helps.

+5
source

Unit testing ensures that your functionality will not be broken after changing the code in any state, I saw a different approach for Unit Test entries

  • some people write Unit Test to cover code.
  • Some guys write Unit Test just to cover their functionality or business requirements.

Be that as it may, Unit Test means you expect some kind of result based on your inputs. I suggest you avoid linking UI components to your Unit Test, because your test case will not work if you change Button to another control , the async and await modifier is also not required, you must use async and await inside the DelegateCommand if you want. Prism 5 supports this, and you can check the source code in codeplex.

Whenever you call RaiseCanExecuteChanged , it calls the CanExecute delegate attached to your DelegateCommand and tries to disable / enable the user interface control. UI elements are in the user interface thread, but your RaiseCanExecuteChanged is in the worker thread. This usually interrupts your code.

My suggestion is to write test cases to expect below output

  • The Execute method should fire if the CanExecute method returns true
  • The excute method should not fire if the CanExecute method returns 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); } 

Unit Test always does an Assertion to verify the output, so you don't need to mark async and await for your test method

-2
source

Source: https://habr.com/ru/post/1202180/


All Articles