Thursday 21 March 2024

Avalonia UI Test Drive

As the title implies, this blog post is about my experience test driving Avalonia UI.

So why am I doing this?

I've been getting growing questions lately about whether MapGuide Maestro works on Linux via Mono

Sadly I no longer emphasize anything regarding Mono compatibility because.

  • WinForms support (in legacy .net Framework 4.x) on Mono is pretty much a hack.
  • And since we've moved away from legacy .net Framework to .net 6.0, the combination of WinForms + .net 6.0 is probably an unsupported combination (you don't use Mono to run .net core/5.0+ applications, you use Microsoft's official SDK/runtime). I'm also not too keen to waste time and resources to test and find out.
The best and practical approach for a multi-platform MapGuide Maestro would be to at a minimum to rewrite the UI in a library/framework where support on non-Windows platforms is a first-class citizen.

And in terms of .net multi-platform UI frameworks, there's only one logical choice: Avalonia UI

While it is not in my immediate plans to rewrite MapGuide Maestro's UI in Avalonia, I wanted to at least explore the feasibility of building such a UI even if all the actual functionality is mocked up, just to see how easy or difficult the whole process is.

Hence the motivation for this post.

So why Avalonia?

Avalonia is effectively the "spiritual successor" to WPF, and adopts most of the same patterns and practices when building UIs for Avalonia.

I had first heard about Avalonia when it was formerly known as Perspex and at the time, from the screenshots of example Perspex applications on Windows and non-Windows platforms, it was clear at that point in time the range of possible applications one can build with Perspex was quite limited and building an application like Maestro on top of Perspex was not feasible.

Just recently, I had heard about the framework now known as Avalonia again and this time round there was a lot more positive buzz around it, so I gave it another look and was much more impressed at its capabilities and richer suite of UI controls to build applications with.

Our objective 

My objective with this Avalonia learning exercise was to build a minimal multi-document interface skeleton application, mimicking the primary functionality (UI-wise) of MapGuide Maestro.

  • Being able to present a login UI to connect to a MapGuide Server
  • Present MapGuide Server resources in a tree view
  • Open resources in a region of tabbed editor panels and being able to close them.
This is a crude wireframe of the kind of UI I wanted to build.
If this kind of UI looks familiar, yes it's basically the UI for tools like:
MapGuide Maestro is an IDE-like application (in terms of UI), so my first instinct is to look for a Visual Studio like docking library for Avalonia. However, I've been using Visual Studio Code more often than full Visual Studio as my daily driver for coding and the VSCode UI has become my default UI that I want to replicate for any IDE-like application with a multi-document interface.

The benefit of such a UI, is that it doesn't require an explicit VS-style window docking control like I have for the current WinForms-based MapGuide Maestro. I can already see the controls needed to build such a UI

Any UI toolkit worth its salt should to able to provide these basic controls.

This is a proof-of-concept, so we won't be using the existing Maestro API to talk to actual MapGuide Servers. Instead such functionality will be mocked up for this application. The main purpose of this exercise is to see if Avalonia provides enough of the base UI elements to build a hypothetical version of MapGuide Maestro on top of.

It turns out this exercise was fraught with several challenges

Challenge #1: Getting familiar with Avalonia concepts

The first challenge was simply getting familiar with Avalonia concepts. Being someone who did .net desktop app development primarily in Windows Forms, I skipped over WPF, and its XAML-based variants/successors like Silverlight, Xamarin Forms, UWP, and MAUI, and Avalonia being a "spiritual successor" to WPF meant that I didn't have a potential conceptual head start on Avalonia that one might have if they already had experience with WPF and its XAML-based derivatives.

However, I was familiar with the MVVM pattern and data-binding, which is used heavily in Avalonia. I already had experience in knockout.js building/maintaining some of our web apps in my day job, and these concepts learned from knockout.js mostly translate cleanly 1:1 to Avalonia.

The other Avalonia concepts I'll have to figure out as we go along.

Challenge #2: WebAssembly (WASM) support

Before I begin, I must take the MapGuide-knowledgable readers of this post back in time, way back to when MapGuide Open Source 1.0 was released.

Do you remember how this new (at the time), re-invented version of MapGuide was released, but the only authoring option at the time was to buy a license of Autodesk MapGuide Studio? Not a pretty look if you are offering a free and open source web map GIS server, but the authoring tools are not?

