Cross-Platform Real-Time Communication with Uno & SignalR

It just works™

Published on 16 July 2020

TL;DR

In this article we will see how to use Uno Platform and SignalR to create applications that run on all major platforms - PC, Mac, Android, iOS and Web - and are capable of receiving real-time updates from a SignalR service. As you will see, these two technologies work incredibly well together, providing an elegant solution to a use-case which, just a few years ago, would have been fiendishly difficult.

All the source code for this post can be found in my UnoChat repository on GitHub.

Intro

A project I'm working on features a web-dashboard. Being a XAML fan, using Uno to create a web-assembly app was a complete a no-brainer... until I realised that I wanted the dashboard to feature real-time updates. Thanks to SignalR (and a host of subsequent technologies) real-time updates is nothing new for traditional web apps but how would these technologies fare when used with Uno/WebAssembly.

Hunting around, I had seen that a couple of people had tried this approach a while back but had hit various stumbling blocks along the way. As far as I could tell, no-one had yet managed to get a working solution which, as you might imagine, was a tad worrying.

Regardless, I knew that Uno and its compilation to WebAssembly (driven by the amazing work undertaken by the Mono team) had progressed massively in the last year so I figured I'd give it a go to see if I could get any further. I was very much prepared for a bit of a slog here, expecting to have to dig into the internals of both Uno and SignalR in order to get it working. But I was not prepared for what actually happened:

It... just... worked.

First time.

With no kludges, no work-arounds, no untoward platform-specific code and no conditional compilation.

It really did Just Work™.

The following post shows how you can use these amazing technologies together to deliver real-time updates to a cross-platform app.

Ingredients

To cook up this little sumptuous little dish, you will need a copy of Visual Studio 2019 with the following installed:

  1. "ASP.NET and Web development" workload
  2. "Azure development" workload
  3. "Universal Windows Platform development" workload
  4. "Mobile development with .NET" workload
  5. ".NET Core cross-platform development" toolset
  6. The Uno Platform Solution Templates.

You will also need a (free) Azure account to which we'll publish our SignalR service so it's easily available to our client platforms.

Objective

We're going to be building a version of the "Get started with ASP.NET Core SignalR" sample app but, instead of an "HTML+js" client, we're going to be using Uno to write an app which can be compiled and run natively across multiple platforms including the web.

When we're finished, we'll have a solution which looks like this:

UnoChat
|- UnoChat.Service
|- UnoChat.Client.Console
|- UnoChat.Client.App
   |- UnoChat.Client.App.Droid
   |- UnoChat.Client.App.iOS
   |- UnoChat.Client.App.macOS
   |- UnoChat.Client.App.UWP
   |- UnoChat.Client.App.Wasm
   |- UnoChat.Client.App.Shared

We'll dive into each of these projects individually below with the occasional "F5" to test our progress.

Right, lets go!

Service, Console Client, Testing & Deployment

UnoChat.Service

We'll first create the SignalR service. As this is covered quite extensively in the sample app we're basing this article on I'm going to shoot through this pretty quickly, only covering notable differences and the code we should end up with.

Ok, start up Visual Studio 2019 and create a new project as shown here:

Create New ASP Net Core Web Application - Step 1 Create New ASP Net Core Web Application - Step 2

Now, follow the steps in the "Create a SignalR hub" section of the sample app to create a ChatHub : Hub class in a Hubs folder within the UnoChat.Service project.

We'll continue with the configuration described in the "Configure SignalR" but we'll also be adding a CORS policy such that we're able to connect to it from a locally hosted Wasm app. The final configuration of the Startup class in the UnoChat.Service project should look like this:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace UnoChat.Service
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddSignalR();

            services.AddCors(o => o.AddPolicy(
                 "CorsPolicy",
                 builder => builder
                    .AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                 )
            );
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseCors("CorsPolicy");

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapHub<Hubs.ChatHub>("/chathub");
            });
        }
    }
}

And that's it. Seriously that's all we need to create a service which is able to provide real-time communication services to a whole host of client applications.... I know, right!

UnoChat.Client.Console

Next we'll create a quick console application to test our SignalR service prior to diving into a cross platform solution with Uno.

BTW, the console app - by virtue of being .NET Core - is also cross-platform and will run on... well... pretty much anything.

