wolfgang ziegler


„make stuff and blog about it“

.NET MAUI, Neural Networks and Observability

November 29, 2022

TL;DR:

  • I ported an old Windows Phone (yes!) app of mine to .NET MAUI.
  • This app makes use of a custom neural network implementation to perform simple OCR.
  • Finally, we will add observability/tracing to the app.

A few years back, I was quite the Windows Phone Fanboy, wrote a couple of apps for it and even developed my own MVVM framework "Newport" for that purpose.

With the demise of Windows Phone I also lost interest in app development for the most part and focussed on other things. However, when I recently saw a .NET Conf talk about MAUI, my interest got rekindled. I was mostly curious, how much of my "old" Windows Phone knowledge would still be useful when working with MAUI. As it turned out, many of the of the concepts that I had acquired over the years working with XAML, WPF, Silverlight, Windows Phone and UWP would still apply to MAUI. But I'm getting ahead of myself here. Here's how I ported SimpleOcr (a showcase Windows Phone app) to MAUI.

The Neural Network Code

The neural network was actually the easiest part of this whole project. It's a small proof-of-concept neural network implementation (named DotNeuralNet) that I had written for an article in a .NET magazine a few years back. Admittedly, it's a really naive implementation that cannot compete by any means with modern machine learning frameworks. However, it was meant to explain basic concepts and does simple jobs quite well. I had already ported DotNeuralNet to .NET Core a while ago and published a recent version to NuGet. So, it was basically read to be used in my soon-to-be MAUI app.

Porting the Windows Phone App to MAUI

The above mentioned Windows Phone app SimpleOcr is still up on GitHub. My plan was to reuse as much of its code as possible.

This is what the application looked like on Windows Phone (its emulator, to be correct).

A screenshot of the SimpleOcr app running in the Windows Phone emulator

The UI was/is quite basic:

  • In Training mode:
    • You could select a digit from the combo box.
    • Then "write" this digit a couple of times in the grid below, confirming it with the OK button.
    • Having done this for all digits, you would switch to Live mode. This triggered the actual training phase of the neural network.
  • In Live mode:
    • You could then write a digit on the grid.
    • When pressing OK the neural network would respond with the digit it had recognized by selecting it in the combo box.

Back then I was also quite obsessed with keeping my apps pure MVVM (I always aimed for 0 "code-behind" in the [Page|View].xaml.cs files). For that reason I had created my "personal" MVVM framework Newport of which SimpleOcr also made quite some use.

As a consequence, in addition to porting SimpleOcr to MAUI, I also had to port the relevant parts of Newport.

Step 1 - Create a boiler-plate MAUI app

I used Visual Studio for Mac and created my very first MAUI app - creatively named SimpleOcr2.

Create a new .MET MAUI app with Visual Studio for Mac

Everything looked quite familiar. There was App.xaml, MainPage.xaml - so far so good.

Step 2 - Structuring the Project

I kept the same project structure that I had used in the original SimpleOcr app, having a ViewModels folder where all the application logic would go into. The view (MainPage.xaml) would only interact with those ViewModels via Binding declarations.

Instead of referencing the Newport Nuget package which was not MAUI-ready at all (not even .NET-Core ready for that matter), I created a library project Newport where I would copy all the required classes in.

Visual Studio project structure

Step 3 - Making it Build 😆

Now I only had to get this to compile again. To my great surprise, the effort was really not that big. Admittedly, it is a really simple application. However, the fact that XAML and its data-binding concepts are so similar between MAUI and Windows Phone/UWP made this quite pleasant.

Here are some notable changes/differences I encountered:

  • Starting with the positive things - some controls that I had to custom-implement in the past were readily available:
    • In Newport I had created ProgressSpinner to indicate a longer-running operation (e.g. the training phase of the neural network). MAUI has ActivityIndicator already built-in.
    • For displaying the data-bound grid cells, I had used the custom UniformGrid control. In MAUI, I could simply use GridItemsLayout for that.
  • DependencyProperty is called BindableProperty in MAUI.
  • Input handling is quite different 😬. MAUI makes use of GestureRecognizers for all kinds of interaction (e.g. drag and drop, pinch, swipe, tap) instead of providing lower-level events.
    • ⚠️ I ended up using the TapGestureRecognizer for detecting "writing" digits on the grid. However, this needs another iteration since it really means tapping a single grid cell each time. Definitely not user-friendly at the moment, but it was enough to proof my point.

Apart from some smaller issues (renamed namespaces and properties) this was really all and suddenly, I had a running, working iPhone app 🥳.

If you compare the screenshot to the original one above, I'm quite happy with result.

A screenshot of the SimpleOcr2 app running in the iPhone emulator

Adding Observability

One thing that also surprised me was how fast the training phase of the neural network was on these newer devices (or their emulators). I remember that with SimpleOcr back on Windows Phone this would take quite some time.

So this is probably something that could still vary today from device to device and would be interesting to gather telemetry data about.

So let's do that as a final step and add some telemetry to this app.

My original plan was using OpenTelemetry .NET for this task hoping that I could get it to run in a MAUI. I had managed to do so with a Blazor app a while ago by adding a few tweaks. However, with this MAUI app I encountered a few nasty assembly dependency issues that I could not figure out in reasonable time.

OpenTelemetry .NET causing an exception due to missing dependencies

Enter: Elastic APM

Well, I would not work for an observability company if I didn't have a proper solution for that problem as well. Instead of using OpenTelemetry .NET, I simply added the NuGet package for the Elastic APM .NET agent.

Adding the Elastic APM .NET Agent NuGet package

Then, with one additional line of code in the ViewModel's Train method ...

private async void Train()
{
    if (_trainingRows.Count > 0)
    {
        await Agent.Tracer.CaptureTransaction("Training", "Custom", async t =>
        {
            using (BusyScope())
            {
                t.SetLabel("nr_of_training_rows", _trainingRows.Count);
                await Task.Factory.StartNew(async () =>
                {
                    var trainer = new BackPropagationTrainer(_network);
                    trainer.Train(_trainingRows, 0.5, 50);
                    await IsolatedStorageHelper.Save("weights.xml", _network.GetWeights());
                });
            }
        });
    }
}

... we already get interesting insights (duration of the training for a given number of training rows) in Elastic APM server.

Elastic APM server showing the SimpleOcr2 transaction

Wrapping it up

Given that this was my first actual hands-on experience with .NET MAUI, I felt "at home" immediately. I could reuse so much knowledge and concepts that I had gathered from working with Windows Phone, WPF, UPW that the learning curve was really flat for me.

In fact, this little experiment has rekindled my interest in (.NET-based) app development and I might look into this again over the course of the next year (getting an app into the iOS app store would actually make for a great new year's resolution 😆).

The code for this app can be found on GitHub at z1c0/SimpleOcr2.

Happy Holidays 🎄🎅

For everyone who made it that far in this blog post I added a little extra. Since you probably came here from dotnet.christmas I decided to give this app a little Xmas touch.

For Newport, I had written ParticleControl, a small particle system that could be used for various effects, like e.g. falling snowflakes. So I also ported this custom control to MAUI and now the SimpleOcr2 app can "let it snow ❄️ ❄️ ❄️".

The SimpleOcr2 app with the added snowfall effect

Happy Holidays and Merry Christmas 🎄🎅!