Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't RelayCommand RaiseCanExecuteChanged work in a unit test?

I'm using the current version of MvvmLight available on Nuget (4.1.23.0) and calling RaiseCanExecuteChanged does not appear to be doing anything in a unit test. The scenario is very simple, I have a command:

public RelayCommand FooCommand { get; private set; }

I new it up in the view model constructor and point it to some private methods:

FooCommand = new RelayCommand(Foo, CanFoo);

private void Foo()
{
    // do some fooing.
}

private bool CanFoo()
{
    return SomeRequiredProperty != null;
}

Then in the setter for SomeRequiredProperty I call RaiseCanExecuteChanged:

public object SomeRequiredProperty
{
    get
    {
        return someRequiredProperty;
    }

    set
    {
        someRequiredProperty = value;
        FooCommand.RaiseCanExecuteChanged();
    }
}

Now in a unit test I do the following:

// Arrange
var canExecuteChanged = false;
viewModel.FooCommand.CanExecuteChanged += (sender, args) => canExecuteChanged = true;

// Act
viewModel.SomeRequiredProperty = new object();

// Assert
Assert.That(canExecuteChanged, Is.True);

The test fails because my event handler is not firing. Why is that?

Update: The behaviour does indeed work at run time.

like image 675
andrej351 Avatar asked Aug 20 '12 07:08

andrej351


1 Answers

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 :)

like image 120
andrej351 Avatar answered Oct 19 '22 01:10

andrej351