The existing model class ModifyVocabularyWordScreen only handles the creation of a new word. We need to modify it to support the modification of an existing word as well.

Construction

Currently, our entry point for creating a new instance of ModifyVocabularyWordScreen is through its constructor. It might, therefore, seem appropriate to add another constructor, one that allows an existing word to be provided as a starting point.

However, this runs up against a design smell. Having multiple constructors is often considered unwise - and we already have two. Our saving grace is that one of them is private, so it’s just an internal implementation issue. Having multiple public constructors is distinctly worse.

Instead of adding a third constructor, I’ve introduced two different static factory methods:

public static ModifyVocabularyWordScreen ForNewWord()
    => new ModifyVocabularyWordScreen();

public static ModifyVocabularyWordScreen ForExistingWord(
    VocabularyWord word)
    => new ModifyVocabularyWordScreen(word);

Using static factory methods instead of overloaded constructors has the advantage that each can be given an intention-revealing name.

Both of these factory methods delegate to the same constructor, with an optional parameter for the word to modify:

private ModifyVocabularyWordScreen(
    VocabularyWord? existingWord = null)
{
    ExistingWord = existingWord;
    Spelling = existingWord?.Spelling ?? string.Empty;
    Pronunciation = existingWord?.Pronunciation ?? string.Empty;
    Phrase = existingWord?.Phrase ?? string.Empty;
    Modified = false;
}

If an existing word is passed, the properties of that word are used to initialize our screen for use. Conversely, if no existing word is passed, we initialize with some sensible defaults. In either case, we mark our screen as unmodified.

Modification

As our user makes changes, we want to track whether there have been any changes made. There are two ways we can do this.

  • We can independently track modifications; or
  • We can compare our current state to where we started

For now, we’re going to use the first approach, but we will be able to change to the later approach down the track if we choose.

We do this by introducing the property Modified; when a new screen is created, we default this to false. Any change made to our screen results in the property being change to true, and it can be reset to false by calling ClearModified().

Messages

When a new word is saved, we need to add it to the VocabularySet we already have; this is handled by our existing message SaveNewVocabularyWordMessage.

For the situation when we’re modifying an existing word, we need a new message:

public class SaveModifiedVocabularyWordMessage : IReduxMessage
{
    public VocabularyWord OriginalWord { get; }
    public VocabularyWord ReplacementWord { get; }

    public SaveModifiedVocabularyWordMessage(
        VocabularyWord originalWord, 
        VocabularyWord replacementWord)
    {
        OriginalWord = originalWord 
            ?? throw new ArgumentNullException(nameof(originalWord));
        ReplacementWord = replacementWord 
            ?? throw new ArgumentNullException(nameof(replacementWord));
    }
}

As we’ve done before, we enact this message in our reducer, modifying the state of our application.

case SaveNewVocabularyWordMessage m:
    return currentState.UpdateVocabularySet(
        s => s.Add(m.Word))
        .CloseScreen();

case SaveModifiedVocabularyWordMessage m:
    return currentState.UpdateVocabularySet(
        s => s.Replace(m.OriginalWord, m.ReplacementWord))
        .CloseScreen();

I’ve shown the handling for both kinds of “save” message to emphasize their differences.

ViewModel & View Changes

Our ViewModel and the linked View also need to change, though the differences are minor.

Instead of a hard-coded caption, we now need one provided through data-binding, reflecting whether we are creating a new word or modifying an existing one.

We can now also use the command infrastructure we talked about a couple of weeks ago to hook up both Save and Close commands.

About the most complicated piece of the store is our Save() implementation which needs to choose which message to send:

public void Save()
{
    var word = Screen!.AsWord();
    var existingWord = Screen!.ExistingWord;

    IReduxMessage message;
    if (existingWord is null)
    {
        message = 
            new SaveNewVocabularyWordMessage(word);
    }
    else
    {
        message = 
            new SaveModifiedVocabularyWordMessage(existingWord, word);
    }

    _store.Dispatch(message);
}

Startup Fixes

The last changes to mention this week are a couple of fixes for errors I made last time.

Adding a namespace for our Program class broke compilation by renaming the class; this is fixed by changing the <StartupObject> property in the WordTutor.Desktop.csproj file.

Implementing IDisposable on WordTutorViewModel triggered a diagnostic warning from our DI framework. Ensuring we dispose of this ourselves in Main() means that it’s safe to suppress that warning.

Prior post in this series:
Hashcodes
Next post in this series:
Modifying Words, Part the Second

Comments

blog comments powered by Disqus