Single file minimum F# Avalonia UI app with notes.
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.
Subscribe to my newsletter
Read articles from Fxplorer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by