VB on Avalonia: the VB6 form-and-handler model, cross-compiled to Linux from Visual Studio 2026
I spent a weekend proving the VB6 loop is alive outside Windows. Visual Basic on Avalonia 11, .NET 10, Visual Studio 2026, two update patterns side-by-side, and the same VB source publishing to a 47 MB self-contained win-x64 .exe and a 47 MB self-contained linux-x64 ELF. No first-party VB template for Avalonia, so the .vbproj is hand-rolled. Everything downstream behaves normally. Full source and two build guides on GitHub.
In Saturday's weekly roundup the Visual Basic section was honest about being a quiet news week, and then I went off-script in the last paragraph and started thinking out loud:
Avalonia is XAML on the surface, but underneath it is straightforward .NET that any language in the family can reference. Nothing in the runtime stops a VB.NET project from pulling Avalonia in, wiring up windows in code, and writing event handlers the same way WinForms taught a generation of us to do. [...] Whether the tooling fully cooperates with a
.vbprojis the kind of thing I would have to actually try. Maybe I should.
So I did. I spent the rest of the weekend building the proof. It is a small Avalonia 11 app written in Visual Basic, on .NET 10, opened in Visual Studio 2026, that I publish to both a Windows .exe and a Linux ELF from the same Windows box, from the same VB source, with one command per target. Drop the binaries on either OS and they run. The repo is public:
- GitHub: EvilGeniusCore/VB-Avalonia
This sits next to the WinForms post from late April, which argued the VB6 form-and-handler model survived on Windows because it sits on Win32. The interesting follow-up question was always what happens to the model when you take Windows out of the picture. If it is genuinely good it survives the move, and if it is just nostalgia it does not. The answer that came out of the weekend: the model survives.
The setup
The application is intentionally small. One window, two bordered groups, two buttons, two labels. Each group demonstrates a different way to push a value from code into the UI: one in the shape a VB6 developer reaches for without thinking, and one in the shape modern XAML frameworks usually want you to reach for. The point is not the app. The point is that the workflow getting you there is recognisable.
The stack:
- .NET 10, the current runtime
- Avalonia 11, the cross-platform UI framework
- Visual Basic, with
Option Strict On,Option Infer On, nullable enabled - Visual Studio 2026, with the Avalonia VS extension installed
VS Code also works for the same project, with the C# Dev Kit handling debugger and .vbproj plumbing. I covered that in the second guide.
The one wart, up front
Avalonia officially ships C# and F# templates. There is no dotnet new template for Visual Basic. When you scaffold a project the normal way, you get a C# project, and the VB compiler does not pick up Avalonia's XAML toolchain.
The fix is one hand-rolled .vbproj. Once that file is right, everything else behaves normally: the Avalonia previewer renders the window in the designer pane, F5 launches the app under the debugger, hot reload works on edits, the publish step produces single-file self-contained binaries. The friction is concentrated in that one file. After that, you write VB the way you always have.
The full .vbproj is in the first guide. The compressed version of what it does is: target net10.0, pull in Avalonia, Avalonia.Desktop, Avalonia.Themes.Fluent, and the one non-obvious package, Avalonia.Markup.Xaml.Loader. The XAML loader is the package that lets the previewer materialise the tree against the VB-compiled assembly. Without it, the designer pane goes blank and the runtime throws on the first AvaloniaXamlLoader.Load(Me) call.
A second gotcha that cost me about ten minutes: do not manually include *.axaml in an <ItemGroup> in the project file. The Avalonia SDK targets already include them as AvaloniaResource. Adding a manual glob registers each file twice and you get AVLN2002 Duplicate x:Class directive at build time. Trust the SDK targets.
Two ways to update a control
The window shows the same VB6 moment twice. Once with a named control that code-behind updates directly, the pattern a VB6 developer will reach for without thinking. Once with a property bound through INotifyPropertyChanged, the pattern modern XAML frameworks usually want. Showing both side by side lets you pick per control which to use.
Pattern 1, x:Name plus direct assignment in code-behind:
Private ReadOnly txtDirect As TextBlock
Public Sub New()
AvaloniaXamlLoader.Load(Me)
txtDirect = Me.GetControl(Of TextBlock)("txtDirect")
DataContext = Me
End Sub
Private Sub DirectButton_Click(sender As Object, e As RoutedEventArgs)
txtDirect.Text = $"Hello via x:Name - {DateTime.Now:HH:mm:ss}"
End Sub
That is functionally what Designer.vb did for you in VB6 and WinForms, just with one line of manual wiring per named control. Avalonia's XAML compiler generates the x:Name field declarations for C# code-behind and not for VB, so we declare the field and resolve it ourselves. GetControl(Of T) throws if the name is missing, so a typo fails at construction rather than as a NullReferenceException halfway through an event handler.
Pattern 2, a bound property with INotifyPropertyChanged:
Public Shadows Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Public Property Greeting As String
Get
Return _greeting
End Get
Set(value As String)
If _greeting <> value Then
_greeting = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(Greeting)))
End If
End Set
End Property
Private Sub BindingButton_Click(sender As Object, e As RoutedEventArgs)
Greeting = $"Hello via binding - {DateTime.Now:HH:mm:ss}"
End Sub
The Shadows keyword is the one VB-specific bit in that snippet. AvaloniaObject defines its own PropertyChanged event with a different signature, so you have to shadow it to coexist with the INotifyPropertyChanged implementation. Bindings dispatch through the interface, the existing AvaloniaObject machinery does not lose anything, and you get to keep the standard XAML binding story.
Both patterns work. Both are productive. The trade-off is roughly:
| Pattern 1: x:Name + direct | Pattern 2: + INPC | |
|---|---|---|
| Familiarity | Highest. Reads exactly like VB6 or .NET WinForms. | Lower. Requires understanding INotifyPropertyChanged. |
| Ceremony | One field declaration, one constructor line per control. | One property, one event field, Shadows, DataContext = Me. |
| Fan-out cost | Three callers means three txt.Text = ... lines. |
One property, every bound control updates for free. |
| Testing | Needs a real TextBlock or a UI test. |
The property is testable with no UI in scope. |
| Best fit | Single-use controls and prototype screens. | A value driving multiple controls, or two-way input. |
Default to Pattern 1 for one-off controls that one button updates one way. Reach for Pattern 2 when the same value drives multiple controls or you want to test the form's state machine without instantiating UI. Do not mix them on the same control; a TextBlock with both x:Name and {Binding} is a debugging trap waiting to happen.
This is the same per-control judgment a WinForms developer makes between "set Label1.Text directly" and "data-bind it to a property on a backing object." The judgment carries over.
The cross-compile
Two publish profiles, identical aside from the runtime identifier. Both produce self-contained single-file binaries, no .NET runtime required on the target. Trimming is off because Avalonia uses runtime reflection that the trimmer can sever.
From the Windows box, both commands run back to back:
dotnet publish src\VBAvalonia.App\VBAvalonia.App.vbproj -p:PublishProfile=win-x64
dotnet publish src\VBAvalonia.App\VBAvalonia.App.vbproj -p:PublishProfile=linux-x64
The artefacts:
publish/win-x64/VBAvalonia.App.exe
PE32+ executable for MS Windows 6.00 (GUI), x86-64, 9 sections
publish/linux-x64/VBAvalonia.App
ELF 64-bit LSB pie executable, x86-64, dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0
Both around 47 MB. The full .NET runtime plus Avalonia plus the app, compressed into one file. Drop the win-x64 .exe on Windows, it runs. Copy the linux-x64 ELF to a Linux box or WSL, mark it executable, run it under any X11 or Wayland session, it runs. The same is true in reverse: a Linux developer publishes both profiles, gets a real Windows .exe out of the win-x64 build, and a Linux ELF out of the linux-x64 build. The .pubxml profiles are platform-neutral.
There is nothing especially exotic about this. .NET has cross-compiled this way since .NET Core 3.0 in 2019. The thing I was curious about was whether the Visual Basic compiler's contribution to that pipeline still produces clean cross-platform output in 2026, on a UI framework Microsoft does not officially ship a VB template for. It does.
The honest scorecard
Things that worked on the first try:
- VB compiler against .NET 10. No surprises, no flags, no special handling.
- The Avalonia XAML toolchain on VB-emitted assemblies, once the hand-rolled
.vbprojwas correct. - F5 in Visual Studio 2026, breakpoint hit on the first click.
- F5 in VS Code with the C# Dev Kit, same experience.
- Both publish profiles, same publish command, both binaries ran without modification on their respective targets.
Things that took a minute:
- The
AVLN2002 Duplicate x:Class directivebuild error from adding<AvaloniaResource Include="**\*.axaml" />manually. Removed it, build clean. - The
Shadowskeyword onPropertyChanged. Without it the compiler complains that the event is already defined onAvaloniaObject. Once you know the trick, you write it once and forget it. - A blank previewer pane until I rebuilt once with
Avalonia.Markup.Xaml.Loaderreferenced.
Things I have not tried yet:
- ARM64 publish, either Windows or Linux.
- Hot reload across a save during a debug session. The previewer pane refreshes for XAML edits in the designer, but I have not pushed it.
- A second window and a shared standard-module equivalent. That is guide 03, planned.
What this proves and what it does not
This is not the claim that everyone who liked VB6 should drop their C# project and rewrite it in VB on Avalonia. It is also not the claim that VB.NET is back in active language evolution. Microsoft stopped adding new language features to VB.NET around 2020 and that has not changed. The runtime ships, the compiler ships, the IDE supports it, and the existing language surface is enough to be productive on a modern UI framework. That is the claim.
What this does prove, narrowly:
- The VB6 form-and-handler model survives intact on a cross-platform UI framework in 2026.
- The friction is concentrated in one hand-rolled project file. After that you write VB the way you always have.
- The same VB source cross-compiles to Windows and Linux from a single host, in self-contained single-file form, with one publish command per target.
- A VB6 developer dropped into this stack is productive in the time it takes to read the first guide and clone the repo.
The form-and-handler shape is durable in a way the implementation layer underneath it is not. WinForms inherits its longevity from Win32. Avalonia gets there a different way, by owning its own rendering pipeline on top of SkiaSharp on every platform it supports. The developer-facing model converges on the same thing once you are inside the code-behind file.
The guide repo is meant to be the place you point a VB-curious developer who wants to try this without spending an evening figuring out which packages and which <ItemGroup> shape stops the build from breaking. Both guides walk through the full setup. The second one is the VS Code variant with the publish profile XML included.
If you have shipped VB6 in production and you want to see whether the muscle memory still fires on a cross-platform UI framework, this is your sandbox. Clone it, F5 it, change something, ship a build to a Linux box you have lying around. The answer should not surprise you. The fact that the answer holds across that move might.
What is next
- Guide 03 on a second window, navigation, and a shared standard-module equivalent. The VB6 standard-module-as-glue pattern translates more cleanly than I expected.
- A guide on modern data binding from VB. Pattern 2 in this app uses hand-rolled
INotifyPropertyChanged, which is the honest starting point for a VB6 developer reading the code. The next step is what the rest of the .NET XAML ecosystem actually does:CommunityToolkit.Mvvmsource generators,[ObservableProperty],[RelayCommand], compiled bindings. All of it is available to a VB project that references the right packages. Whether the source-generator story is as smooth from VB as it is from C# is the open question. - ARM64 publish.
- A first response to the comments and replies that came in on the VB6 question post. The pile is bigger than I expected and I owe people a substantive answer rather than a thank-you note.
Sources
- GitHub: EvilGeniusCore/VB-Avalonia, full source and the two guides.
- Avalonia documentation, 11.x release notes.
- Microsoft DevBlogs, .NET 10 release announcements.
- Microsoft Learn,
dotnet publishand self-contained single-file deployment. - Personal: about a hundred VB3, VB4, VB5, and VB6 line-of-business systems shipped between 1995 and 2010.
// comments