Now that our Redux store supports subscriptions, we can register to update our existing view models, allowing them to automatically stay current as our application state changes. The changes to each will be similar, but with a few variations on the common theme.

For AddVocabularyWordViewModel, we modify the constructor to initialize our members directly and then to register the required subscription:

public AddVocabularyWordViewModel(IReduxStore<WordTutorApplication> store)
{
    _store = store ?? throw new ArgumentNullException(nameof(store));

    var screen = (AddVocabularyWordScreen)_store.State.CurrentScreen;
    _spelling = screen.Spelling ?? string.Empty;
    _phrase = screen.Phrase ?? string.Empty;
    _pronunciation = screen.Pronunciation ?? string.Empty;

    _screenSubscription = _store.Subscribe(
        app => app.CurrentScreen as AddVocabularyWordScreen,
        RefreshFromScreen);
}

Directly updating our member fields, instead of calling UpdateFromScreen(), avoids problems that can be caused by firing messages through our store while we’re still initializing this instance.

Our subscription is based on the CurrentScreen of our application. The RefreshFromScreen() method triggered by the subscription is fairly simple:

private void RefreshFromScreen(AddVocabularyWordScreen screen)
{
    if (screen is null)
    {
        _screenSubscription.Dispose();
        return;
    }

    Spelling = screen.Spelling ?? string.Empty;
    Phrase = screen.Phrase ?? string.Empty;
    Pronunciation = screen.Pronunciation ?? string.Empty;
}

If the screen we’re supplied is not an AddVocabularyWordScreen, we release our subscription and do nothing; this ensures we disconnect cleanly and that the screen is available for garbage collection once the user moves on to a different screen.

After that check, we update our properties - which will in turn fire the right events to update our user interface.

For VocabularyBrowserViewModel, things are essentially the same, though we do have two different subscriptions: one for the screen, one for the vocabulary set.

public VocabularyBrowserViewModel(IReduxStore<WordTutorApplication> store)
{
    _store = store ?? throw new ArgumentNullException(nameof(store));

    var screen = (VocabularyBrowserScreen)_store.State.CurrentScreen;
    var vocab = _store.State.VocabularySet.Words;

    _selection = screen.Selection;
    _modified = screen.Modified;
    _words = new ObservableCollection<VocabularyWord>(
        vocab.OrderBy(w => w.Spelling));

    _screenSubscription = _store.Subscribe(
        app => app.CurrentScreen as VocabularyBrowserScreen,
        RefreshFromScreen);

    _vocabularySubscription = _store.Subscribe(
        app => app.VocabularySet,
        RefreshFromVocabularySet);
}

Again, if the CurrentScreen changes, we disconnect - but here we need to release both subscriptions:

private void RefreshFromScreen(VocabularyBrowserScreen screen)
{
    if (screen == null)
    {
        _screenSubscription.Dispose();
        _vocabularySubscription.Dispose();
        return;
    }

    Selection = screen.Selection;
    Modified = screen.Modified;
}

When the list of words changes, we update our display of words, showing all the words in alphabetical order (we apply the same ordering in our constructor, a minor violation of the DRY principle).

private void RefreshFromVocabularySet(VocabularySet vocabularySet)
{
    var words = vocabularySet.Words
        .OrderBy(w => w.Spelling)
        .ToList();
    UpdateCollection(_words, words);
}

The last view model we need to update is WordTutorViewModel, and here the logic is slightly different. We still need to create the subscription in our constructor, as before:

public WordTutorViewModel(
    IReduxStore<WordTutorApplication> store,
    ViewModelFactory factory)
{
    _store = store;
    _factory = factory;
    _currentScreen = _factory.Create(_store.State.CurrentScreen);

    _screenSubscription = _store.Subscribe(
        app => app.CurrentScreen,
        RefreshFromScreen);
}

But, our logic on what to do when we’re notified is quite different. Our other view models will update themselves to match our current application state, so we only need to take action here if/when the type of screen changes.

private void RefreshFromScreen(Screen screen)
{
    // Only need a new instance if the screen type changes
    // As long as the type of screen is unchanged, 
    // the exiting ViewModel will update
    var neededType = ViewModelFactory.FindViewModelType(screen.GetType());
    if (CurrentScreen?.GetType() != neededType)
    {
        CurrentScreen = _factory.Create(screen);
    }
}

If Type implemented IEquatable<Type> then we could have used a simpler solution, creating a subscription based on the current type of the screen. But, that’s not the case - and the constraint requiring IEquatable<T> still seems reasonable.

Prior post in this series:
Redux Subscriptions
Next post in this series:
Debugging word selection

Comments

blog comments powered by Disqus