Some members of the MapGuide community were aware of this glaring discrepancy and created a web-based equivalent of MapGuide Studio called Web Studio built fully with HTML/JS/CSS. Unfortunately, Web Studio was really bare-bones in terms of authoring capabilities and the code being written in the pre-historic era of javascript (this was circa 2006-2007) which did not make it conductive to external contribution. React, TypeScript and friends weren't around then, so trying to enhance Web Studio with new features was extremely challenging. I once tried to add at least a generic XML text editor fallback for Web Studio so you had something to edit various resources where Web Studio did not provide a dedicated editor UI for, but alas this was just too challenging for me and I gave up on such an idea.

But eventually another user of the MapGuide community solved this problem more directly by developing and releasing a v1.0 of a windows .net desktop application that you all know as MapGuide Maestro. As an aside, I am not the original author of MapGuide Maestro in case you're wondering, I merely took over development and maintenance of Maestro from the 2.1 release onwards.

Anyways, back to the topic at hand, when I created the new Avalonia application with the provided project template, I was most surprised to see that the generated solution came with a project that compiled to WebAssembly (WASM) that was ready to run in your web browser!

This was a mind-blowing revelation for me from a conceptual standpoint. It means our hypothetical MapGuide Maestro built on Avalonia could not only exist as a regular desktop application, but the WASM build of this application could be dropped in the the wwwroot of a MapGuide Web tier installation and you would now have a modern version of Web Studio, but much more powerful and capable, because it is just MapGuide Maestro ... now in your web browser!

Since our starter project template includes a functioning WASM browser target. I now had a strong incentive to keep this target active and working, because the prospect of being able to run MapGuide Maestro in a web browser is a very tempting proposition. Therefore, the choice of libraries and APIs I use is constrained by my new requirement of being able to work in a browser/WASM environment.

For example, I originally wanted to use MVVMDialogs to simplify working with dialogs (Maestro has lots of dialogs, so I figure such a library could be useful), but I couldn't get this library to work in a WASM environment with some of my testing dialogs, so this was a no-go. Since this was just a proof-of-concept, there wasn't a need to have working dialog system, but it does mean if this were to go beyond a proof-of-concept and into an actual application where we will inevitably have to present a dialog of some sort, I'd have to come with a paradigm that can work in both desktop and browsers.

Another problem with this WASM target is that I can't seem to debug it in Visual Studio. You can launch the WASM target in the debugger and spawn a new browser window to launch your app, but any breakpoints you stick in your C# code are not being hit. I'm not sure if this a shortcoming or a broken feature, but it is somewhat concerning if we were to go full steam ahead with support WASM as a compilation/deployment target.

Challenge #3: "Large scale" MVVM

Although I already knew the MVVM pattern from knockout.js, my scope of usage was mostly limited to using knockout.js to building "islands" of interactive client-side content on primarily server-generated web pages. So I didn't really have an idea of how to apply such a pattern on a full blown Single Page Application (SPA), which is pretty much what we're trying to achieve (conceptually) in Avalonia. By the time I was building SPAs proper, I had moved on the popular stuff like React, which is how I gained the knowledge needed to build a modern replacement map viewer for MapGuide and my usage of knockout.js fell by the wayside as a result, so I never figured out the answer for how to do large scale MVVM.

The main problem was that in a large scale MVVM, how do view models communicate with each other without a tight parent-child coupling?

I deduced that for starters, we definitely need to use dependency injection. Various view models will need to access different services and if we had a root view model with explicit nested child view models (each with their own service requirements), it would be an absolute pain to have to setup these various view models. Using a DI container means we can offload this concern to it and we can focus on just asking the DI container for a particular view model and it will setup all the required services for it for us provided we register everything properly with the DI container.

For better WASM support, I wanted a dependency injection container that is not driven by reflection to make the code more friendlier to app trimming. We want to be able to app trim on publish so that we can eliminate unused code and reduce the final binary size. This is most desirable for the WASM target as app trimming means smaller binaries, which means smaller payloads to download in a web browser. StrongInject was chosen for this reason as it was a "compile-time" DI container that can verify all your dependencies are registered properly before running the application through the magic of source generators, generating all the necessary registration and validation code for you.

