Avalonia and .NET Core Migration Notes

Avalonia is on the verge of releasing their 0.9 update (it’s up to it’s fourth preview). The .NET Core system released 3.0 was released last month. I’ve been working on non-Avalonia related projects for the past several months but when last I left all my tutorials and projects they were running on Avalonia 0.8 and .NET Core 2.1. I’m looking forward to the near term release as well as having some upcoming projects that I think could be suited for Avalonia. In preparation I went updated all of my tutorial code repositories in a side-branch waiting for the day 0.9 hits prime time. Through that process I learned about some of the small and not so small (but all good I think) changes people may encounter migrating versions.

(As I side note, this is why it is a good idea to always be forward migrating code as libraries and runtimes jump versions. It’s easier to tweak from one release to another rather than two, three, or more.)

Odds and Ends

I’ll start off with some of the easy things first. First, in .NET Core 3.0 there is a new OutputType when you define your C# Projects. Previously for these executables you’d set it to Exe. You can still do that but now there is also a WinExe setting. What does that get you? On Mac and Linux absolutely nothing. On Windows however it gets rid of an annoying .NET Core application behavior. Windowed applications with an output type of Exe would launch a shell window and then your application. By setting it to WinExe it makes the behavior consistent across all three platforms: no extraneous shell windows are created.

A second small thing is that on the FileSystemDialog controls the old property InitialDirectory is now just called Directory. Essentially all the dialog boxes have collapsed their variously named directory keyword with Directory. There are a few little changes like that in Avalonia that has accumulated over time (34 by my count) but the good news is that for each one if your old code uses it you will get a warning at compile time saying it is obsolete and the recommended replacement, just like in this case.

A third small thing has to do with classes you use for design-time data. This is data that is used by the IDE to render your views with appropriate data, such as in this tutorial article on the topic. To make this work properly the class now needs to be a public class. Prior to that the default scoping was fine. Now we move on to the other changes get a little more involved but still aren’t dramatic but are good to highlight.

Explicit ReactiveCommand Property Typing

First up on the larger changes is the definition of Reactive command properties. These are the “movers” of our system which define behavior when you click a button or something else happens by building them out using ReactiveUI. My code for defining and initializing a ReactiveCommand has looked something like this:

public ReactiveCommand Ok { get; }

...

public AddItemViewModel()
{
  Ok = ReactiveCommand.Create(
    () => new TodoItem {Description = Description},
    okEnabled
  );
}

This definition of the property worked however now it throws a compile time “static types cannot be used as return types” error. The solution is simple. We have to be more explicit and define the types of the input and return using generics ReactiveCommand<T_in, T_out>. The code examples I had seen generally tended to do that anyway. It also makes the code more explicit about what is going on so it’s probably a less sloppy practice anyway. What this looks like in practice is simply defining the two generic types for the ReactiveCommand property: one for the input type and one for the output type. In this particular case it takes no argument but returns a TodoItem so the new property definition is simply:

public ReactiveCommand<Unit, TodoItem> Ok { get; }

One which takes no inputs and returns no outputs would simply be:

public ReactiveCommand<Unit, Unit> Cancel { get; }

Essentially Unit is the stand in for no input or no output.

Application Lifecycle Management

The last and biggest change, if you choose to use it, are the new Application Lifetimes capabilities that are being introduced in Avalonia with the 0.9 release. The long story short is that while Avalonia is primarily targeting the desktop there is some work being done to target other platforms like mobile and WebAssembly. Certain things that one can expect from starting up a desktop lifecycle cannot be expected for these other platforms. With a view forward they are addressing that by creating this concept of Application Lifetimes so that a single code base can properly handle the application lifecycles of their respective platforms. In terms of bootstrapping behavior one can do it the old way or not, and as far as I know this will remain the case for the foreseeable future. However in terms of interacting with things like the MainWindow within other code you will be required to make some code changes. Let’s look at both but start with comparing the old and new application bootstrapping behavior.

Below is the standard (and still supported) way of initializing the application as part of the main program code:

class Program
{
  public static void Main(string[] args) => BuildAvaloniaApp().Start(AppMain, args);

  // Avalonia configuration, don't remove; also used by visual designer.
  public static AppBuilder BuildAvaloniaApp()
    => AppBuilder.Configure<App>()
      .UsePlatformDetect()
      .UseReactiveUI();