So, right click on the "UnoChat" solution and add a new "Console App (.NET Core)" project as shown here:

Create New Net Core Console App

Then, add the Microsoft.AspNetCore.SignalR.Client nuget package to the UnoChat.Client.Console project as shown here:

Install Microsoft Asp Net Core SignalR Client In UnoChat Client Console

Finally, replace the code in Main.cs with the following:

using Microsoft.AspNetCore.SignalR.Client;
using System.Threading.Tasks;

namespace UnoChat.Client.Console
{
    using Console = System.Console;

    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hi! Err... who are you?");

            var name = Console.ReadLine();

            Console.WriteLine($"Ok {name} one second, we're going to connect to the SignalR server...");

            var connection = new HubConnectionBuilder()
                .WithUrl("http://localhost:61877/ChatHub")
                .WithAutomaticReconnect()
                .Build();

            connection.On<string, string>("ReceiveMessage", (user, message) => Console.WriteLine($"{user}: {message}"));

            await connection.StartAsync();

            Console.WriteLine($"Aaaaaand we're connected. Enter a message and hit return to send it to other connected clients...");

            while (true)
            {
                var message = Console.ReadLine();

                await connection.InvokeAsync("SendMessage", name, message);
            }
        }
    }
}

Note: You'll need to ensure the port number in the .WithUrl("http://localhost:61877/ChatHub") line is correct. You can find the port number your UnoChat.Service is set to use by right clicking on the UnoChat.Service project, selecting Properties, navigating to the Debug tab and examining the App URL setting in the Web Server Settings section as shown here: UnoChat Service Debug Settings

Local Testing

Now we can test our service with our client console. Right click on the UnoChat.Service project and select Debug->Start New Instance. After a few seconds compilation a browser window should open and show something similar to this:

UnoChat Service Debug Browser

With that running, go back to Visual Studio and right click on the UnoChat.Client.Console project and again select Debug->Start New Instance. A console window should appear asking who you are. Enter a name, hit return and wait for the app to tell you that it has connected. At this point you can send messages to the UnoChat.Service which should be echoed back to the console window prefixed with your name as shown here:

Just to show we're not just echoing these things locally, right click on the UnoChat.Client.Console project again and start a second instance using Debug->Start New Instance. In this window enter a different name and wait for connection. Now when you send a message, you'll see it in both console windows as shown here:

UnoChat Client Console Two Instances

Pretty neat huh!

Deployment

With our SignalR service running nicely, lets deploy it to Azure by right clicking on the UnoChat.Service project and selecting Publish.... I'm not going to cover this process in too much detail as it's thoroughly documented elsewhere but, if you've not done this before, the following screen shots should help you through:

Publish UnoChat Service To Azure - Step 1 Publish UnoChat Service To Azure - Step 2 Publish UnoChat Service To Azure - Step 3
Publish UnoChat Service To Azure - Step 4 Publish UnoChat Service To Azure - Step 5 Publish UnoChat Service To Azure - Step 6
Publish UnoChat Service To Azure - Step 7 Publish UnoChat Service To Azure - Step 8 Publish UnoChat Service To Azure - Step 9

Still with me? Great.

Lets test our deployed SignalR service by updating the .WithUrl("http://localhost:61877/ChatHub") line in our UnoChat.Client.Console app to match the deployed service as shown in the last screenshot above; for me it's .WithUrl("https://unochatservice20200716114254.azurewebsites.net/ChatHub"). Once done you should be able to start the UnoChat.Client.Console app and send/receive messages to/from your deployed SignalR service.

Now for the magic...

Uno Client

Creating & Preparing An Uno Project

Back in Visual Studio, right click the UnoChat solution and Add->New Project.... Select Cross-Platform App (Uno Platform) from the Add a new project dialog and click Next. Name it UnoChat.Client on the Configure your new project dialog and finally click Create:

Create New Cross Platform App - Step 1 Create New Cross Platform App - Step 2

To help keep my solution organised, I like to group all the Uno "head" projects in a solution folder. This is shown below but don't feel obliged to follow suit:

UnoChat Client Solution Folder

