For our second screen, we’ll create a browser to show all of the words in a single vocabulary list. Let’s start with a small bit of design, so we know what we’re aiming to build.

As before, I’ve created a low fidelity mockup. Our screen needs to show the current list of words, allowing for one of the words to be selected for modification or removal. We also need a few buttons, used to trigger various update actions.

The structure that we’re building follows the prototype of the Add Word screen - an immutable model class to capture our state, a WPF user control for interaction with the user, and a view-model that bridges the flow of information between the two. Rather than digging into details that we’ve already seen before, let’s focus on the new and different.

Vocabulary Browser Screen

Our screen contains the boolean property Modified and we need to have a way to make changes.

However, creating a WithModified() method would create a bit of a problem. For one, boolean parameters are evil. For another, we need to treat our model types as proper classes with behaviour, not just simple data carriers.

So instead of a single method, let’s create two: One to flag that our vocabulary has been modified, and one to flag that the list hasn’t been changed. Here’s one of them:

public VocabularyBrowserScreen MarkAsModified()
{
    if (Modified)
    {
        return this;
    }

    return new VocabularyBrowserScreen(
        this,
        modified: true);
}

Similar to other model classes, short-circuiting is used to reuse instances where possible.

The other method (FlagAsUnmodified()) is symmetric.

Vocabulary Browser View Model

This is our first view model that exposes a collection:

public class VocabularyBrowserViewModel : ViewModelBase
{
    private readonly ObservableCollection<VocabularyWord> _words;

    public ObservableCollection<VocabularyWord> Words
    {
        get => _words;
    }
}

Unlike the view model properties we’ve seen before, the property is read-only. This is because of the way WPF binds to collections: replacing one observable collection with another will result in a lot of on-screen flickering.

Instead, we retain a single readonly collection and update the contents when needed.

In our method to update our view model from the application, we use UpdateCollection() to update the contents of the collection.

Aside: I should point out at this point that the current implementation of UpdateCollection() is the simplest thing that could possibly work - it too will cause a lot of flickering, but we’ll address that in a later post.

Our previous view model could pull all of the information it needs from one location - but here we need to reference both the current screen and our core application state.

private void RefreshFromApplication(WordTutorApplication application)
{
    var screen = application.CurrentScreen as VocabularyBrowserScreen;
    if (!(screen is null))
    {
        Selection = screen.Selection;
        Modified = screen.Modified;
    }

    if (!(application.VocabularySet is null))
    {
        var words = application.VocabularySet.Words
            .OrderBy(w => w.Spelling)
            .ToList();
        UpdateCollection(_words, words);
    }
}

We sort the words before updating the collection, so they’re easier for our users to scan. To avoid unsightly crashes, this method has to check that information is available before it accesses it.

The Selection property allows us to track which word has been selected by the user; when it’s modified, we dispatch a SelectWordMessage to update our screen model.

public VocabularyWord Selection
{
    get => _selection;
    set => UpdateProperty(
        ref _selection,
        value,
        sel => _store.Dispatch(new SelectWordMessage(sel)));
}

Vocabulary Browser View

Our browser view uses a ListBox to show all the words. This is one of the more powerful WPF controls as the ItemTemplate gives great control over how the items in the list are presented.

<ListBox
    Grid.Row="3"
    Grid.Column="1"
    ItemsSource="{Binding Words, diag:PresentationTraceSources.TraceLevel=High}"
    SelectedItem="{Binding Selection}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Spelling}"
                            FontSize="24"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

For the DataTemplate, we use a StackPanel to arrange things; this might seem odd, given that we have just the TextBlock - but we’ll use this later on when we add some buttons.

Prior post in this series:
Revisiting ViewModelBase
Next post in this series:
Restructuring Reducers

Comments

blog comments powered by Disqus