Fixed!
nemesv was correct in that FooCommand.RaiseCanExecuteChanged()
simply calls CommandManager.InvalidateRequerySuggested()
.
In addition to that, FooCommand.CanExecuteChanged
simply forwards the handler on to the CommandManager.RequerySuggested
event:
public event EventHandler CanExecuteChanged
{
add
{
...
CommandManager.RequerySuggested += value;
}
...
}
The cause of the problem was the following line of code in the CommandManager class:
private void RaiseRequerySuggested()
{
...
_requerySuggestedOperation = dispatcher.
BeginInvoke(
DispatcherPriority.Background,
new DispatcherOperationCallback(RaiseRequerySuggested),
null); // dispatcher is the Dispatcher for the current thread.
...
}
This line places a work item with DispatcherPriority Background
on the Dispatcher work item queue. The work item is supposed to notify all handlers of the CommandManager.RequerySuggested
event.
The problem is that this work item is never run.
The solution is to force the dispatcher to run the work item.
I found the solution in this discussion on the MVVM Foundation CodePlex page. I managed to simplify the code somewhat into the following helper class.
public static class DispatcherTestHelper
{
private static DispatcherOperationCallback exitFrameCallback = ExitFrame;
/// <summary>
/// Synchronously processes all work items in the current dispatcher queue.
/// </summary>
/// <param name="minimumPriority">
/// The minimum priority.
/// All work items of equal or higher priority will be processed.
/// </param>
public static void ProcessWorkItems(DispatcherPriority minimumPriority)
{
var frame = new DispatcherFrame();
// Queue a work item.
Dispatcher.CurrentDispatcher.BeginInvoke(
minimumPriority, exitFrameCallback, frame);
// Force the work item to run.
// All queued work items of equal or higher priority will be run first.
Dispatcher.PushFrame(frame);
}
private static object ExitFrame(object state)
{
var frame = (DispatcherFrame)state;
// Stops processing of work items, causing PushFrame to return.
frame.Continue = false;
return null;
}
}
My test now looks like this:
// Arrange
var canExecuteChanged = false;
viewModel.FooCommand.CanExecuteChanged +=
(sender, args) => canExecuteChanged = true;
// Act
viewModel.SomeRequiredProperty = new object();
DispatcherTestHelper.ProcessWorkItems(DispatcherPriority.Background);
// Assert
Assert.That(canExecuteChanged, Is.True);
And, most importantly, it passes :)
RaiseCanExecuteChanged
method only callsCommandManager.InvalidateRequerySuggested()
so it's not directly raising the event. I guess this causes your test to fail, but I don't know how to fix it. During runtime the command is working, am I right? – Manassas