With the add vocabulary word screen that we defined last time, we can now look at the implementation of the related view model.

As a quick recap, a view model plays a pivotal role in any WPF application (and, indeed, in any application using the MVVM architectural pattern). The view model acts as an intermediary between the current state of the application, as captured by the model, and the user interface widgets visible to the user. Each view model is therefore shaped by this tension.

View model implementation

For our “Add Word” screen, we need to start with a trio of backing fields, one for each of the data entry fields we want to have, along with a member to retain a reference to the application store (we’ll need that when we send messages).

public class AddVocabularyWordViewModel 
    : ViewModelBase<AddVocabularyWordScreen>
{
    private readonly IReduxStore<WordTutorApplication> _store;

    private string _spelling;
    private string _phrase;
    private string _pronunciation;

Our constructor is simple:

    public AddVocabularyWordViewModel(
        IReduxStore<WordTutorApplication> store)
    {
        _store = store 
            ?? throw new ArgumentNullException(nameof(store));
        Model = store.State.CurrentScreen 
            as AddVocabularyWordScreen;
    }

When changing the Spelling property, we need to do these things:

  • Store the new value.
  • Update the user interface by triggering the NotifyPropertyChanged event if the new value is different.
  • Update the application state by sending a ModifySpellingMessage to the store

Using helper methods declared on the parent class ViewModelBase<> that we developed earlier, the implementation is:

    public string Spelling
    {
        get => _spelling;
        set
        {
            if (UpdateProperty(ref _spelling, value))
            {
                _store.Dispatch(new ModifySpellingMessage(_spelling));
            }
        }
    }

The Phrase and Pronunciation properties use the same approach.

The last thing we need (for now) is to implement the ModelUpdated() method required by our parent class:

    protected override void ModelUpdated(AddVocabularyWordScreen model)
    {
        Spelling = model?.Spelling ?? string.Empty;
        Phrase = model?.Phrase ?? string.Empty;
        Pronunciation = model?.Pronunciation ?? string.Empty;
    }
}

View model testing

Writing unit tests to ensure the view model behaves as expected was more complicated than I expected.

When I first wrote a test to ensure that the Spelling property didn’t fire the NotifyPropertyChanged event when the value was unchanged, I wrote this test:

[Fact]
public void AssigningExistingValue_DoesNotSendEvent()
{
    PropertyChangedEventArgs args = null;
    _model.PropertyChanged += (_, a) => args = a;

    _model.Spelling = _model.Spelling;

    args.Should().BeNull();
}

This test works, but I noticed a lot of repetition as I wrote further tests. First, there was a test to ensure the event was sent when the value was changed:

[Fact]
public void AssigningDifferentValue_SendsExpectedEventX()
{
    PropertyChangedEventArgs args = null;
    _model.PropertyChanged += (_, a) => args = a;

    _model.Spelling = "word";

    args.Should().NotBeNull();
    args.PropertyName.Should().Be(nameof(_model.Spelling));
}

Then, I found I was writing very similar tests for the Phrase and Pronunciation properties.

The amount of boilerplate - and the level of repetition - were both concerns. Very little changes between each of these tests. It would be easy to make a mistake writing one of the tests.

To try and reduce or eliminate this repetition, I created a helper method using some reflection:

private void Assert_AssigningExistingValue_DoesNotSendEvent<T>(
    T model, string propertyName)
    where T : INotifyPropertyChanged
{
    PropertyChangedEventArgs args = null;
    var propertyInfo = typeof(T).GetProperty(propertyName);
    propertyInfo.Should()
        .NotBeNull("can only test known public properties.");
    var existingValue = propertyInfo.GetValue(model);
    model.PropertyChanged += (s, a) => args = a;

    propertyInfo.SetValue(model, existingValue);

    args.Should().BeNull();
}

This did make the tests shorter to write:

[Fact]
public void AssigningExistingValue_DoesNotSendEvent()
    => Assert_AssigningExistingValue_DoesNotSendEvent(
        _model, _spellingProperty);

[Fact]
public void AssigningDifferentValue_SendsExpectedEvent()
    => Assert_AssigningDifferentValue_SendsExpectedEvent(
        _model, _spellingProperty, "word");

But it didn’t really seem that the tests were easy to read; it wasn’t clear what was actually being tested.

After taking some time away from the keyboard, I realized that most of the boilerplate was related to the NotifyPropertyChanged event, and checking that it had (or had not) been fired.

I, therefore, created a probe that allowed me to easily hook into the event and state the required assertions:

public class NotifyPropertyChangedProbe
{
    private readonly INotifyPropertyChanged _model;
    private PropertyChangedEventArgs _args;

    public NotifyPropertyChangedProbe(
        INotifyPropertyChanged model)
    {
        _model = model;
        _model.PropertyChanged += (_, a) => _args = a;
    }

    public void AssertNotFired()
    {
        _args.Should().BeNull();
    }

    public void AssertFired(string propertyName)
    {
        _args.Should().NotBeNull(
            "we expect the event to have been fired.");
        _args.PropertyName.Should().Be(
            propertyName,
            $"we expect the event to have been fired for '{propertyName}.");
    }
}

With this probe, I was able to reduce the tests into a couple of lines that are still easy to read:

[Fact]
public void AssigningExistingValue_DoesNotSendEvent()
{
    _model.Spelling = _model.Spelling;
    _notifyPropertyChanged.AssertNotFired();
}

[Fact]
public void AssigningDifferentValue_SendsExpectedEvent()
{
    _model.Spelling = "word";
    _notifyPropertyChanged.AssertFired(nameof(_model.Spelling));
}

The moral of the story is that you haven’t always finished just because all your tests have been written and everything is green. Sometimes you need to revisit and simplify.

Prior post in this series:
Add Word Screen
Next post in this series:
Add Word View

Comments

blog comments powered by Disqus