Single file minimum F# Avalonia UI app with notes.

ยท

5 min read

After some experimentation, a minimum Avalonia app that can be ran from an .fsx file or a Polyglot Notebook.

The Polyglot Notebook can be found at:

https://github.com/Fxplorer/Fxplorer_www/blob/main/docs/MinimumFsharpAvaloniaApp.ipynb

The .fsx script can be found at:

https://fxplorer.github.io/Fxplorer_www/MinimumFsharpAvaloniaApp.fsx

Including the #if/#endif block will allow loaders to only run when in an F# interactive session, a .fsx script file or a polyglot notebook session. My future ideas are to build docs into the compiled source (.fs) files so this would allow those .fs files to be converted to .fsi or notebooks.

if INTERACTIVE

#r "nuget: Avalonia"
#r "nuget: Avalonia.Desktop"
#r "nuget: Avalonia.Themes.Simple"

#endif

open Avalonia
open Avalonia.Controls

Avalonia uses the concept of a TopLevel


AI COMMENTARY:

TopLevel represents the base class for Avalonia UI containers that require independent window management capabilities like Windows, Dialogs or hosted controls.

It encapsulates logic to properly display, hide, position top-level visual roots.


The Window is going the be the TopLevel in a desktop app. There are a number of properties, which Title and Content are included.

The Content of a Window allows only 1 value. It does not have a Children property. When a control has only one slot to put something in, it will have the Content property. If it allows containing multiple controls, it will have a Children property that is a list. A control like StackPanel has the Children property that you can put multiple controls in. The Content of this window could be

Window(Title = "Hello World App", Content = ( new Stackpanel() ))

where the Content is equal to 1 control, but THAT control has many more controls in it.

let view1 () =
    //Avalonia.Controls.Window
    Window(Title = "Hello World App",          Content = "Hello World from Avalonia F#!")
           //Avalonia.Controls.Window.Title    Avalonia.Controls.ContentControl.Content

My first attempt at this script failed because I was doing let view1 = and was getting System.InvalidOperationException: Unable to locate 'Avalonia.Platform.IWindowingPlatform'. Changing it to a function works. The timing of building these controls is important.

Teechnically there is a Avalonia.Themes.Default.DefaultTheme that is built in. However, in my experience, it does not help. A new window will be transparent and you really are unable to use it. So a theme HAS to be applied to be practical. So far, the only way to get it into the mix is to create a type based on the Application. The SimpleTheme is what I have used for my testing. The themes are seperate nuget packages, so you have to grab it and open it.

Avalonia App

Avalonia has actually been around for more then 10 years. As what happens with long lived projects, there is a lot of information that unavailable (404s and the like) or just plain wrong because of progression of the code base. Version 11 also bought some pretty large structure changes and new capabilities and that has been fun to naviagte. In addition, there is not a clear presentation of how Avalonia actually works. Like from a conceptual view or even a high technical view that is helpful.

After some research and digging and some AI conversations about Avalonia I am starting to gain some understanding. The ceremony involved and some of the values needed have really confused me. What I discovered is that Avalonia is based on the .NET Generic Host This model is still not explained rather well but I did find some materials that helped.

Building a Console App with .NET Generic Host ๐Ÿ‘
Understanding .NET Generic Host Model
Quick Introduction To Generic Host in .NET Applications

Reading through these turned on some light bulbs because I started to associate what Avalonia was doing with things like "lifetimes" and the appBuilder stuff. So once the HOST app is ready and the descriptions of the UI have been feed in, Avalonia creates 'pipelines' or instruction sets describing the intended screen or something like that and then feeds those instruction into Skia (SkiaSharp) that uses the GPU if available and shows the pixels on the sceen back in the window. PDF's can be extracted, which my current understanding is that is actually because of Skia and come from it. PNG and Bitmap can also be generated I think. That will be in future research experiments.

Lifetimes

Again, the lifetimes are a reflection of the .Net Host basis. The avalonia lifetimes are based on if the code is running in a desktop enviroment or on a phone or in wasm, etc. Xploring more into those things will come in the future. So after the OnFrameworkInitializationCompleted the running app needs to know how it is running. That is the match. This section will get expanded in future versions of this script. I added the printfn just to see where that showed up, if anywhere and to tell me I got to that point when running dotnet fsi script.fsx.

type App() = 
    inherit Application()  //Avalonia.Application

    override this.Initialize() =
        this.Styles.Add ( Avalonia.Themes.Simple.SimpleTheme() )

    override this.OnFrameworkInitializationCompleted() =

        match this.ApplicationLifetime with
        | :? Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime as desktop ->
            desktop.MainWindow <- view1()
            printfn "Avalonia app running..."
        | _ -> ()

Running the app!

The App type is just the blueprint. Injecting the view1 there is not where I want to do it as I want to have a base that I can run different experiments on. I will rework this script to work on that soon.

The following binding will actually start the app (and show you a window!) when it hits the StartWithClassicDesktopLifetime line. Yes, it requires the empty string array. Many Avalonia apps will have that line not in the Configure portion. I was trying to do minimal, so I put it there. Future versions will probably do that differntly, as I have been discoving there are a few ways to actually get a running app and that very well depends on what you are trying to do.

let app = 
    AppBuilder.Configure<App>()
        .UsePlatformDetect()
        .StartWithClassicDesktopLifetime([||])

Once you have the window, when you printfn, the output will show in the output of the block in a notebook or show up where you did the start command like in the console where you did dotnet fsi script.fsx I have gotten button clicks to printfn like that too. So that is fun.


NOTE:

While this will work from a notebook, after it runs once and the window is displayed, in order to run again, you need to restart the kernel.


ย