Summary
The article shows how to create a cross-platform Uno template app that consists of a basic NavigationView and basic ViewModel navigation capabilities, and can run on Windows, Android, iOS, and as a website (via WASM). It uses the following frameworks: Uno Platform, ReactiveUI, and Microsoft Extensions for hosting, dependency injection, and for logging.
GitHub code
The Code is located under the UnoRx repo on GitHub, so just clone the code from GitHub to start an Uno project with RxUI and Microsoft Extensions DI and logging.
Be Reactive
What’s System.Reactive (Rx)?
The System.Reactive (Rx hereinafter) library, is a set of extensions that offer LINQ-like extensions methods on the IObservable<T> type.
An IObservable<T> is very similar to IEnumerable<T>. The major noticeable difference between the two is that while the IEnumerable<T> is pull-based, and the consuming part is the one making its move iterating and retrieving the data and the source listens, IObservable<T> is the opposite – the consumer listens to the source, and the source is the one notifying all listeners about a new item that is available. This makes subscribing to events, or any other unexpected data sources, even infinite ones very easy.
Like LINQ, Rx offers a wide set of operators that can filter (e.g. Where), transform (e.g. Select), and perform many more manipulations on your data.
Due to their difference in functionality, IObservable<T> and IEnumerable<T> also differ in their interface structure. Similar to IEnumerable<T>, that classes implementing it expose the GetEnumerator method, classes implementing the IObservable<T> interface expose the Subscribe method. The difference is that the Subscribe method takes an IObserver<T>. An IObserver<T> is the mechanism that has the capability to subscribe to push based data. Because we don’t iterate over an IObservable<T> and the elements don’t necessarily come in a continuous manner, its ‘iteration’ lifecycle is meant to be long, sometimes as long as of the app’s. For this reason, the IObservable<T> can also notify the IObserver<T> with additional two notification types: error, and completion.
Here’s what the IObserver<T> interface looks like:
public interface IObserver<in T> { void OnNext(T value); void OnError(Exception error); void OnCompleted(); }
Don’t worry, the Rx library provides you with numerous tools that do the job for you creating IObservable<T>s, so that you don’t ever have to implement these interfaces by hand. One of the most widely used toolbox in the Rx ecosystem is the Observable class, which is where the LINQ to Observable extensions stuff is going on (LINQ to Objects Enumerable’s counterpart), so it would be a good idea that you become familiar with its methods. It’s important to emphasize, that like LINQ to objects that doesn’t get materialized until you call ToList, ToArray or any of the aggregate functions (Sum etc.), the observable pipeline doesn’t execute until you subscribe to it. In Rx, you can use one of the Subscribe overloads to finalize the subscription.
Functional thinking
As object oriented and MVVM users, the way we think about our problem becomes the method we’re going to achieve it programmatically, e.g. planning how we’ll make INPC properties, what other properties they’re going to notify, what commands’ CanExchange status they should notify etc. Besides impacting our thinking, this also causes our VM code to be scattered all over the file, and each problem area can sometimes span several properties, commands and methods. As things get more complicated, it gets harder to keep track of which properties notify, whom are they notifying, as well as which properties are dependent, and whom are they dependent on.
One of the traits of functional programming, is to address each problem in a compositional and concentrated way, so that each problem area can be configured in a single pipeline, composed with extension methods added on top of each other fluently. I highly recommend the book “Rx.NET in Action” by Tamir Dresher, that will teach you how to think reactively and will guide you through the power of Rx.NET. Another great resource to learn about Rx is the
ReactiveUI
The ReactiveUI (RxUI hereinafter) is an MVVM framework (and more), based on the Rx library, that provides you with tools that make it easier for you to maintain your UI project in a Reactive way.
RxUI is not affiliated with Rx, but is rather based on it, and thus the two shouldn’t be confused with each other. After having read the book Rx.NET, reading “You, I, and ReactiveUI” by Kent Boogaart will help you leverage your knowledge about Rx and take it a step further taking advantage of the power ReactiveUI provides.
Dependency Injection (DI)
What’s DI?
In order to make an app portable and testable, it’s good practice to separate the app in various layers (i.e. View, Model, and ViewModel), so we can target and test each layer separately.
Naturally, every layer in the app, may depend on external frameworks or services, such as access to data from a web-service, database, or local filesystem; use of external APIs to calculate stuff, or many other scenarios.
To keep the dependent layer portable, testable, and maintainable, we want the initialization and management of all external services off the layer consuming them, and have them maintained by an external central service-container in the app that injects the services into the layer whenever required.
Even the initialization and maintenance of classes inside the current layer, are all managed by the DI framework and provided by it.
Microsoft DI
Since we’re Microsoft fans and we love to make use of anything out the .NET box, we’ll make use of the Microsoft.Extensions.* packages, which provides various services, such as app bootstrapping, logging, as well as dependency injection and others.
The way Microsoft DI works is that at the app’s initialization stage, you register all services that could be required later, and for all the consuming classes, you add the required services as constructor parameters. So, for instance if you have a ViewModel (VM from now on) that depends on a service IIdentityService, you first register the service with the container, and have the VM’s constructor requires a parameter of type IIdentityService.
You also need to register the VM itself with the container, for the container to recognize it and be able to provide it.
When you’re requesting the VM from the container, it initializes it by providing all required constructor parameters.
You can register services with different lifetimes, for example register a service as singleton, so that the same service is only initialized once in the app and reused through the entire app’s lifetime, or as transient – each request results in a newly created instance for the matter.
Let’s code
Prerequisites
- Visual Studio 2019
- Uno Platform template extension
Project creation
After installing the Uno extension, create a Uno Platform App project. Right click the solution, select “Manage NuGet packages for solution”, and install the following packages:
- ReactiveUI.Uno
- ReactiveUI.Events
- ReactiveUI.Fody
- Microsoft.Extensions.Hosting
- Microsoft.Extensions.Logging.Console
- Microsoft.Extensions.Logging.Filter
- Splat.Microsoft.Extensions.DependencyInjection
- Splat.Microsoft.Extensions.Logging
Additionally, update the following packages:
- Uno.UI – (update to latest prerelease version, make sure “Include prerelease” is checked) – All heads except UWP
- Uno.UniversalImageLoader – Droid head only
- Uno.Wasm.Bootstrap – WASM head only In order for Rx to properly work in the WASM project, and due to this issue, a component in the Rx runtime needs to be manually enabled.
To do this, replace Program.Main method in the WASM project with the following:
static int Main(string[] args) { #pragma warning disable CS0618 // Type or member is obsolete PlatformEnlightenmentProvider.Current.EnableWasm(); #pragma warning restore CS0618 // Type or member is obsolete FeatureConfiguration.UIElement.AssignDOMXamlName = true; Windows.UI.Xaml.Application.Start(_ => _app = new App()); return 0; }
Generic host and service registration
For our app bootstrapping we’re going to use Microsoft.Extensions.Hosting Generic Host.
One of the steps of setting up the host is configuring its DI services. This is the stage where we tell the DI engine that when we want a certain service, it should return the specified type or even a specific instance.
Some parts of the Microsoft Extensions API are relatively new and doesn’t function well in all platforms, thus, they were removed.
Here’s what the code looks like:
/// <summary> /// Provides application-specific behavior to supplement /// the default Application class. /// </summary> sealed partial class App : Application { public IHost AppHost { get; } /// <summary> /// Initializes the singleton application object. /// </summary> public App() { AppHost = Host .CreateDefaultBuilder() .ConfigureAppConfiguration((hostingContext, config) => { //some features don't yet work well in all platforms config.Properties.Clear(); config.Sources.Clear(); hostingContext.Properties.Clear(); }) //here’s where we set up all services we’ll be consuming in our app .ConfigureServices(services => { //RxUI uses Splat as its default DI engine //Microsoft DI support has been integrated into Splat (by me ) //So we can instruct it to use Microsoft DI instead //with the UseMicrosoftDependencyResolver extensions services.UseMicrosoftDependencyResolver(); var resolver = Locator.CurrentMutable; resolver.InitializeSplat(); resolver.InitializeReactiveUI(); var allTypes = Assembly .GetExecutingAssembly() .DefinedTypes .Where(t => !t.IsAbstract); // register view models { services.AddSingleton<NavigationViewModel>(); services.AddSingleton<IScreen>(sp => sp.GetRequiredService<NavigationViewModel>()); //scan and register assembly // for all implementations of RoutableViewModel var vmTypes = allTypes.Where(t => typeof(RoutableViewModel).IsAssignableFrom(t)); foreach (var rvm in vmTypes) services.AddTransient(rvm); } // register views { var vf = typeof(IViewFor<>); bool isGenericIViewFor(Type ii) => ii.IsGenericType && ii.GetGenericTypeDefinition() == vf; var viewTypes = allTypes .Where(t => t.ImplementedInterfaces.Any(isGenericIViewFor)); foreach (var viewType in viewTypes) { var serviceType = viewType.ImplementedInterfaces.Single(isGenericIViewFor); services.AddTransient(serviceType, viewType); } } }) .ConfigureLogging(loggingBuilder => { // remove loggers incompatible with UWP { var eventLoggers = loggingBuilder .Services .Where(l => l.ImplementationType == typeof(EventLogLoggerProvider)) .ToList(); foreach (var el in eventLoggers) loggingBuilder.Services.Remove(el); } loggingBuilder.AddSplat(); #if !__WASM__ loggingBuilder.AddConsole(); #else loggingBuilder.ClearProviders(); #endif #if DEBUG loggingBuilder.SetMinimumLevel(LogLevel.Debug); #else loggingBuilder.SetMinimumLevel(LogLevel.Information); #endif }) .Build(); //Unlike Splat, MS DI’s engine uses two separate parts //for service registration and service consumption, //here’s where we provide Splat with the service provider built by MS DI AppHost.Services.UseMicrosoftDependencyResolver(); this.InitializeComponent(); this.Suspending += OnSuspending; } }
ViewModels
Assuming you have basic knowledge of how MVVM and property notification (INotifyPropertyChanged – INPC) works, the central property-notification object in RxUI is the ReactiveObject. RxUI even offers auto INPC injection via the ReactiveAttribute attribute, which when decorated on a property, instructs the compiler to weave INPC implementation in the property, so you can just write:
public class MyModelBase : ReactiveObject { [Reactive] public string SearchTerm { get; set; } }
for INPC to be automatically implemented.
The ReactiveObject class not only implements and facilitates INPC, but also offers some very powerful extension methods, via the IReactiveObject interface. One of the most popular ex. methods, is the WhenAnyValue method, that gets called whenever a property or multiple properties get changed, and returns an observable pipeline from that. You can the transform the property change item, its value, or have it combined from a few properties. Get yourself familiar with it by reading this article.
Views and navigation
In order to tell RxUI, which views belong to which VMs, views should implement the IViewFor<T> interface, where T refers to the VM type it is designed for. Our app navigation is entirely managed in the VM side, that means that you don’t navigate to pages or views, but to VMs, and it’s for the RxUI (via the IViewLocator) to resolve the matching view, based on its IViewFor<T> implementation.
The two main navigation types in RxUI, is the IScreen which is the app’s ‘shell’, and the various IRoutableViewModel VMs, which with the aid of the registered IScreen, provide navigation between VMs.
Navigating to a different VM can be achieved using the Router property of the app’s IScreen, which can be accessed from all VMs via the HostScreen property (implementation of IRoutableViewModel).
The Router property returns a value of type RoutingState, which offers various navigation command properties, as well as observables you can subscribe to, that emit notifications whenever routing is changing (about to change), changed, and others.
To make this work in the UI, we’re going to use the NavigationView control to display the shell, and use the RoutedViewHost as the placeholder for the pages we’re navigating to. The latter automatically knows what view to resolve and display based on the VM it was navigated to.
Guest blog post by:
Shimmy Weitzhandler, an Uno fan and contributor.