Intro
This is part 4 of my series on using the Uno Platform to write COduo, a highly graphical cross-platform app, able to target both single and dual-screen devices. In this post I show how COduo uses the TwoPaneView to provide a single, adaptive UI which functions across multiple form-factors, screens and orientations. I then detail how to set up an Uno Platform solution such that you're able to use (one of the myriad implementations of) the TwoPaneView in your apps.
For an introduction to COduo or to find further posts in this series, please use the links below:
- Part 1 - Background
- Part 2 - Infrastructure
- Part 3 - Client Architecture
- Part 4 - Using the TwoPaneView
- Part 5 - Implementing the interactive UK Map
- Part 6 - Charts on the Uno Platform
- Part 7 - Windows, Win10X and releasing to the Microsoft Store
- Part 8 - Android and releasing to the Google Play Store
- Part 9 - iOS and releasing to the Apple App Store
The TwoPaneView
Windows Dev Center describes the TwoPaneView as:
a layout control that helps you manage the display of apps that have 2 distinct areas of content, like a master/detail view.
While it works on all Windows devices, the TwoPaneView control is designed to help you take full advantage of dual-screen devices automatically, with no special coding needed. On a dual-screen device, the two-pane view ensures that the user interface (UI) is split cleanly when it spans the gap between screens, so that your content is presented on either side of the gap.
As outlined above, the central tenet of the TwoPaneView is that, by separating the UI of your app into two parts, your app can automatically capitalize on the additional screen real-estate offered by dual-screen devices. While splitting a UI into 2 distinct areas may seem odd, Microsoft offer several examples of how this can be achieved in their "Introduction to dual-screen devices" article, a summary of which can be seen in the image below:
What Microsoft do not make clear though, is how good this approach is for providing a reactive UI on single screen devices. By splitting your UI in this way, it can be composed into a variety of layouts to automatically fit the myriad different screen resolutions and aspect ratios provided by devices ranging from PC's and tablets to mobile phones and IoT devices (and the various orientations thereof). For example, below I show common screen sizes, layouts and orientations which are natively catered for by the TwoPaneView:
Note that the two panes do not need to be the same size and scroll-bars are introduced if either of the panes causes the layout to exceed the screen bounds.
Now this kind of reactive UI is nothing new but historically it would have had to be handled manually; usually (in the XAML world) through the use of Visual States and Adaptive Triggers. But with the TwoPaneView this is all taken care of for you while providing the added benefit of also allowing these panes to intelligently span across screens. Pretty neat huh.
Microsoft provide a fairly comprehensive guide to using the TwoPaneView here but there are numerous additional tips for using the control - particularly on multiple screens - that could easily warrant an entire blog post. Here though I would like to refocus on how you can start using the control in an Uno Project which, unfortunately, isn't as straight forward as it ought to be.
Three Implementations of Two Panes
At the time of writing, there are three implementations of the TwoPaneView control:
- The Windows 10 SDK version, released as part of the v10.0.18362.0 SDK
- The WinUI version released as part of the WinUI 2.1 nuget package
- The Uno version released as part of the Uno.UI 2.1 nuget package
Getting an Uno Platform solution to correctly use the desired implementations has been the cause of more than a little confusion (not least of which from me), so here I will cover the various combinations that allow you to use the TwoPaneView in a cross-platform code-base.
Uno + Windows 10 SDK
If your UWP head project is targeting platform 1903 or later, then the easiest way to use the TwoPaneView is to mix the Uno.UI and Windows 10 SDK implementations of the control. To do this, first ensure:
- That all head projects except UWP have Uno.UI version 2.1 or later installed
- The UWP head project is targeting platform 1903
With these pre-requisites, the following XAML will compile and run successfully across all heads:
<Page
x:Class="UnoWithWinUI.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UnoWithWinUI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TwoPaneView Pane1Length="0.3*" Pane2Length="0.7*" Background="Yellow" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWideModeWidth="100">
<TwoPaneView.Pane1>
<Border>
<Rectangle Fill="LightBlue" />
</Border>
</TwoPaneView.Pane1>
<TwoPaneView.Pane2>
<Border>
<Rectangle Fill="LightGreen"/>
</Border>
</TwoPaneView.Pane2>
</TwoPaneView>
</Grid>
</Page>
Uno + WinUI
WinUI is "The Future of Windows Development" and, accordingly, the Uno platform has committed to "put WinUI on every platform possible". As such, if you're looking to start a new cross-platform project, you should probably be looking to use controls from the WinUI package (not the Windows 10 SDK) where possible.
Unfortunately, this isn't as simple as one might hope. Uno currently only implements a small subset of the controls available in WinUI and, as the namespaces between these controls are different, you will need to limit yourself to only using controls from WinUI that have also been implemented in Uno if you want to maintain a single code-base for your cross-platform project (at the time of writing Uno.UI has implemented just the NumberBox and the TwoPaneView controls).
The following steps describe how to get an Uno solution setup such that you can correctly use a WinUI control - in this instance the TwoPaneView - without resorting to head project specific views:
- Ensure that all head projects except UWP have Uno.UI version 2.1 or later installed
- Install the WinUI nuget package (version 2.1 or later) into the UWP head project
- Add the required WinUI XAML resources to
App.xaml
in the Shared project as shown here:<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>
- Add the
xmlns:winui="using:Microsoft.UI.Xaml.Controls"
namespace to the XAML page in which you wish to use the TwoPaneView control. - Add the TwoPaneView to the XAML page.
<Page
x:Class="UnoWithWinUI.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UnoWithWinUI"
xmlns:winui="using:Microsoft.UI.Xaml.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<winui:TwoPaneView Pane1Length="0.3*" Pane2Length="0.7*" Background="Yellow" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWideModeWidth="100">
<winui:TwoPaneView.Pane1>
<Border>
<Rectangle Fill="LightBlue" />
</Border>
</winui:TwoPaneView.Pane1>
<winui:TwoPaneView.Pane2>
<Border>
<Rectangle Fill="LightGreen"/>
</Border>
</winui:TwoPaneView.Pane2>
</winui:TwoPaneView>
</Grid>
</Page>
At this point you should all project heads should compile and run successfully. If all goes well, you should see something akin to the following on each platform:
Two pains with the TwoPaneView
While developing COduo I found that the TwoPaneView exhibited two curious behaviours that I had not expected. Firstly, the control would continue to use proportional sizing of the panes even when the control was being used across multiple screens and, secondly, it wrapped each pane's content in a scroll viewer which made it difficult to correctly design an "adaptive" UI.
I spent an age trying to work out why the control was behaving this way and potential methods to get it to work the way I expected. Finally I ended up writing a custom control which "just worked" and moved on with trying to deliver some more functional aspects of the app.
Sometime later, while discussing this issue with the Uno Platform team, I decided to recreate the issues I had experienced in a new solution. Yet, when I came to demonstrate the issues - this time on the Windows 10X Emulator - the TwoPaneView worked perfectly. Looking at the associated code I confirmed that it had not changed yet I was no longer seeing either of the behaviours I had previously experienced... until I tried running the project back on the Surface Duo emulator.
Bingo.
It turned out that, while the WinUI implementation of the TwoPaneView worked exactly as I had originally expected, the Uno recreation of the control didn't exhibit the same behaviour. I created an issue in the Uno Platform github repository and will revert to using the TwoPaneView when they - or I - have time to resolve the issue.
Using the TwoPaneView in COduo
COduo uses the TwoPaneView in the "root" view. This root view is displayed in the UWP Window's Frame
and never changes. To support navigation and layout changes COduo employs a Reactive State Machine which dictates the content that should be displayed within each pane of the TwoPaneView. This is done by reacting to mode changes in the TwoPaneView (i.e. SinglePane, Tall, Wide) and emitting Layout.Changed
events, all communicated between the view and state machine via the Event.Bus
. These events are received by the Root.ViewModel
which coordinates updating the TwoPaneView control in the Root.View
by directly setting the content of each panel.
To illustrate this here is the code from the Home.State
which reacts to layout changes:
var viewModel = _viewModelFactory.Create<IViewModel>();
var layouts = Observable
.Merge(
_eventBus.GetEvent<Event.LayoutModeResponse>().Select(@event => @event.Mode),
_eventBus.GetEvent<Event.LayoutModeChanged>().Select(@event => @event.Mode))
.ObserveOn(_schedulers.Dispatcher)
.Select(mode => AsLayout(viewModel, mode))
.Select(AsEvent)
.Subscribe(_eventBus.Publish);
And the code from the Root.ViewModel
which applies the layout:
return _eventBus.GetEvent<Event.LayoutChanged>()
.WithLatestFrom(_view, (@event, view) => (@event.Layout, View: view))
.Where(tuple => tuple.View != null)
.ObserveOn(_schedulers.Dispatcher)
.Subscribe(tuple => tuple.View.PerformLayout(tuple.Layout));
And the code from the Root.View
which updates the TwoPaneView (currently my custom DualPaneView
control due to the issues described above):
public void PerformLayout(Layout layout)
{
dualPaneView.Pane1 = layout.Pane1Content as UIElement;
dualPaneView.Pane2 = layout.Pane2Content as UIElement;
}
Part 5
In Part 5 I will outline how I implemented the interactive map of the UK. I believe the approaches used for this control leverage some of the incredible power of UWP - and the Uno Platform - to "build modern, seamless UIs that feel natural to use on every Windows device."
Finally
I hope you enjoy this series and that it goes some way to demonstrating the massive potential presented by the Uno Platform for delivering cross-platform experiences without having to invest in additional staff training nor bifurcating your development efforts.
If you or your company are interested in building apps that can leverage the dual screen capabilities of new devices such as the Surface Duo and Surface Neo, or are keen to understand how a single code-base can deliver apps to every platform from mobile phones to web sites, then please feel free to drop me a line using any of the links below or from my about page. I am actively seeking new clients in this space and would be happy to discuss any ideas you have or projects you're planning.