Before we can start adding screens to our application, we need to set up some infrastructure for our WPF specific classes. In order to keep the
WordTutor.Core technology independent, we’ll do this by creating a new project.
First, we need to install .NET Core 3.0 - at the time of writing, the latest version is Preview3.
After installation, we can use the dotnet command to create our WPF project:
PS> dotnet new wpf -o WordTutor.Desktop The template "WPF Application" was created successfully. Processing post-creation actions... Running 'dotnet restore' on WordTutor.Desktop\WordTutor.Desktop.csproj... Persisting no-op dg to ...\WordTutor.Desktop\obj\WordTutor.Desktop.csproj.nuget.dgspec.json Restore completed in 7.68 sec for ...\WordTutor.Desktop\WordTutor.Desktop.csproj. Restore succeeded.
This creates an empty WPF application, ready to run.
We also need to create an xUnit test project:
PS> dotnet new xunit -o WordTutor.Desktop.Tests The template "xUnit Test Project" was created successfully. Processing post-creation actions... Running 'dotnet restore' on WordTutor.Desktop.Tests\WordTutor.Desktop.Tests.csproj... Persisting no-op dg to ...\WordTutor.Desktop.Tests\obj\WordTutor.Desktop.Tests.csproj.nuget.dgspec.json Restore completed in 29.79 sec for ...\WordTutor.Desktop.Tests\WordTutor.Desktop.Tests.csproj. Restore succeeded.
Tip: After adding these new projects into our
.sln file, check to see if any of the referenced packages is in need of updating. In my case, I found a few minor updates to apply. This isn’t surprising, given that it’s far easier to update a NuGet package than it is to rebuild and distribute a project template.
As in any WPF application, our view models need to adhere to some specific contracts. These behaviours are needed so that the user interface itself updates appropriately.
To make it easier for us to adhere to these contracts, let’s create a base class for all our view models. This foundation will help us create a “pit of success”, where it’s easier to do thing the right way than to make a mistake.
Our base class needs to implement
INotifyPropertyChanged. This is the interface used to notify user interface controls when things have changed and an update is needed.
PropertyChanged event includes a
PropertyChangedEventArgs argument that names the single property that has just been changed. If many properties are being changed in one go, the property name can be omitted, informing consumers that every property should be checked.
To trigger the event when needed, let’s create a helper method:
I like the way the
?. operator (aka the Elvis operator) allows this method to be simplified. Without it, we’d need to cache the current value of
PropertyChanged in a local variable to avoid potential threading issues. Fortunately, the
?. operator does its job in a threadsafe way.
Recently, I learned that the
Invoke() method on a delegate is somewhat special. The implementation of
Invoke() is synthesized by the compiler as needed! This means that there isn’t a performance cost to use.
For example, calling the event directly results in this code:
Changing this code to use
Invoke() doesn’t change the generated IL very much. In fact, it looks pretty much identical:
Ok, it looks exactly identical. Did you spot the call to
Invoke() in the first method?
As usual, a few minutes of actually testing something beats hours of online prognostication. There are a lot of folks who suggest
Invoke() calls are much much slower. Seems they’re wrong.
OnPropertyChanged() method is private because I don’t want actual view models to be calling it directly. As a part of the pit of success mentioned earlier, I want to instead provide a set of helper methods that do the right thing, making it easy for later implementations to be correct.
For example, for an int property, this method will do everything required:
[CallerMemberName] attribute gives us a trouble free way to obtain the name of the property, requesting the C# compiler to insert the correct name at compile time automagically.
The short-circuiting is also important; without it, our user interface will flicker at strange times.
We return true if a change was made, so that our properties can trigger other activities. If no change was made, we return false.
Here’s a similar method for enumerations:
There are equivalent helpers for string, for
DateTimeOffset, and for
TimeSpan. In the interests of space, I won’t go through that code here.
Referencing a Model
Many of our view models (but perhaps not all) will be referencing a single model instance retrieved from our Redux store. We can avoid a lot of boilerplate with a base class:
ModelUpdated() method provides a hook for subclasses to use when updating all of their properties in one go.
IViewModel interface allows us to group related viewmodels together, as long as their models are compatible.
The user interface widgets available in WPF don’t support multi-threading; all interaction with the controls must happen on the same synchronisation context. (For experts only: there are some exceptions if you’re very very careful.)
Within this contract is a little trap - the events fired from our view models count as interaction. If our events fire from a different synchronization context, we will get some very odd bugs in our application. We might end up with stale information showing on screen, we might have our application crash completely, or other things might go awry.
One way to address this is to be extremely very careful with our updates, ensuring we only update our models from the main synchronization context of our application. I don’t find this to be a particularly reliable approach - it’s too easy for a later optimization (say, moving something to a background task) to cause problems.
Instead, let’s explicitly pay attention to our synchronization context and make sure that we abide by the WPF contact.
In our constructor, let’s capture the active context and reuse it when we fire our events:
If our current synchronization context is the one we captured in our constructor, we send the event straight away. If it isn’t, we use
Send() to synchronously transition to that context and send the event that way.
With this, it doesn’t matter which thread we’re using when we update our models, the events used to keep our user interface synchronized will always be dispatched in a safe way.
Now we’ve got this foundation in place, we can start looking at our first screen - to add a new word.