SkyLight: Composable UI Hackery with WPF
First off, This is my delayed F# Advent 2018 post I didn’t get posted, I was originally going to write a post on Push/Pull paradigms and how they were so closely related, but I was late and someone else ended up releasing a great post on it so I had to change topic and write on something else. I’ve written about web stuff before so was looking for another new topic.
I have seen the recent developments with Fabulous on the Xamarin side, and Fable/Giraffe templating on the Web side and thought it would be nice if there were some composable way to create UI applications for the Desktop, like in WPF. Unfortunately WPF is currently windows only, but given I’m on Windows, and with the open sourcing of WPF and integration into .net core 3, we may still someday see WPF make it’s way to cross platform … and if not there is always Avalonia.
Designing an alternative approach to WPF
So moving forward with WPF as a POC, the first thought was to do another MVU Elmish model, given it’s all the rage these days in the F# community. After looking at how it works , re-rendering the entire view on each model update, I felt it may have scalability problems and wanted to explore alternatives approaches.
WPF has a model of Dependency Objects & Properties and normally to use controls and bind values you use property wrappers, there is no virtual Dom mechanism as there is no slow web dom to begin with, the vdom Web model just isn’t as massive a win for WPF. At a later stage I would like to try implement propery delta vdom at a low level in WPF , but for now I will focus the first steps of making WPF more composable.
To dive a little deeper into the WPF model, the reason it does not use POCO objects for all the controls is for three main reasons (among others):
- They need to build a dependency graph for propagating targeted updates
- The nested dictionaries also give the benefit of trickling down properties to children such that as long as a parent obj has the key, a call for it will return a value ( rec prop exists? try parent) , while conversely, a property updated on a local control dependency object will override and use that inserted value for itself and all it’s children
In order to make the dependency model accessible for OO .net Dev’s who think in POCOs, they put wrapper property definitions on all the dependency properties in order to avoid interacting with static Dependency Properties.
member x.MyProp with get() = x.GetValue(Control.MyPropProperty)
The issue is, for data binding to a POCO model; that is outside the dependency model; you need to get creative so that the graph will push updates into your model, and your model will push its updates into the graph. As a an afterthought, and perhaps pushback from devs, WPF released an
Putting all the binding failings aside, and knowing what it’s doing under the covers, we should be able to model the pushing of updates and avoid using the
INotifyPropertyChangedinterface if we correctly push data into the graph reactively. I do not need / want to explicitly use a reactive library as it’s more fun and specialised to roll out a simple implementation ourselves. To maintain easy compatibility with POCO model (for serialisation etc), I have initially chosen to create bind environments where we target the properties to update using FSharp Quotations (
<@ model.Property @>), which for now forces us to use mutable properties but it will be ok for the proof of concept. This manually wired up event spaghetti will be efficent and direct but require managing handler disposal.
An Alternative Binding Strategy
Now that we have decided to circumvent the traditional interface binding model with our own events driven one, we can look to the UI now. The biggest factor on designing a new POC F# library was to make a type safe composable one that did not need XAML. I know loads of people love XAML, makes them feel like their working in good ole HTML, but with the composability of F#, and the OO awkwardness of C#, you can see why it works well with C# but is clunky and verbose for F#. Fable/Giraffe templates are clean, terse, and type-safe in comparison to XAML files. XAML may seem like a nice and easy HTML alternative, which it is, but for binding triggers,child properties, namespaces etc, the xml can start to get really ugly with little to broken type feedback … and who likes working with xml namespace/path resolution? I do not. The one advantage Xaml does have is its ability to be loaded standalone into an emulator/viewer which is very helpful and productive but If we could build a clean F# DSL to build UI it could be terse, composable and type checked, a decent compromise. I looked at both how Giraffe / Fabulous templated and noticed they both fall victim of re-rendering the entire view each time, my Zebra view engine does not and performs a little better in TechEmpower as a result, so I once again figured I would sacrifice the cleanness of an immutable model for a mutable model like in a MVVM type application … but try and reduce the complexity to be more like a MVU model.
MVM (a MVVM/MVU Hybrid)
My Model View Mutate pattern is a hybrid of both MVVM & MVU, potentially being the worst of both but that’s ok just hacking and fleshing out the design possibilities. In all patterns there is still essentially a view, a view model, and a model.
In MVVM, the View Model is an explicit and separate POCO that you need to bind your controls to, incorporating the views state, and to save the model, there is a mapping action required from view model to model or visa versa (as you normally will not be saving view state data to your database).
In MVU instead of having a persisted VM state object, we map our model into the view on the render function each update, this view has a VM state built into it by the render and is persisted till the next render. The MVU pattern is indeed cleaner, instead of maintaining two separate model states, you are mapping the model into the view’s state (VM) each render. The obvious drawback to this is that it is tearing down and rebuilding entire UI elements and negating the targeted dependency graph infrastructure WPF is built upon. Some progress has been made to cache some static elements in Fabulous reducing rebuild work as well as improving virtual dom plumbing but we do not have this in WPF yet.
My MVM pattern will do a one-time initial static “render” build out of the view from it’s mutable model, registering Model / ViewModel binds, and after that all UI updating is done through targeted mutation events. This is similar to rendering in MVU, with the VM logic embedded in the single view/render function; all type checked with View; but only running the render one time at startup, with mappings handling deltas. UI Updates will re-use created UI Components, pushing targeted updates on properties to their bound controls. The FSharp Quotes are converted into compiled Lambdas, Func/Action, capturing the object instance and property path, so by mapping our actions and binds as functions that return values, the plumbing will set the target property on our model, as well as propagating it through our UI graph. Some VM related state may not be able to be mapped in view from our model (such as state of a request etc, purely view related state), so for these we would need to create small sub models or ref variables to hold view state.
With the logic being contained in the view it could make the views a mess but thanks to composability, we can separate out all the separate domain pieces into composable sub-view functions and manage the complexity that way. In this way we can have a more Domain Driven UI, where loads of small encapsulated logic components can be tacked into our window UI component tree. We will proceed with a convention that each view render function takes a model/submodel and returns a 1–1 view.
Now to look at the main focus, how to compose the UI? A DSL? There are loads of WPF controls so building a helper library would be pain, also, for reasons already mentioned, I do not want to have property slots for all properties as the override of property values is dynamic, I could try lists of functions/DUs like Giraffe, or I could try overloaded constructors like Fabulous, but to be different, and to try a different composition pattern, I chose to mix it up with Computation Expressions (“CE”).
Building with Computation Expressions requires a lot of boiler plate code, the code would need to natively bind to my binding event model, and handle dispatch correctly. UI controls need updates to be pushed on from UI thread while events usually are triggered from a non-UI thread.
By copying and pasting a few c# def files into one big text file, I used an f# script to build a CE file for a load of the controls, As its a rough and dirty POC, I didn’t include many interfaces and less used controls, but they can be added later if/when needed.
In addition, given there is always some confusion and difficulty with doing async work around the UI, I included a simple natively handled worker type called
Work which is just a DU of
Work. The events are overloaded to accept a function
CancellationToken -> Task so that users can integrate the cancellation Token into Task method overloads being used, to ensure they can be cancelled correctly like shown:
Wrapping all this together you get a basic and limited POC library I call Skylight as it is a ‘high level window’ library. There are many unresolved issues like how can you emulate UI edits for testing, integrating 3rd Party Controls etc, but that is beyond this POC. A Basic example of the API is given below for a taster.
Skylight is relying on a few conventions and patterns so I will outline:
- The CEs return Binder interfaces that are handled natively for nesting and disposing of resources and binding.
- CE operations allow control properties to be set with a value or bound via a binding target, provided by an FSharp Quotations. To ensure updates to the property are picked up by the UI, updates to the property need to be provided as the return value of an Action bound to a target, setting a property via
<-will not propagate through the bindings.
(<@ model.prop @>, fun () -> "newPropVal")
- Although we need to set the properties via our binding function return values, getting model values directly off the model is fine
- for events/actions like
onClickwe can provide a plain action, a task, or a binding action, where we specify the target to update, then the update function.
- for normal control properties, we can just either set a static value, bind a target, or bind with mapping functions. Binding with mapping functions allows us to watch a model property, and then update the UI when it changes with a mapping function.
- For control events like
onClickwe can either provide an action function, with no mutation, or provide a property target and run a function that returns a value that will be assigned to the target.
- Given there are many combinations/permutations of setting multiple values, there will be overloads on the CE operations, to allow different combinations or setter/bindings.
I wanted to see what a WPF computation expression UI might look like, and at the same time, try implement my own binding system with WPF that did not rely on the existing interface. This is not meant to be a new production UI library, but more a jumping off point to what may be possible if the design patterns are interesting to some. The flaky demo app is now on Github here.
For the next iteration, i will be looking to extend communication hierarchy with some kind of MVU messaging, as well as custom (service) event binding so that sockets etc can be fed into the binding and trigger updates in same environment as the UI Binding, and potentially, an immutable model that uses an alternative diffing model, calculated, targeted delta analysis at compile time, not at runtime as we are statically typed and should do better.
Big thanks to Reed Copsey Jr for doing a review and providing feedback.