Finally, to be able to communicate between view models without necessary parent-child coupling, the messenger facility of the MVVM toolkit can be used. With this messenger facility, we simply:
  • Have select view models be recipients of certain messages
  • Have our application services (that various view model commands call) send these messages.
  • Relevant recipients get notified and update themselves (and their bound UIs) accordingly as a result.
I don't know if this is the proper usage of MVVM pattern in a "large scale" application, but it makes sense internally in my mind.

Challenge #4: TabControl binding of heterogenous/polymorphic content

Our proof-of-concept app has a main tab region where tabs of various document content are shown. I figured out easily that their TabControl component is supposed to be data-bound to an observable collection of view models, but what really stumped me was how bind this control to an observable collection of polymorphic editor view models?

The reason is because the tab content we want to show is not homogenous. One open editor tab could be for a layer, one for a map, one for a feature source, etc, etc. So we need to be able to show different tabs on the same tab control. 

Avalonia documentation is pretty scant on this topic. All examples I found assume homogenous series of tab content, which is not our case.

Just for laughs, since AI has been hyped for soon taking away everyone's jobs (even us devs), I figured I'd ask huggingface chat (as a guest), how would you solve this problem?

Unfortunately, the provided code sample does not work out of the box. It clearly assumed Avalonia = WPF and gave me a WPF-based solution. The giveaway was the Avalonia TabControl does not have an ItemTemplateSelector, but the WPF TabControl does.

But Avalonia sharing many conceptual similarities with WPF meant that although the answer provided wasn't correct, parts of the answer were applicable and did lead me down to further avenues of inquiry and eventually I found the solution: It was to define a data template for every possible derived tab view model class in the same UserControl where the TabControl was specified.

Final challenge: Avalonia VS designer

This wasn't so much a challenge, rather an annoyance. The Avalonia designer in Visual Studio has some teething issues
  • Intellisense/autocomplete is somewhat flaky when writing binding expressions and when you're doing a lot of data-binding, having the editor giving you and incomplete or outdated list of properties you can bind to becomes annoying. A full project build generally fixes this, but it is annoying having to do this every time I add new observable properties or commands to an existing view model class.
  • It's also not rename-aware, so observable property or command renames will result in stale binding expressions, causing havoc with the designer and have to be manually fixed up in the XAML. Choose your observable property names wisely I suppose, because renaming them afterwards is painful.

The end result and overall thoughts

A week later from that post, after addressing and/or navigating around these various challenges, we have a functional skeleton application!

And if these screenshots don't convince you, thanks to being able to deploy as a WASM target, I deployed a copy to GitHub pages, so you can see this app for yourself right in your WASM-enabled web browser!

The source code for this app can be found here. One day this may form the basis of a new (true) multi-platform version of MapGuide Maestro, but for now this lies as a potentially useful starting point for building a tabbed-multi-document editor application with Avalonia.

So what did I think of Avalonia from this little experiment? 
  • I like it mostly. My pre-existing experience on knockout.js helped greatly with picking up MVVM and data-binding. My initial prediction of a VSCode-style UI layout being buildable turned out to be true.
  • I like the default (project template provided) choice of MVVM Toolkit for applying the MVVM pattern. I like their heavy use of source generators to make adding new observable properties and commands to a view model being a simple case of tacking [ObservableProperty] on a field or tacking [RelayCommand] to a private method and having the source generator generate all of the boilerplate code for you (and it's a lot of boilerplate!).
  • The revelation that Avalonia has a WASM deployment target was both exciting and "cramping my style". It meant that certain libraries I wanted to use (eg. The MVVM dialogs library) could not be used and it wasn't clear what would work in a WASM browser environment and what wouldn't. Which leads to ...
  • Documentation is lacking in some areas. What really stumped me for a while was how do a TabControl bound to a collection of polymorphic or heterogenous tab view models. Their provided examples completely failed to tell me how to do this. I suppose if I came into this with existing WPF experience, this wouldn't have been so difficult as most of the concepts and patterns seem to be mostly transferable, but I happened to have skipped WPF and its bajillion XAML-based derivatives, so I didn't have this pre-existing knowledge to fall back on. Through perseverance and looking at the source code for many existing Avalonia applications on GitHub, I was finally able to determine that data templates was the solution.
So all-in-all, this was a fun and useful exercise and you get a useful starting point app from this effort!

Now I better get back onto this MapGuide/FDO work.

No comments: