- 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.
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).
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.
Everything looked quite familiar. There was
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
ViewModels folder where all the application logic would go into. The view (
would only interact with those ViewModels via
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.
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
ProgressSpinnerto indicate a longer-running operation (e.g. the training phase of the neural network). MAUI has
- For displaying the data-bound grid cells, I had used the custom
UniformGridcontrol. In MAUI, I could simply use
- In Newport I had created
- Input handling is quite different 😬. MAUI makes use of
GestureRecognizersfor all kinds of interaction (e.g. drag and drop, pinch, swipe, tap) instead of providing lower-level events.
- ⚠️ I ended up using the
TapGestureRecognizerfor 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.
- ⚠️ I ended up using the
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.
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.
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.
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 =>
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.
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 ❄️ ❄️ ❄️".
Happy Holidays and Merry Christmas 🎄🎅!