For me, the redux architecture has been a game changer in how I write UI programs. All the common problems surrounding observability, which is so important for good UX, are neatly solved without signal spaghetti or having to trap the user in modal dialogs.
For the past two years, we have been working on writing a whole suite of applications in C# and WPF, and most programs in that suite now use a redux-style architecture. We had to overcome a few problems adapting the architecture and our coding style to the platform, but I think it was well worth it.
We opted to use Odonno’s ReduxSimple to organize our state. It’s a nice little library, but it alone does not enable you to write UI apps just yet.
Unidirectional UI in a stateful world
WPF, like most desktop UI toolkits, is a stateful framework. The preferred way to supply it with data is via two-way data binding and custom view-model objects. In order to make WPF suitable for unidirectional UI, you need something like a “controlled mode” for the WPF controls. In that mode, data coming from the application is just displayed and not modified without a round-trip through the application state. This is directly opposing conventional data-binding, which tries to hide the direction of the data-flow.
In other words: we need WPF to call a function when the user changes a value in an input control, but not when we are updating the value from our application state. Since we have control when we are writing to the components, we added a simple “filter” that intercepts our change event handlers in that case. After some evolution of these concepts, we now have this neatly abstracted in a couple of tool functions like this:
public UIDuplexBinder BindInput(TextBox textBox, IObservable<string> observable, Func<string, object> actionCreator)
{
// ...
}
This updates the TextBox
whenever new values are coming in on the IObservable
, and when we are not changing the value via that observable, it calls the given action creator and dispatches the action to the store. We have such helper functions for most of our input controls, and similar functions for passive elements like TextBlock
s and to show/hide things.
Since this is relatively straight-forward code, we are skipping MVVM and doing this binding directly in the code behind. When our binder functions are not sufficient, which sometimes do more complex updating in view models.
Immutable data
In a Redux-style architecture, observability comes from lightweight diffing, which in turn comes from immutable data updates in your reducers.
System.Collection.Immutable
is great for updating the collections in your reducers in a non-mutable way. But their Equals
implementation does not behave value-like, which is needed in this case. So in the types that use collections, we use an extension method called LazyEquals
that ||
s Object.ReferenceEquals
and Linq.Enumerable.SequenceEqual
.
For the non-collection data, C#9’s record types and with
expressions are great. Before switching to .NET 5 earlier this year, we used a utility function from Converto, a companion library of ReduxSimple, that implements a .With
via reflection and anonymous types. However, that function silently no-ops when you get the member name wrong in the anonymous type. So we had to write a lot of stupidly simple unit-tests to make sure that no typos slipped through, and our code would survive “rename” refactorings. The new with
expressions offload this responsibility to the compiler, which works even better. Nothing wrong with lots of tests, of course.
Next steps
With all this, writing Redux style WPF programs has become a breeze. But one sore spot remains: We still have to supply custom Equals
implementations whenever our State types contain a collection. Even when they do not, the generated Equals
for records does not early-out via a ReferenceEquals
, which can make a Redux-style architecture slower.
This is error prone and cumbersome, so we are currently debating whether this warrants changing C#’s defaults via something like Undefault.NET so the generated Equals
for records all do value-like comparison with ReferenceEquals
early-outs. Of course, doing something like that is firmly in danger-zone, but maybe the benefits outweigh the risks in this case? This would sure eliminate lots of custom Equals
implementations for the price of a subtle, yet somewhat intuitive behavior change. What do you think?
Hi would you mind sharing a project template with all the stuff you discussed here. I would appreciate it.
Hey there! I can do that, but it’ll take some time to extract. But we’re already using it in a few projects now, so it might actually be a good investment now.
Thanks, I will be waiting for it, I followed you on Twitter/Github