Skip to main content
Migrating WPF Caliburn.Micro Apps to .NET MAUI: Keeping the Magic Alive

Migrating WPF Caliburn.Micro Apps to .NET MAUI: Keeping the Magic Alive

Learn how to migrate your WPF Caliburn.Micro MVVM applications to .NET MAUI while keeping the conventions and developer experience you love.

  1. Posts/

Migrating WPF Caliburn.Micro Apps to .NET MAUI: Keeping the Magic Alive

·946 words·5 mins· loading
👤

Chris Malpass

Author

If you’ve spent any significant time in the WPF trenches over the last decade, chances are you’ve crossed paths with Caliburn.Micro. For many of us, it was the framework that finally made MVVM “click.” It stripped away the boilerplate, replaced verbose configuration with smart conventions, and let us focus on building features.

Now that .NET MAUI is the standard for cross-platform .NET development, you might be staring at your existing WPF codebases and wondering: “Do I have to learn a whole new MVVM framework? Do I have to give up my conventions?”

The answer is a resounding no.

Caliburn.Micro is alive and well in the .NET MAUI era. While the underlying platform has changed, the “magic” feels remarkably familiar. In this post, we’ll walk through how to migrate the core structure of a Caliburn.Micro WPF app to .NET MAUI.

The Paradigm Shift: Goodbye Bootstrapper
#

In the WPF world, everything started with the Bootstrapper. You’d subclass BootstrapperBase, override OnStartup, and wire up your IoC container there. It was the heart of your application.

In .NET MAUI, the application lifecycle is different. We don’t have a Bootstrapper class anymore. Instead, the responsibilities of the bootstrapper have moved directly into the App class, which now inherits from Caliburn.Micro.Maui.MauiApplication.

Let’s look at how to set this up from scratch.

Step 1: The Setup
#

First, create a new .NET MAUI project. Once you’re in, you’ll need to install the package. Be careful here—you don’t just want the core library; you need the platform-specific integration.

1
dotnet add package Caliburn.Micro.Maui

Note: Ensure you are targeting .NET 9 or a compatible version, as Caliburn.Micro has kept pace with the latest platform updates.

Step 2: The New “Bootstrapper”
#

Open your App.xaml.cs. In a standard MAUI app, this inherits from Application. We’re going to change that to Caliburn.Micro.Maui.MauiApplication.

This is where we configure our container and set the root view.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using Caliburn.Micro;
using Caliburn.Micro.Maui;
using MyMauiApp.ViewModels;

namespace MyMauiApp;

public partial class App : Caliburn.Micro.Maui.MauiApplication
{
    private SimpleContainer _container;

    public App()
    {
        InitializeComponent();

        // 1. Initialize the framework
        Initialize();

        // 2. Set the root view (Async!)
        DisplayRootViewForAsync<MainViewModel>();
    }

    protected override void Configure()
    {
        _container = new SimpleContainer();
        _container.Instance(_container);

        // 3. Register your services and ViewModels
        _container.Singleton<IWindowManager, WindowManager>();
        _container.Singleton<IEventAggregator, EventAggregator>();
        
        _container.PerRequest<MainViewModel>();
    }

    protected override object GetInstance(Type service, string key)
    {
        return _container.GetInstance(service, key);
    }

    protected override IEnumerable<object> GetAllInstances(Type service)
    {
        return _container.GetAllInstances(service);
    }

    protected override void BuildUp(object instance)
    {
        _container.BuildUp(instance);
    }
}

Key Differences to Note:
#

  1. Initialize() in Constructor: We call Initialize() directly in the constructor.
  2. Async Root View: We use DisplayRootViewForAsync<T>(). MAUI’s navigation primitives are asynchronous, and Caliburn respects that.
  3. Configure(): This looks almost identical to the WPF version. Your DI logic (whether you use SimpleContainer or Autofac) lives here.

Step 3: The Entry Point
#

MAUI uses a builder pattern in MauiProgram.cs to construct the app. We need to make sure our App class is registered correctly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
namespace MyMauiApp;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>() // This points to our Caliburn-enhanced App class
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        return builder.Build();
    }
}

Step 4: The Magic (Conventions)
#

This is why we’re here. Does x:Name still automagically bind to properties? Yes.

Let’s create a simple MainViewModel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using Caliburn.Micro;

namespace MyMauiApp.ViewModels;

public class MainViewModel : Screen
{
    private string _message = "Hello from Caliburn.Micro in MAUI!";

    public string Message
    {
        get => _message;
        set => Set(ref _message, value);
    }

    public void ClickMe()
    {
        Message = "You clicked the button! Magic is real.";
    }
}

And the corresponding MainView.xaml (in the Views folder):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.Views.MainView">
    <VerticalStackLayout Spacing="20" Padding="30">
        
        <!-- Convention Binding: Binds to 'Message' property -->
        <Label x:Name="Message" 
               FontSize="Large"
               HorizontalOptions="Center" />

        <!-- Convention Binding: Binds to 'ClickMe' method -->
        <Button x:Name="ClickMe"
                Text="Click Me"
                HorizontalOptions="Center" />
                
    </VerticalStackLayout>
</ContentPage>

Just like in WPF, Caliburn.Micro locates MainView based on the name MainViewModel, binds the Label to the Message property, and wires the Button to the ClickMe method. No Command boilerplate required.

Pitfalls & Gotchas
#

While the MVVM structure is preserved, the platform underneath has shifted.

1. Async Navigation
#

In WPF, TryClose() was synchronous. In MAUI, navigation is inherently async. You’ll find yourself using await more often when coordinating screens.

2. XAML Dialect
#

Caliburn handles the binding, but it doesn’t translate the controls.

  • WPF StackPanel -> MAUI VerticalStackLayout / HorizontalStackLayout
  • WPF TextBox -> MAUI Entry or Editor
  • WPF TextBlock -> MAUI Label

You will still need to rewrite your XAML, but your ViewModels can often be ported with minimal changes (mostly changing namespaces).

Conclusion
#

Migrating from WPF to MAUI is a significant undertaking, but sticking with Caliburn.Micro reduces the cognitive load immensely. You keep your architectural patterns, your testing strategies, and your muscle memory for conventions.

If you have a mature WPF application built on Caliburn, you don’t need to throw it all away. The framework has evolved with the times, proving once again that good abstractions are timeless.

For a complete working example, check out the official Setup.Maui sample in the Caliburn.Micro repository.

Happy coding!