TL;DR
The Uno platform allows native UWP code to be run across Windows, Android, iOS and even in the browser. In this post I will cover the use of the Uno Platform to implement the 7GUIs: A GUI Programming Benchmark, across 5 platforms, employing FRP paradigms and all in a (mostly) seasonal style! Will it all hang together? Read on to find out.
The Twelve Days ... err.... Seven GUIs of Christmas
This is a lengthy post so first up, let me provide you with a seasonal - if a little tenuous - index:
Ahem...
On the seventh day of Christmas my true love gave to me:
Seven GUIs shining
Six points opining
Five platform bin[arie]s!
Four important words
Third advent yens
Dual screen loves
And an app-bridge for UWP
Well, it almost works.
Anyway, this post starts with "the first day of Christmas" and provides some background to, and explanation of, the use of the Uno Platform for implementing the 7GUIs "programming benchmark". If you're not interested in this background and just want to see the actual GUIs in action then feel free to jump ahead to the "Seven GUIs shining" or the Conclusion where I summarise my findings.
On the first day of Christmas... an app-bridge for UWP.
Well, to be honest, it was some time before the first day of Xmas when I became aware of the Uno platform by nventive:
a Universal Windows Platform Bridge that allows UWP-based code to run on iOS, Android, and WebAssembly.
I have been writing Xaml for over a decade and find it to be the most powerful and productive UI framework I've ever encountered. As such, I was immediately intrigued by the Uno platform and interested in what it might bring to the table for a UI technology that is increasingly being overlooked in favour of (the scourge that is) web front-ends.
Obviously there had been attempts at this kind of thing before - most notably Xamarin and Avalonia - but the approach taken by nventive is notable in that, instead of having to learn a new dialect of Xaml and/or buy into a framework to the exclusion of all else, they would allow UWP code to be run 'as is' across each platform. Furthermore, by supporting transpilation to WebAssembly, the same code could then be run in the browser.
Unfortunately, I was a little too busy to dive into it at the time so I added the Uno Platform to my (ever growing) backlog of things to evaluate and continued with current projects until...
Dual Screen Loves
... October, when Microsoft's Panos Panay surprised everyone with these beauties:
A new category of Surface device featuring dual screens, similar to the previously cancelled - but much lauded - Courier project.
Having been mourning the loss of Windows Mobile/Phone since begrudgingly switching to an Android device some months back, these devices looked like salvation. The two devices, aimed at productivity and mobility respectively, looked amazing in the introductory videos and I was immediately, and unashamedly, sold.
Until, that is, I learned something rather perplexing. You see, while the larger device - the Surface Neo - would be running Microsoft's new Windows 10X OS, the smaller 'mobile' device - the Surface Duo - was apparently running a heavily customised version of Android.
What? Really? Surely not!
While I understood why Microsoft might not want to re-engage in a smartphone market war it had already fought for and lost (twice!), Windows 10X was supposedly based on CShell, a UI layer specifically designed to adapt to any form factor and - it would seem - perfectly suited to running on devices like these. Indeed, I wasn't the only one surprised by this announcement and many echoed my confusion, some even going so far as to set up a petition to provide the Duo with Win 10X.
Despite the immediate outcry, Microsoft remained adamant that the Duo will run Android and so it was that I realised that the Uno platform seemed perfectly - and almost uniquely - positioned as a framework for developing apps for these devices. Then...
Third Advent Yens
yen n. - A strong desire or inclination; a yearning or craving.
... the day after Microsoft announced these devices, two articles arrived in my news feed: "The Third Annual C# Advent" from Matthew Groves's blog and, 7GUIs: A GUI Programming Benchmark.
"So let's get this straight", I thought, "I have a GUI framework I want to evaluate, a GUI programming benchmark to attempt and a call for blog posts on .NET technologies". Well, sometimes synchronicity simply can't be ignored. I decided a blog post on the 7GUIs using the Uno platform would make for a great post while allowing me to evaluate this platform for UWP + Android development ahead of the release of the Surface Neo and Surface Duo devices.
I applied for a slot in the Annual C# Advent series and got the 16th December, which is what you're reading now. If you enjoy this post (or even if you don't!), perhaps you might like to take the time to visit the full list of posts in this series and check out some of the others. There are loads of great posts by some really terrific authors.
Four Important Words
Over the course of a software engineering career spanning more than 20 years, I have developed and refined (and occasionally changed) opinions about how best to perform this craft. While most of my career has been spent following OOD/OOP paradigms, in recent years I have developed a strong preference for declarative programming - typically using some form of functional, reactive programming (FRP) - following behavioural/domain driven design principles. I wrote a blog post back in 2016 showing a practical application of these principles for an app I had released and, in the intervening years, have become only more convinced that these approaches represent an elegant and productive means of taming complexity in many modern software systems.
Below I outline four "important words" (aka principles) I intend to use while implementing the 7GUIs.
Behavioural
From Wikipedia:
Behavior-driven development combines the general techniques and principles of TDD with ideas from domain-driven design and object-oriented analysis and design to provide software development and management teams with shared tools and a shared process to collaborate on software development.
While I agree with this quote, for me BDD doesn't stop at writing tests in a behavioural style but should instead permeate deep into the implementation of the software itself. Fundamentally I believe the desired behaviours of the software system should be encapsulated and expressed in such a way that even a non-technical reader could see reference to them should they happen across the code.
In a blog post back in 2015 I explored ways in which this might be achieved through the use of declarative FRP principles. While aged (and containing a questionable use of a Subject), I believe this post still provides a decent introduction to how specific behaviours - i.e. 'The login button should be enabled when the user has entered both a username and a password' - can be implemented in, and fully encapsulated within, a single appropriately named method - i.e. ShouldEnableTheLogInButtonWhenTheUserHasEnteredBothUsernameAndPassword
.
Declarative
Paraphrased from Wikipedia:
Declarative programming focuses on 'what' the program must accomplish instead of 'how' that task is to be accomplished. This is in contrast with imperative programming, which implements algorithms in explicit steps.
Ostensibly, this can be seen through a comparison of the following two code snippets, both of which sum a value from a collection:
Imperative:
int value = 0;
foreach (var item in collection)
{
value += item.Value;
}
return value;
Declarative:
return collection.Sum(item => item.Value);
Now being declarative can be considered a somewhat relative term but, as you can see from the simplistic example above, if you follow the principle of expressing 'intent rather than algorithm' you can, in my opinion, greatly improve readability and transparency of functionality. Obviously this approach compliments the goals of behavioural design expressed above.
Functional
More Wikipedia:
[Functional programming] is a declarative programming paradigm in that programming is done with expressions or declarations instead of statements. In functional code, the output value of a function depends only on its arguments, so calling a function with the same value for an argument always produces the same result. This is in contrast to imperative programming where, in addition to a function's arguments, global program state can affect a function's resulting value. Eliminating side effects, that is, changes in state that do not depend on the function inputs, can make understanding a program easier, which is one of the key motivations for the development of functional programming.
Functional programming (FP) has become fashionable in the software industry recently and support for functional paradigms are increasingly being released for - and, in some cases, are core to - many languages and frameworks that were once solely object-oriented. Unfortunately FP still faces significant opposition from many quarters, mainly - I believe - due to a perceived complexity around it's core principles; a perception somewhat fomented by the idiom of many functional practitioners! Indeed, I often encounter this issue when discussing FP with clients and have to explain how it is, in many ways, far simpler than many of the myriad principles employed by those writing object-oriented code.
C# has enjoyed first class support for functional constructs since the introduction of LINQ back in .NET 3.5 yet many using the language today - even those employing LINQ-to-X features - are unaware of functional programming or how it can be used to simplify and improve their code. This is a shame because - as will be seen in many of the GUIs below - C# is able to elegantly mix OO and FP paradigms such that the relative strengths of each can be leveraged where they make most sense.
Reactive
Last couple of Wikipedia quotes for a while:
Reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change. [It] has been proposed as a way to simplify the creation of interactive user interfaces and near-real-time system animation.
Wikipedia goes on to state how:
In an imperative programming setting,
a := b + c
would mean thata
is being assigned the result ofb + c
in the instant the expression is evaluated, and later, the values ofb
andc
can be changed with no effect on the value ofa
. On the other hand, in reactive programming, the value ofa
is automatically updated whenever the values ofb
orc
change, without the program having to re-execute the statementa := b + c
to determine the presently assigned value ofa
.
This is a somewhat long handed way of saying that, in reactive programming, state is derived from declarative flows of functional computations over source values such that it is not necessary to explicitly recompute state when one of these values change.
Again C# has had fantastic support for Reactive Programming for many years in the form of the Reactive Extensions library. In fact, such was the success of this library that, since it's original implementation in C#, it has been ported to numerous other languages and used within an incredible variety of software systems; not least of which being a significant number of modern web frameworks.
Five Platform Bin[arie]s!
Well, sort of.
WPF
Out of the box, Uno supports four platforms; UWP, Android, iOS & WebAssembly. For this blog post, I also wanted to try supporting WPF due, in most part, to the following:
- WPF is XAML based and, in many ways, the progenitor to UWP so it should be able to leverage much of the code that will be written
- WPF still plays an important role in the .NET GUI ecosystem, particularly for enterprise applications.
- WPF was recently open-sourced as part of .NET Core 3.0 and I hadn't yet had a chance to try it!
Fortunately, it turns out that, once various other considerations had been accounted for (see below) supporting WPF didn't present much friction at all. In fact, with the exception of a couple of compiler directives in IValueConverter implementations, supporting WPF necessitated hardly any additional code at all.
iOS
Until recently, I viewed Apple devices as over-priced, walled-off, proprietary guff bought by fan-bois with more money than sense. In light of Apple's stance on privacy, this opinion has softened slightly in the last couple of years such that you may no longer have to be a fan-boi to buy an Apple device. Regardless I still refuse to buy Apple products and am extremely happy with a combination of devices from other manufactures - most notably Microsoft and Dell. These manufactures don't artificially limit the interoperability of their products nor do they engage in 'planned obsolescence' to force consumers into a viscous and costly upgrade cycle (seriously, I have a 7 year old Dell laptop which - despite being bulky - is still my main mobile workhorse).
Unfortunately, given Apple's ludicrous position that you must own (or rent!?!) an Apple device to build applications for iOS (it seems it's still illegal to run iOS on anything other than an approved Apple device), I have no way of testing the iOS binaries being produced herein. As such, all the screen shots for Apple will be a simple placeholder until some kind soul decides they want to run them for me (I promise not to call you a fan-boi) and provide screen shots / bug reports / PRs.
Six Points Opining
Some other considerations for implementation:
1. Model-View-ViewModel
To me, the MVVM pattern is one of those methodologies that once you "get" you wonder how you ever managed to deliver anything without. In fact, decoupling a view from its interaction model is critical if you wish to achieve many of the paradigms listed above (i.e. behavioural driven, declarative, functional and reactive). Therefore, all GUIs presented here use this pattern despite it possibly rating lower on a number of the 7GUIs Dimensions of Evaluation.
This then leads us to the question of whether to adopt a 'View-First' or 'ViewModel-First' approach (a good comparison of which can be found in this SO question). While I generally prefer the latter over the former - particularly when writing large applications - the 'ViewModel-First' is undeniably more complicated given that UWP and WPF apps adopt, by default, 'View-first' mechanisms (e.g. StartupUri). So, for simplicity's sake, I've used a 'View-First' approach for these GUIs wherein each View creates the associated ViewModel within it's constructor.
Lifetime management of the view model (and it's constituent behaviours) is provided by Activate
/Deactivate
calls to the ViewModel from within the OnNavigatedTo
/OnNavigatedFrom
methods (for UWP) or OnActivated
/OnClosed
(for WPF). Where the GUI being implemented requires intimate interaction with the UI (i.e. the CircleDrawer GUI), these interaction points are passed - as IObservable
instances - to the ViewModel as parameters to the Activate
call.
2. ReactiveUI
The Uno platform comes with templates for implementing GUIs using ReactiveUI:
An advanced, composable, functional reactive model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming.
Ostensibly this sounds like a great fit for satisfying the paradigms outlined above and I had initially intended to use ReactiveUI as another exploratory feature of this blog post. Unfortunately this intention was quashed almost immediately as I found the following:
- Platform support
At the point of attempting to use ReactiveUI, the latest Universal Windows platform it supported was 1803 (the April 2018 Update). This was quite surprising and somewhat troubling given the age of this platform. - Inheriting a Framework
ReactiveUI requires that ViewModels derive from specific base classes. This, to me, puts it firmly on the Framework side of the Framework vs Library debate. Given I would already be adopting various constraints from the Uno platform, I didn't want to find myself in a position of encountering potential incompatibility between these technologies some way down the line. - Overly complex API Structure
Just getting started with ReactiveUI is a daunting prospect when its handbook contains some 22 sections on subjects ranging from data persistence to logging. While I appreciate that opinionated frameworks can have a lot to offer, this sort of complexity is indicative of one which, in my opinion, ought to be broken into smaller, composable libraries which can be adopted as and when necessary.
3. MVx.Observable
Rather than shoe-horn ReactiveUI into this evaluation I instead elected to modernize and generalize a package I had written some time ago: Caliburn.Micro.Reactive.Extensions. This package, as it's name implies, was implemented specifically for use within the Caliburn Micro framework and provides all the "good stuff" of ReactiveUI (i.e. composable, functional, reactive) while remaining a library that places no untoward restrictions or dependencies on consumer code.
Fundamentally this library constitutes just three classes:
- ObservableProperty
A class which implements bothIObservable<T>
andIObserver<T>
- such that it can be used within declarative reactive flows - and which provides some additional functionality to facilitate its use as a data-binding source (i.e. Get/Set methods). - ObservableCommand
A class which implementsICommand
through implementations of theIObservable<bool>
(for 'can execute' state changes) andIObservable<T>
(for invocations) interfaces. - ObservableBus
An "observable" implementation of an Event Aggregator pattern for inter-ViewModel communication.
Generalizing this library required removing the use of Caliburn.Micro's base classes - which were typically used to facilitate property change notification - and replacing them with callbacks (which can then be hidden in derived classes for specific frameworks if so desired). Modernizing involved recreating the project as a .NET Standard 2.0 library and using up-to-date dependency versions (specifically System.Reactive 4.x).
The most difficult part of bringing this library up to date was undoubtedly picking a name for it. I spent ages playing with acronyms that accurately reflected the library's purpose and, for a second, even toyed with naming it the "IBCBDFR". Finally I decided on 'MVx.Observable' to indicate its reactive nature and its applicability to multiple forms of the Model-View pattern.
Source is available on GitHub and a prebuilt package is available in Nuget should you wish to give it a try.
4. Separate Views
The Uno platform is, somewhat amazingly, able to display (almost) the exact same XAML page across multiple platforms (or 'heads' to use Uno parlance) with a very high degree of fidelity. This is quite an achievement and the team at nventive are rightly proud of this capability.
However, from the perspective of someone looking to write large applications on this platform, I don't believe this facility is particularly important nor - to a certain extent - even desirable. You see, in my experience, it is often the case that each platform and/or form-factor requires such different UI and/or UX that trying to shoe-horn everything into a single XAML page results in a page that is difficult, if not impossible, to maintain. Instead I find it much better to be able to share business logic, user flows and - where it makes sense - common controls across platforms while using specific page layouts for each form-factor.
As such, the GUI's presented here will use a dedicated view implementation on each platform while using a common view-model to share interaction patterns and user-flows.
5. Implementation Process
For each of the GUIs I separated implementation into two phases; first implementing the prerequisite functionality in UWP then porting (which mainly constituted lots of copy/pasting) the completed GUI to Uno. This allowed me to ensure the GUI was functionally complete prior to incurring the additional friction of multi-platform builds.
6. Project Structure
When you create a new Uno project, the standard template gives you the following structure:
Solution
|-Project.Droid
|-Project.iOS
|-Project.UWP
|-Project.Wasm
|-Project.Shared
In this structure, the 'Shared' project contains the majority of the code/Xaml including the App.xaml[.cs] and MainPage.xaml[.cs] files. It is literally a "Shared Project" which each of the other project reference in order to - in essence - copy its contents into themselves.
To support WPF and remove the need to share a single view across all platforms (as described above), I refactored to the following structure:
Solution
|-Project.Common
|-Project.Droid
|-Project.iOS
|-Project.UWP
|-Project.Wasm
|-Project.Wpf
|-Project.Shared
In this structure, the (fully implemented) App.xaml[.cs] and MainPage.xaml[.cs] were copied from the Shared project to each head project where they could be suitably customised. Next the ViewModel (and any other reusable code) was moved to the new .Net Standard 2.0 Common project and referenced by each head project. This left the Shared project containing just assets and, where necessary, any common controls or IValueConverter
implementations.
Seven GUIs Shining
Now, without further ado (of which there has been plenty), may I present the 7GUIs!
Each of the GUIs shown here represent an implementation of one of the tasks from the 7 GUIs task list. Despite the fact that many of the GUIs in this list represent - in my opinion - particularly poor UI/UX implementations, I've endeavoured to stay as close as possible to the example GUI provided for the task so that my implementation might be easily compared to other implementations.
Furthermore, given the various constraints presented by writing for multiple platforms, I have employed a 'lowest common denominator' approach to implementing some features. For example, dialogs are typically represented by messages appearing in the current UI rather than opening an additional modal overlay. Obviously each platform presents various mechanisms for implementing these common UI metaphors and it would be entirely possible, using the Uno platform, to leverage these mechanism by means of platform specific implementations of an abstracted interface. However, for expediency, I chose not to do this at this time and to just keep it simple.
For each GUI I have provided a visualization of the GUI running on each platform (click/press to see a larger version) along with a description of the specific challenges and issues presented by the implementation. It is my hope that these details provide the reader with a good insight into the value of implementing interaction patterns as a series of declarative, functional, behavioural flows along with how the Uno platform may be used to provide 'write-once, run-anywhere' style, cross-platform, native applications.
1. Counter
Challenge: Understanding the basic ideas of a language/toolkit.
Implementation
This task required just a single CounterShouldBeIncrementedWhenIncrementIsInvoked
behaviour in the view model. Given its simplicity, here's the behaviour in full:
private IDisposable CounterShouldBeIncrementedWhenIncrementIsInvoked()
{
return _increment
.Scan(0, (value, _) => value + 1)
.Subscribe(_counter);
}
In this example, _increment
is an Observable.Command
and _counter
is an Observable.Property<int>
. Both these members are exposed as properties for data-binding. And that's it. There's no class level _currentValue
member variable as all state is encapsulated within the reactive function.
2. Temperature Converter
Challenges: bidirectional data flow, user-provided text input.
Implementation
This task required just two behaviours in the view model; ShouldUpdateFahrenheitWhenCelciusIsChanged
and ShouldUpdateCelciusWhenFahrenheitIsChanged
. These behaviours follow the same pattern but use a different conversion so, for brevity, only the ShouldUpdateFahrenheitWhenCelciusIsChanged
behaviour as shown here:
private IDisposable ShouldUpdateFahrenheitWhenCelciusIsChanged()
{
return _celcius
.DistinctUntilChanged()
.Select(Domain.ConvertCelciusToFahrenheit)
.Subscribe(_fahrenheit);
}
As you can see, the actual domain logic has been split into a static function on a Domain
class and used to convert the values from the _celcius
Observable.Property<int>
into Fahrenheit which is then forwarded to the _fahrenheit
Observable.Property<int>
. As these values write to each other, the DistinctUntilChanged
operator is used to prevent a feedback loop.
3. Flight Booker
Challenge: Constraints.
Implementation
The relatively simple paragraph describing the behaviour of this task belies quite a bit of complexity and required five behaviours to fully implement:
ShouldEnableReturnWhenFlightTypeIsReturn
ShouldSetOutboundValidWhenOutboundDateHasValue
ShouldSetReturnValidWhenReturnDateHasValueOrFlightTypeIsOneWay
ShouldEnableBookWhenDatesAreValidForTheSelectedFlightType
ShouldDisplayMessageWhenBookInvoked
I won't dig into each of these behaviours as you should be getting the gist of how this might work now. If not you can find the implementation in the view model.
The ShouldSetOutboundValidWhenOutboundDateHasValue
and ShouldSetReturnValidWhenReturnDateHasValueOrFlightTypeIsOneWay
could have been implemented in the view with various bindings and converters but as they form part of the behaviour specification, they're kept in the view model so that they can be tested.
Amazingly, the most difficult bit of this task was finding a means of reliably setting the initial value of the "Flight Type" combo-box. It turns out that this was due to a bug in UWP that means the SelectedValue
property of a combo-box doesn't support binding to an enumeration property. This was eventually resolved by binding the SelectedIndex
property of the combobox to the enumeration via a converter.
4. Timer
Challenges: concurrency, competing user/signal interactions, responsiveness.
Implementation
This task is where the Reactive Extensions and MVx.Observables really shine. Here's the task description:
The task is to build a frame containing a gauge G for the elapsed time e, a label which shows the elapsed time as a numerical value, a slider S by which the duration d of the timer can be adjusted while the timer is running and a reset button R. Adjusting S must immediately reflect on d and not only when S is released. It follows that while moving S the filled amount of G will (usually) change immediately. When e ≥ d is true then the timer stops (and G will be full). If, thereafter, d is increased such that d > e will be true then the timer restarts to tick until e ≥ d is true again. Clicking R will reset e to zero.
And here's the single behaviour required to satisfy this specification:
private IDisposable ShouldIncrementElapsedUntilResetOrEqualToMax()
{
return _reset
.StartWith((object)null)
.Select(_ => Observable
.Interval(Interval)
.WithLatestFrom(_max, (interval, max) => max)
.Scan((long)0, (acc, max) => acc + Interval.Ticks >= max ? max : acc + Interval.Ticks))
.DistinctUntilChanged()
.Switch()
.ObserveOn(_scheduler)
.Subscribe(_elapsed);
}
To break this down: Every time the _reset
Observable.Command
is invoked and starting with an initial invocation, select a new observable. This new observable will emit a value at a specific interval which will be incremented automatically until it is equal to the _max
value. Once it is equal to the _max
value, no further values will be emitted until _max
changes or the timer is reset. Finally, subscribe to the new observable and forward the emitted values to the _elapsed
Observable.Property<int>
on the UI thread.
Now, admittedly the timer underlying this observable doesn't 'stop' once the value has reached the max value per the description. While it would be possible to do this in a single behaviour, the required expression would be significantly more complicated and, given the low cost of recalculating the value every interval, I elected to keep the observable simple and obvious.
5. CRUD
Challenges: separating the domain and presentation logic, managing mutation, building a non-trivial layout.
CRUD is a great test of a UI as it forms a matrix of operations across numerous controls. This is shown by the complexity of the description of the task and the number of behaviours required to meet this specification:
ShouldEnableUpdateWhenSelected
ShouldEnableDeleteWhenSelected
ShouldEnableCreateWhenNameAndSurnameArePopulated
ShouldPopulateNamesWithFilteredNames
ShouldAddFullNameWhenCreateInvoked
ShouldUpdateFullNameWhenUpdatedInvoked
ShouldRemoveFullNameWhenDeleteInvoked
ShouldPopulateNameWhenFullNameSelected
ShouldPopulateSurnameWhenFullNameSelected
6. Circle Drawer
Challenges: undo/redo, custom drawing, dialog control*.
I think it's fair to say Uno struggled with this task. While the UWP version was completed in short order, neither the Android nor WASM heads worked at all and the WPF project had several issues.
It turned out that both the UWP and WASM platforms suffered similar - but not the exactly the same - issues. The first issue I encountered was with determining where a user clicked on a Canvas used as an items container for a ListView control. Digging into this I found further issues where the TappedRoutedEventArgs
resulting from the click returned different OriginalSource
objects - sometimes the Canvas, sometimes the ListView - and getting the clicked position relative to the OriginalSource
always returned the position {X: 0, Y: 0}
. Then I found that the attached property binding mechanism I'd used to bind Canvas.X
and Canvas.Y
properties on the ListViewItem instances representing the circles just wasn't being applied consistently. The WPF head, on the other hand, struggled with distinguishing between clicks on an item and clicks in an empty area of the ListView such that it's not possible to reselect a circle once it's been deselected.
Perhaps a lot of the above issues could have been resolved by using a custom control or a custom item container for the ListView but, as the UWP solution worked perfectly, this was something I felt I ought not to have to do. Ultimately I hacked through as many of the issues as I could but this was one task where all the heads don't perform in the same way. Furthermore, I'm sure the WPF issue could be resolved by working out if the click caused an item to be selected but unfortunately I ran out of time before I could investigate this.
Anyway, this solution used a basic form of command processor to apply interactions to a state container which contained the current circles along with undo and redo stacks of commands. This provided a very simple mechanism for undoing or redoing a command such that the view model needed only the following behaviours:
ShouldPopulateCirclesFromState
ShouldEnableOrDisableUndoBasedOnState
ShouldEnableOrDisableRedoBasedOnState
ShouldEnableOrDisableAdjustDiameterBasedOnSelectedItem
ShouldSetSelectedFromState
ShouldShowAdjustDiameterDialogWhenAdjustDiameterInvoked
7. Cells
Challenges: change propagation, widget customization, implementing a more authentic/involved GUI application.
This project was a lot of fun to write; well, at least for the UWP head anyway. Never before had I even conceived of trying to write a spreadsheet, yet here I was six GUI's in with one to go and I didn't want to turn back now. It actually turned out to be both easier and more difficult than I had imagined:
Easier in that the business logic of a spreadsheet application turned out to be surprisingly simple and involved building a recursive dependency tree whenever any cell changed and then evaluating each of the dependencies using an off-the-shelf expression parser married to a custom expression visitor implementation. All this was isolated in a business domain with communication between this domain and the MVVM layer taking place via events transmitted over a message bus.
The view layer on the other hand was very, very frustrating. It proved to be impossible to find a DataGrid control for UWP that worked with both row and column virtualization so I ended up having to instantiate the entire spreadsheet ahead of time. This was possible due to the description of the problem specifying that there need be only 100 rows and 26 columns otherwise I would have had to write some (probably quite clunky) custom control or give up as it would have been too much work for a blog post. After this I then found that the DataGrid I had chosen to use (because it had been ported specifically to the Uno platform - Uno.Microsoft.Toolkit.Uwp.UI.Controls.DataGrid) didn't actually work for Android or WASM anyway and just displayed an empty grid.
It would have been fun to dig into the issues I found with the DataGrid on Android and WASM as it would have been cool to say that I'd written a cross-platform spreadsheet. Unfortunately I ran out of time and will, for now, have to admit defeat here.
Anyway, in this task the MainPageViewModel
simply consisted of a collection of RowViewModel
instances which, in turn, consisted of CellViewModel
instances. As I wasn't able to use virtualisation in the DataGrid, all behaviour was in the CellViewModel
and consisted of:
ShouldUpdateContentWhenContentChangedReceived
ShouldPublishTextChangedWhenTextChanged
Both these behaviours interact with the message bus to send change events or receive content updates.
Conclusion
Uno Platform
As I hope you have seen from the above, the Uno Platform really does make it possible to develop true "write-once, run-anywhere" applications. Its ability to provide a consistent UWP API across multiple devices/browsers is truly a monumental achievement.
However, as we have also seen, it can sometimes struggle with low-level or complicated UIs which can make some advanced UX patterns difficult to get working consistently across all platforms. I was very surprised that the DataGrid control didn't function at all on Android or WASM as this control is often considered central to a lot of line-of-business applications.
Fortunately Uno is currently under active development and has a very engaged community of developers reporting issues and submitting pull-requests. Version 2.0 of the platform has just been released and new packages are pushed to nuget almost every day. I therefore have high hopes that many of the issues I encountered here will be resolved in the near future.
So would I advocate the use of Uno platform? Well, as any good developer will tell you, "It depends". It certainly has a great many strengths and should be considered a viable alternative to Xamarin but the fact that it's still suffering some growing pains can't be denied. I guess if I were ThoughtWorks, the Uno Platform would feature in the 'Trial' section of my Technology Radar.
7GUIs
The "7GUIs Programming Benchmark" provided a diverse and interesting set of challenges. From the super simple 'Counter' to the implementation of a full spreadsheet it really does cover a lot of common UI/UX patterns.
As a benchmark for evaluating the Uno platform however, I don't think it was a particularly good choice. Uno is targeted at rich visuals using controls which have native interaction patterns across multiple form-factors, something that almost certainly wasn't a consideration for the creators of the 7GUIs.
Declarative Behaviours & FRP
While a significant amount of the detail regarding these aspects of the UI implementation had to be omitted from the descriptions of each task, I trust you are able to see how Behavioural Design can be incorporated into declarative code and how FRP can provide numerous benefits to an implementation.
I would very much encourage you to examine the use of these paradigms in the implementation of the UIs and more broadly from the growing body of code employing these practices. Also, if you get the chance, take some time to learn F#; I almost guarantee that, once you hit that functional "light-bulb" moment, your C# will never be the same again.
Dual-Screen Devices
So how does this all fare for the underlying use case of developing applications for the Surface Neo and Surface Duo? Well, personally I'm very encouraged. The current version of Uno works beautifully with my preferred implementation paradigms and, with just a couple of exceptions, provided an extremely consistent and stable API surface to work with across all its supported platforms.
I am very much looking forward to getting my hands on these devices (nudge, nudge, Microsoft) and spending some serious time looking at what can be achieved with these tools and form-factors. I'm especially interested to try some of the new WinUI features in a cross platform application; I mean, can you imagine some of the beautiful new Acrylic UIs running natively on Android... woah.
Source, Questions & Feedback
All the source code for this article can be found in my SevenGuis Github Repository. Fork it and have a play (and send me a PR if you manage to resolve any of the issues I got stumped by).
I'd also love to hear any questions or feedback you may have about this article. Feel free to get in touch using any of the social links below or from my about page.