  // Your application's entry point. Here you can initialize your MVVM framework, DI
  // container, etc.
  private static void AppMain(Application app, string[] args)
  {
    var window = new MainWindow
    {
      DataContext = new MainWindowViewModel(),
    };

    app.Run(window);
  }
}

If you create a new template MVVM application using dotnet new this is pretty much exactly what you get. We have an app builder object that has a start method that can take a reference to an initialization method and our program’s input arguments. This is where we create our main window object, do all our dependency injection, etc. In the new lifecycle management this is going to move to out of the Program.cs and into an event handler on our App object. The initialization of our components will be the same but we will now have the flexibility to support multiple deployment platform constructs. What this looks like in practice is a main program class that looks like this:

class Program
{
  public static void Main(string[] args) => BuildAvaloniaApp() 
      .StartWithClassicDesktopLifetime(args);

  // Avalonia configuration, don't remove; also used by visual designer.
  public static AppBuilder BuildAvaloniaApp() 
    => AppBuilder.Configure<App>()
      .UseReactiveUI()
      .UsePlatformDetect();
}

That looks a lot simpler doesn’t it? The first thing that stands out is the initialization code is obviously missing because it has been moved. The second thing is that rather than calling Start on our app builder we are telling it what lifecycle to start as. In this case we are starting with the classic desktop lifetime. By bringing in other life cycle libraries our app can choose other options, as documented in the GitHub link above. The initialization code is now in our App.xaml.cs file. Whereas previously that code looked like this:

public class App : Application
{
  public override void Initialize()
  {
    AvaloniaXamlLoader.Load(this);
  }
}

We will now add the below event handler code to that class event handler for handling the OnFrameworkInitializationCompleted:

public override void OnFrameworkInitializationCompleted()
{
  var window = new MainWindow
  {
    DataContext = new MainWindowViewModel(),
  };

  if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
    desktop.MainWindow = window;
  else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
    singleView.MainView = new MainView();
  base.OnFrameworkInitializationCompleted();
}

You can see we still are initializing our MainWindow object in the exact same way as we did previously. Now we just have to determining what we want to do for specific application lifetimes. Right now it is either going to be either a traditional desktop or a single view style application. We are checking the type of ApplicationLifetime passed into our application and initializing the values accordingly. You will note that to support single view mode we will actually have to factor out the view from the window otherwise that code makes no sense. If you intend to use this, like I do, for my main bootstrapping methodology then I’ll instead probably use something like a switch statement and having the default behavior to throw a NotSupportedException exception for unsupported types.

As I mentioned in the beginning while the bootstrapping behavior can stay the original way or move to the new way, the refactoring of the actual application class into lifecycle specific behaviors will have ramifications elsewhere if you are used to directly working with Windows and things like that. For example in Avalonia 0.8 if I wanted to get a reference to the application’s main window from my code I could simply reference it like this:

var mainWindow = Application.Current.MainWindow;

Those methods are now on the current application’s Lifetime but only if it is a desktop style application. A single view app would instead have a MainView that is referenced. Code-wise the accessing of these references would look like this for a desktop:

var desktop = Application.Current.ApplicationLifetime 
      as IClassicDesktopStyleApplicationLifetime;
var mainWindow = desktop.MainWindow;

…and to get the equivalent main view for a single view app would look like this:

var sva = Application.Current.ApplicationLifetime 
      as ISingleViewApplicationLifetime;
var mainView = sva.MainView;

While the old bootstrapping idiom still works and there is no indication it is going away I like the idea of consistently using the same bootstrapping code in desktop and other application types so my personal choice will be to write my new apps using the new Lifecycle System.

Next Steps

So what are the next steps? I have a repository with the changes made to my example code ready for the ultimate 0.9 release. Once Avalonia 0.9 is out I’ll change the references to the production release rather than preview code and then merge that back to master. I’ll try to go through my previous tutorials and update the code snippets in them as well so that it corresponds to 0.9 as well and make a note of that as well. I have some applications that I’m starting to work on which I’m now going to use Avalonia 0.9 preview builds for and I’ve already converted to .NET 3.x for my work prior to all of this. Ultimately there are some interesting things I’m looking forward to doing with Avalonia so am excited to be playing with it again.



Picture of Me (Hank)

Categories

Updates (124)
Journal (115)
Software Engineering (96)
Daily Updates (84)
Commentary (66)
Methodology (57)

Archive

2019
2018
2017
2016
2015
2014
2013