Modernising and cross-platforming a .NET Windows desktop app - Part 3
- 10 minutes read - 2075 wordsIn this blog I conclude my journey of modernising and cross-platforming a .NET Framework Windows desktop application.
- Part 1: Why you might need to modernise and cross-platform an existing desktop application and how you can do the simplest (but wrong) thing.
- Part 2: Using my guinea-pig application, create a safety net of tests around the code before refactoring (where necessary) and moving from .NET Framework to .NET (Core).
- Part 3 (this post): Continuing the journey, I’ll replace the WinForms part of my guinea-pig application with a true cross-platform UI framework.
In part 1 I looked at why you might need to do this in the first place and how doing the simplest thing works (kind of), but probably isn’t setting you up for success in the longer term. In part 2 I created a safety net of tests and then improved, refactored and upgraded my code to .NET Core.
In this blog I will investigate cross-platform alternatives to using WinForms and tackle any other Windows-only code that
my application might be harbouring. The end result will be my application targeting net8
, using the latest language features,
with consistent code style, good test coverage running locally and in a build pipeline, refactored and improved and able to
run across Windows, macOS and Linux.
What can I use instead of WinForms?
Well, it depends 😄. Seriously though, there are lots of options for C# developers looking for a cross-platform UI framework. The problem is deciding which route to choose based on your application, it’s target audience, what 3rd party packages it relies on and any number of other requirements/restrictions/limitations that might influence your decision.
There are a number of different approaches that cross-platform UI frameworks (that are available to C# developers) take, and they have their pros and cons. Let’s take a look…
Full custom rendering
As implied, these frameworks take over full control of rendering their controls, instead of wrapping and using controls that are part of the OS. As a result, applications built with this type of framework look exactly the same across all supported OSs, but will not blend seamlessly with native OS controls and will not inherit OS-based features, such as OS theming.
Avalonia
Avalonia is a free, open-source, XAML-based framework. It was inspired by WPF, although it is not a strict clone.
- Platforms: Windows, macOS, Linux, iOS, Android, WebAssembly
- Native look and feel:Native look and feel: No
- 3rd party control libraries: Some, but it includes a rich set of built-in controls
One interesting off-shoot is Avalonia XPF, that can run WPF applications on macOS and Linux with very little change. It supports 3rd party WPF control libraries as well so would offer a low-friction way for a WPF application to run on macOS and Linux. It is a commercial product and aimed at enterprises.
Unity / MonoGame
Unity and MonoGame are both frameworks aimed a game development.
- Platforms: Windows, macOS, Linux, iOS, Android, game consoles
- Native look and feel: No
- 3rd party control libraries: Some
Being aimed at game development, these frameworks offer a very different look and feel than a “normal” desktop application.
Part native control mapping, part emulation
Frameworks of this type use native control mapping on one OS and emulate (or mimic) controls on other OSs. So they have a native look and feel on one OS but something that only approaches it elsewhere. The emulation is just that, and inevitably will diverge from native and will not blend and integrate fully with the OS.
QtSharp
QtSharp is a C# wrapper around the Qt framework.
- Platforms: Windows, macOS, Linux, iOS, Android, WebAssembly, embedded
- Native look and feel: Windows, macOS
- 3rd party control libraries: Some, but it includes a rich set of built-in controls
QtSharp has not seen active development in several years and appears to have been abandoned. An alternative might be Qml.Net which offers a C# binding for the related QML framework.
GTK#
GTK# is a C# wrapper around the GTK toolkit.
- Platforms: Windows, macOS, Linux
- Native look and feel: Linux
- 3rd party control libraries: Some
GTK# is not as actively developed or maintained as it once was, and it is not listed as a maintained language binding in the GTK docs.
Full native control mapping
Frameworks of this type use native controls on all the OSs they support. They have a “front-end” abstraction layer and different “back-ends” for each target OS. This means your single codebase runs on the targeted OSs and your application blends seamlessly with the OS and can use OS features (e.g. system tray, theming). Conversely, this also means that your application will look and behave differently on different OSs. You can end up with an application that has the lowest-common-denominator feature-set, and you might still need to write platform-specific code.
Xamarin / MAUI
Xamarin is a framework that was initially designed for mobile applications, but evolved to support desktop applications as well. It is the older framework, but more mature than MAUI, it’s successor.
- Platforms: Windows, iOS, Android (as well as macOS, Blazor for MAUI)
- Native look and feel: Yes
- 3rd party control libraries: Lots for Xamarin, some for MAUI
There are several things to note here:
- Support for Xamarin ended in May 2024
- Microsoft promotes an upgrade path from Xamarin to MAUI
- Whilst it is born out of Xamarin, MAUI is still relatively new
- Neither Xamarin nor MAUI support Linux
Eto.Forms
Eto.Forms is a framework that supports creating desktop applications that work across Windows Forms, WPF, MonoMac, and GTK#. iOS and Android ports are work in progress and incomplete.
- Platforms: Windows, macOS, Linux (mobile is incomplete)
- Native look and feel: Yes
- 3rd party control libraries: Not many
Eto is relatively new (compared to other above) and does not seem to have widespread use.
Uno Platform
Uno Platform is an even newer framework that is a reimplementation of UWP. It uses UWP XAML and WinUI API for it’s “front-end” abstraction layer and then different “backend” renderers for each OS.
- Platforms: Windows, macOS, Linux, iOS, Android, Blazor
- Native look and feel: Mostly, but not when targeting Skia
- 3rd party control libraries: Yes, including commercial
Although relatively new, development does appear to be very active, and use is growing. Not all parts of WinUI are implemented on all platforms. When targeting Skia, native controls are not used so this is more like emulation, as we have discussed above.
What to consider when choosing a framework?
There are many factors to consider when choosing any framework to use in your application. The list below is neither exhaustive nor in priority order, the individual circumstances of your application and it’s context will dictate that, but it is a starting point:
- How active is the development of the framework?
- How easy is it to use the framework?
- How popular is the framework?
- Is there an active developer community? How large is that community?
- What are the support options? Community and/or paid-for?
- How good is the documentation?
- What is the current skill-set of your development team? How would they adapt to using the new framework?
- What platforms does your application really need to target?
- What 3rd party packages does your application use? Will they work with the new framework? If not, what work is involved in fixing that?
- Is there a licensing cost? If so, what are the terms?
- What business pressures are in play?
- Does the adoption of a new framework block time-sensitive features from being released?
- And, I’m sure, many more…
What did I choose and why?
For my Time In Words application, these questions are much more straight-forward. First of all, I can rule out the following quite quickly:
- QtSharp and GTK#: not actively developed and/or abandoned; small communities;
- Xamarin and MAUI: support has ended for Xamarin; MAUI is quite new; mobile platforms are not a priority; but crucially, neither support Linux and I want my application to run on a Raspberry Pi.
- Eto.Forms: does not seem to have widespread use; small community;
So short-listed are:
Unity/MonoGame: Time In Words is not your normal line of business desktop application (with toolbars, menus, etc), it is actually more akin to a game. So, these could be viewed as an option. However, both of these framework have a steep learning curve and my time is limited.
Uno Platform: Although relatively new, it does seem quite polished and has good documentation. Definitely an option, but it does seem quite complicated overall when compared to Avalonia.
Avalonia: Mature, well documented and enjoys more widespread use, even with large commercial enterprises. The UI of Time In Words is “different” anyway, so I’m not concerned about having a truly native look-and-feel.
In the end, I choose Avalonia, but Uno was a close second.
How hard was it to replace WinForms?
For Time In Words, it was quite straight-forward, although my application is only small. To get started I installed the
Avalonia templates with dotnet new install Avalonia.Templates
and then followed the
test drive and other documentation
to get myself a bit more acquainted with Avalonia.
With that under my belt I dived in and created a new project to sit side-by-side with the WinForms project and started pulling over code and converting to Avalonia as appropriate. Things of note:
- Avalonia heavily promotes using the MVVM pattern. This is
fine but as Time In Words is so small, it doesn’t really warrant the complexity and I had already refactored it to use the MVP pattern. - The tests I put in place as part of the MVP refactor changed very little, which is nice, and proves the abstractions were appropriate.
- I spent most of my time finding the Avalonia equivalents for WinForms controls and APIs.
- Avalonia supports creating unit tests for your Avalonia-based code and provides a headless platform
and extensions for XUnit and NUnit. At the most basic level, for XUnit, you use
[AvaloniaFact]
instead of[Fact]
to decorate your tests which sets up a UI thread. For Time In Words this means I was able to use TDD to drive the porting of my views from WinForms to Avalonia. This also means I ended up with better test coverage, as the WinForms
views did not have any tests.
What about other Windows-only code?
Now, .NET (Core) still includes a bunch of frameworks on top of the Base Class Library (BCL) and it doesn’t attempt to fully abstract away the underlying OS. As such, you could still call an API and get an exception, depending on which OS your code is running. The good news is that the .NET 5+ SDKs include the platform compatibility analyzer as a set of code analyzers. Essentially you will get compiler warnings if you are using APIs that are not supported on the platform that your project is targeting. The link above also discusses several ways you can handle these situations.
If your application uses any native Windows APIs or other code that is not supported on all of your target platforms then you will need to find those and work out what to do with them. You can still call native code using P/Invoke provided you reference a native library that is available on all of your target platforms. You can also use preprocessor directives to compile and/or call platform-specific code.
Lastly, if you are targeting more than just desktop devices, Avalonia also has some documentation on how they recommend you handle targeting multiple platforms from a single solution and their templates includes one that sets up such a cross-platform solution. There is also further guidance if you need to call platform-specific code.
Conclusion
This blog series has covered quite a lot:
- Why you should modernise your Windows desktop application.
- Why you should cross-platform your Windows desktop application.
- Mono might be the easy choice, but it probably won’t work for you long-term.
- Creating a safety net of tests for existing functionality using the Golden Master technique.
- Using your IDE to enforce code style and code standards.
- Separating presentation from business logic using the MVP pattern.
- Different approaches and frameworks for building a cross-platform UI.
- Replacing WinForms with Avalonia.
- What to do with other Windows-only code.
Below you can see the finished app running on my multiscreen Windows 11 machine and my Raspberry Pi (Linux) powered word clock “appliance”:
I hope you’ve enjoyed following along the journey with my hobby project and that you can use some of the information and techniques I’ve described in your own projects.
You can browse the Time In Words repo here.