Now, the first thing to do here is to get our dependencies in order using the following steps:

  1. Upgrade all Uno.* packages to the latest non-prelease versions (at the time of writing this is Uno.UI & Uno.UI.RemoteControl to v2.4.4, Uno.Wasm.Bootstrap & Uno.Wasm.Bootstrap.DevServer to v1.3.0)
  2. Install the Microsoft.AspNetCore.SignalR.Client nuget package to all the head projects (UnoChat.Client.Droid, UnoChat.Client.iOS, etc, etc).
  3. Install the MVx.Observable nuget package to all the head projects (UnoChat.Client.Droid, UnoChat.Client.iOS, etc, etc).

Quick tip: Use the Manage NuGet Packages for Solution... option from the solution's right-click menu to get this done much faster than modifying individual projects.

Lastly use the Properties window to change the Root namespace value for the UnoChat.Client.Shared project from UnoChat.Client.Shared to just UnoChat.Client (I'm kind hoping this change makes it into the Uno templates at some point).

MVx.Observable

I like to implement UI/UX flows using behavioural, declarative and functional paradigms. I wrote MVx.Observable to be a "(mostly) unopinionated, light-weight alternative to ReactiveUI provided as a library not a framework" and have written about it extensively here and here.

You don't need to use MVx.Observable to implement the functionality present in this project but I'd encourage you to at least give it a try as, like ReactiveUI, these patterns really can help manage UI state, ensure UX flows are testable and keep discrete behaviours... well, discrete.

MVx.Observable uses System.Reactive to embody it's behaviours in a reactive manner and, as these behaviours interact with the UI, we need to use IScheduler instances to ensure we update the UI from the correct thread. This is somewhat complicated by the fact that we're writing a cross-platform app which uses different IScheduler implementations to marshal updates to the appropriate platform threads. Fortunately, this complexity is easily tamed through the use of Partial Classes and Methods.

In the UnoChat.Client.Shared project, add a Schedulers.cs file with the following content:

using System;
using System.Reactive.Concurrency;
using System.Threading;

namespace UnoChat.Client
{
    public static partial class Schedulers
    {
        static partial void OverrideDispatchScheduler(ref IScheduler scheduler);

        private static readonly Lazy<IScheduler> DispatcherScheduler = new Lazy<IScheduler>(
            () =>
            {
                IScheduler scheduler = null;

                OverrideDispatchScheduler(ref scheduler);

                return scheduler == null
                    ? new SynchronizationContextScheduler(SynchronizationContext.Current)
                    : scheduler;
            }
        );

        public static IScheduler Dispatcher => DispatcherScheduler.Value;

        public static IScheduler Default => Scheduler.Default;
    }
}

Then, in the UWP head, override the OverrideDispatchScheduler method to provide the correct scheduler for the platform by adding a Schedulers.cs file to the head project with the following content:

using System.Reactive.Concurrency;
using Windows.UI.Xaml;

namespace UnoChat.Client
{
    public static partial class Schedulers
    {
        static partial void OverrideDispatchScheduler(ref IScheduler scheduler)
        {
            scheduler = new CoreDispatcherScheduler(Window.Current.Dispatcher);
        }
    }
}

Now we can safely use the scheduler in our solution knowing that we're able to easily marshal operations to and from the UI thread.

ViewModel

Leveraging MVx.Observable we're now going to create a ViewModel to manage all the interaction with SignalR ensuring we don't need have any logic in the view's code-behind.

Create a new ViewModel class in the UnoChat.Client.Shared project containing the following code:

using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows.Input;
using Uno.Extensions;

namespace UnoChat.Client
{
    public class ViewModel : INotifyPropertyChanged
    {
        private readonly MVx.Observable.Property<string> _name;
        private readonly MVx.Observable.Property<HubConnectionState> _state;
        private readonly MVx.Observable.Command _connect;

        private readonly MVx.Observable.Property<string> _lastMessageReceived;
        private readonly MVx.Observable.Property<IEnumerable<string>> _allMessages;
        private readonly MVx.Observable.Property<string> _messageToSend;
        private readonly MVx.Observable.Property<bool> _messageToSendIsEnabled;
        private readonly MVx.Observable.Command _sendMessage;

        private readonly HubConnection _connection;

        public event PropertyChangedEventHandler PropertyChanged;

        private static string DefaultName => typeof(ViewModel)
            .Assembly
            .GetName()
            .Name
            .Split('.')
            .Last();

        public ViewModel()
        {
            _name = new MVx.Observable.Property<string>(DefaultName, nameof(Name), args => PropertyChanged?.Invoke(this, args));
            _state = new MVx.Observable.Property<HubConnectionState>(HubConnectionState.Disconnected, nameof(State), args => PropertyChanged?.Invoke(this, args));
            _connect = new MVx.Observable.Command();
            _lastMessageReceived = new MVx.Observable.Property<string>(nameof(LastMessageReceived), args => PropertyChanged?.Invoke(this, args));
            _allMessages = new MVx.Observable.Property<IEnumerable<string>>(Enumerable.Empty<string>(), nameof(AllMessages), args => PropertyChanged?.Invoke(this, args));
            _messageToSend = new MVx.Observable.Property<string>(nameof(MessageToSend), args => PropertyChanged?.Invoke(this, args));
            _messageToSendIsEnabled = new MVx.Observable.Property<bool>(false, nameof(MessageToSendIsEnabled), args => PropertyChanged?.Invoke(this, args));
            _sendMessage = new MVx.Observable.Command();

            _connection = new HubConnectionBuilder()
                .WithUrl("https://unochatservice20200716114254.azurewebsites.net/ChatHub")
                .WithAutomaticReconnect()
                .Build();
        }

        private IDisposable ShouldEnableConnectWhenNotConnected()
        {
            return _state
                .Select(state => state == HubConnectionState.Disconnected)
                .ObserveOn(Schedulers.Dispatcher)
                .Subscribe(_connect);
        }

        private IDisposable ShouldEnableMessageToSendWhenConnected()
        {
            return _state
                .Select(state => state == HubConnectionState.Connected)
                .Subscribe(_messageToSendIsEnabled);
        }

        private IDisposable ShouldConnectToServiceWhenConnectInvoked()
        {
            return _connect
                .SelectMany(_ => Observable
                    .StartAsync(async () =>
                    {
                        await _connection.StartAsync();
                        return _connection.State;
                    }))
                .ObserveOn(Schedulers.Dispatcher)
                .Subscribe(_state);
        }

        private IDisposable ShouldDisconnectFromServiceWhenDisposed()
        {
            return Disposable.Create(() => _ = _connection.StopAsync());
        }

        private IDisposable ShouldListenForNewMessagesFromTheService()
        {
            return Observable
                .Create<string>(
                    observer =>
                    {
                        Action<string, string> onReceiveMessage =
                            (user, message) => observer.OnNext($"{user}: {message}");

                        return _connection.On("ReceiveMessage", onReceiveMessage);
                    })
                .ObserveOn(Schedulers.Dispatcher)
                .Subscribe(_lastMessageReceived);
        }

        private IDisposable ShouldAddNewMessagesToAllMessages()
        {
            return _lastMessageReceived
                .Where(message => !string.IsNullOrWhiteSpace(message))
                .WithLatestFrom(_allMessages, (message, messages) => messages.Concat(message).ToArray())
                .Subscribe(_allMessages);
        }

        private IDisposable ShouldEnableSendMessageWhenConnectedAndBothNameAndMessageToSendAreNotEmpty()
        {
            return Observable
                .CombineLatest(_state, _name, _messageToSend, (state, name, message) => state == HubConnectionState.Connected && !(string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(message)))
                .Subscribe(_sendMessage);
        }

        private IDisposable ShouldSendMessageToServiceThenClearSentMessage(IObservable<object> messageToSendBoxReturn)
        {
            var namedMessage = Observable
                .CombineLatest(_name, _messageToSend, (name, message) => (Name: name, Message: message));

            return Observable.Merge(_sendMessage, messageToSendBoxReturn)
                .WithLatestFrom(namedMessage, (_, tuple) => tuple)
                .Where(tuple => !string.IsNullOrEmpty(tuple.Message))
                .SelectMany(tuple => Observable
                    .StartAsync(() => _connection.InvokeAsync("SendMessage", tuple.Name, tuple.Message)))
                .Select(_ => string.Empty)
                .ObserveOn(Schedulers.Dispatcher)
                .Subscribe(_messageToSend);
        }

        public IDisposable Activate(IObservable<object> messageToSendBoxReturn)
        {
            return new CompositeDisposable(
                ShouldEnableConnectWhenNotConnected(),
                ShouldEnableMessageToSendWhenConnected(),
                ShouldConnectToServiceWhenConnectInvoked(),
                ShouldDisconnectFromServiceWhenDisposed(),
                ShouldListenForNewMessagesFromTheService(),
                ShouldAddNewMessagesToAllMessages(),
                ShouldEnableSendMessageWhenConnectedAndBothNameAndMessageToSendAreNotEmpty(),
                ShouldSendMessageToServiceThenClearSentMessage(messageToSendBoxReturn)
            );
        }

        public string Name
        {
            get => _name.Get();
            set => _name.Set(value);
        }

        public HubConnectionState State => _state.Get();

        public string LastMessageReceived => _lastMessageReceived.Get();

        public IEnumerable<string> AllMessages => _allMessages.Get();

        public string MessageToSend
        {
            get => _messageToSend.Get();
            set => _messageToSend.Set(value);
        }

        public bool MessageToSendIsEnabled => _messageToSendIsEnabled.Get();

        public ICommand Connect => _connect;

        public ICommand SendMessage => _sendMessage;
    }
}

While this code is fairly lengthy it includes a large number of behaviours such as asynchronous connection management and message handling for SignalR along with enabling and disabling controls based on the current state of the UI and/or connection. All these behaviours are separated into discrete methods allowing them to be easily modified, supplemented or removed by simply changing, adding or removing an appropriated named "ShouldXXXX" method.

View

With the ViewModel in place and taking care of all the fundamental logic for the application, we now need to use it from the view. In the code behind for MainView.cs add the following code:

using System;
using System.Reactive.Linq;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace UnoChat.Client
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private readonly ViewModel _viewModel;
        private IDisposable _behaviours;

        public MainPage()
        {
            this.InitializeComponent();

            _viewModel = new ViewModel();
            DataContext = _viewModel;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            var messageToSendReturn = Observable
                .FromEvent<KeyEventHandler, KeyRoutedEventArgs>(
                    handler => (s, k) => handler(k),
                    handler => MessageToSendTextBox.KeyUp += handler,
                    handler => MessageToSendTextBox.KeyUp -= handler)
                .Where(k => k.Key == Windows.System.VirtualKey.Enter);

            _behaviours = _viewModel.Activate(messageToSendReturn);
        }

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            base.OnNavigatingFrom(e);

            if (_behaviours != null)
            {
                _behaviours.Dispose();
                _behaviours = null;
            }
        }
    }
}

In this code we instantiate the ViewModel, set it as the View's DataContext and call it's Activate method when the user navigates to this view. Note how we pass an observable to the Activate method which will emit a value when the user hits return on the MessageToSendTextBox. This allows us to receive feedback from the UI without compromising View/ViewModel segregation.

The Activate method returns an IDisposable which, when disposed, will tear down all the associated behaviours, unsubscribe from events and release resources. Accordingly, we dispose of this IDisposable when the user navigates away from this view, thereby correctly managing the lifetime of the ViewModel's resources.

Finally, lets implement our UI by editing the MainView.xaml to the following:

<Page
    x:Class="UnoChat.Client.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UnoChat.Client"
    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}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="Name:" Style="{StaticResource BaseTextBlockStyle}" Grid.Row="0" Grid.Column="0" Margin="4" VerticalAlignment="Center" />
        <TextBox Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="0" Grid.Column="1" Margin="4"/>
        <Button Command="{Binding Path=Connect}" Content="Connect" Grid.Row="0" Grid.Column="2" Margin="4" Padding="16,4" HorizontalAlignment="Stretch"/>
        <ItemsControl ItemsSource="{Binding Path=AllMessages}" Grid.Row="1" Grid.ColumnSpan="3" Margin="4" />
        <TextBlock Text="Message:" Style="{StaticResource BaseTextBlockStyle}" Grid.Row="2" Grid.Column="0" Margin="4" VerticalAlignment="Center"/>
        <TextBox x:Name="MessageToSendTextBox" Text="{Binding Path=MessageToSend, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding Path=MessageToSendIsEnabled}" Grid.Row="2" Grid.Column="1" Margin="4"/>
        <Button Command="{Binding Path=SendMessage}" Content="Send" Grid.Row="2" Grid.Column="2" Margin="4" Padding="16,4" HorizontalAlignment="Stretch"/>
    </Grid>
</Page>

And that's that. Let's give it a go!

Testing

Set the UnoChat.Client.UWP project as the "Startup Project" and hit F5. After a short compilation cycle you should see the following:

UnoChat Client UWP Running - 1

Click "Connect" and, after a short pause you should see the "Message" textbox become enabled. Enter some text in the "Message" textbox and click the "Send" button (or hit enter) and the message should be sent to SignalR. SignalR will then publish this message to all connected clients which, given we are one of the connected clients, will result in the message being sent back to us and being displayed in ItemsControl in the middle of the window as shown below:

UnoChat Client UWP Running - 2

But having one client on one platform is no fun!

Lets kick off the Android head project by right clicking on it and selection Debug->Start New Instance. After another short compilation an Android Emulator should started and our app should be deployed to it then run. Clicking "Connect" then (after connection is complete) entering a message in the "Message" text box and hitting "Send" should result in the following:

UnoChat Running On Uwp And Android

Nice, two platforms for the price on one!

Now (and you'll need to be paired to a Mac for this), right click the iOS head project and select Debug->Start New Instance. Once the iOS device emulator has started, follow the same steps as with with other head projects and you'll see the following:

UnoChat Running On Uwp, Android and iOS

That's three for three.

Now, right click on the WASM head and select Debug->Start New Instance. After a short compilation you'll see a browser window appear and.... get stuck at the splash screen.

Booo! So close...

Getting WASM Linked

Opening the browser's "Developer Tools" with F12 we see this on the Console:

UnoChat Wasm Link Issues

Well, we've seen these "A suitable constructor ... could not be found" exceptions before. They're due to the assemblies containing the associated types being omitted by the Mono linker. By looking up which assemblies the various types belong to we're able to explicitly instruct the linker to include the assemblies by modifying the LinkerConfig.xml file in the WASM head project.

After a few "start -> fail -> find type -> amend config" iterations I ended up with the following in my LinkerConfig.xaml file:

<linker>
  <assembly fullname="UnoChat.Client.Wasm" />
  <assembly fullname="Uno.UI" />

  <assembly fullname="Microsoft.AspnetCore.Http.Connections.Client"/>
  <assembly fullname="Microsoft.Extensions.Options"/>
  <assembly fullname="Microsoft.AspNetCore.SignalR.Client"/>
  <assembly fullname="Microsoft.AspNetCore.SignalR.Client.Core"/>
  <assembly fullname="Microsoft.AspNetCore.SignalR.Protocols.Json"/>

  <assembly fullname="System.Core">
	<!-- This is required by JSon.NET and any expression.Compile caller -->
	  <type fullname="System.Linq.Expressions*" />
  </assembly>
</linker>

And, with this in place starting the WASM head resulted in:

UnoChat Wasm Running

ooOOoo... exciting!! Could it be??

Yes, yes it could.

Here we have UnoChat.Client.Console (Red Leader), UnoChat.Client.UWP (Red 3), UnoChat.Client.Wasm (Red 6), UnoChat.Client.Droid (Red 5) and UnoChat.Client.iOS (Red Buttons) all connected to the SignalR service and all receiving real-time updates.

Nice.

Conclusion

And there we go. In about an hour (hey I stopped for lunch) we have an app which is capable of receiving real-time updates and which runs on pretty much every OS - either natively or through the browser. Moreover, the code to deliver this somewhat epic feat is short, concise, maintainable and - most importantly - 99% shared amongst the various project heads.

The Uno platform really has matured amazingly well since I first blogged about it as part of last December's Third Annual C# Advent. Back then I found that it worked... mostly... but not all the head projects functioned correctly and it required a whole host of kludges to get them all running from a shared codebase. Just over seven months later and the change is incredible: You now have an expectation of things working "out-of-the-box" and any minor difference/issue on a given platform to be an easy work around.

I can't wait to see what the Uno Platform has in store for us at UnoConf 2020 (personally I'm hoping for Uno on Linux and - most importantly - Raspberry Pis). Hope to see you all there on August 12th!

Lastly...

If you're interested in using the Uno Platform to deliver cross-platform apps or have an upcoming project for which you'd like evaluate Uno Platform's fit, then please feel free to drop me a line to discuss your project/ideas using any of the links below or from my about page. As a freelance software developer and remote contractor I'm always interested in hearing from potential new clients or about potential new collaborations.