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

1

10

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.

Alegar answered 20/8, 2012 at 7:34 Comment(3)
The RaiseCanExecuteChanged method only calls CommandManager.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
Right, that makes sense. Thank you. The method name does imply otherwise hey? I'm not at the stage of hooking up the UI to this view model yet, but I did a spike with MvvmLight before choosing frameworks and the CanExecute functionality worked fine.Alegar
Hooked it up quickly to test and updated the question.Alegar
A
10

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

Alegar answered 24/8, 2012 at 2:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.