Skip to content

Migration Guide: Xamarin to .NET MAUI

This comprehensive guide helps you migrate your ReactiveUI Xamarin.Forms application to .NET MAUI.

Why Migrate to MAUI?

Benefits of .NET MAUI

? Modern .NET: Latest .NET features and performance ? Single Project: One project for all platforms ? Hot Reload: Faster development iteration ? Better Performance: Native AOT and trimming support ? Active Development: Ongoing Microsoft support ? Modern Tooling: Latest Visual Studio features ? Unified API: Consistent across platforms

Xamarin Limitations

? End of Support: Xamarin support ended May 2024 ? Old .NET: Limited to .NET Standard 2.0/2.1 ? Multiple Projects: Separate project per platform ? Slower Updates: Bug fixes and features deprecated ? Limited Tooling: Older Visual Studio integration

Prerequisites

  • Visual Studio 2022 17.8+ or VS Code with .NET MAUI extension
  • .NET 8.0 SDK or later
  • MAUI workload installed: dotnet workload install maui
  • ReactiveUI 22.x or 23.x (current)

Migration Overview

Project Structure Changes

Xamarin.Forms Structure

??? MyApp (Shared .NET Standard)
??? MyApp.Android
??? MyApp.iOS
??? MyApp.UWP (optional)
??? MyApp.Tests

.NET MAUI Structure

??? MyApp (Single Project)
?   ??? Platforms/
?   ?   ??? Android/
?   ?   ??? iOS/
?   ?   ??? MacCatalyst/
?   ?   ??? Windows/
?   ??? Resources/
?   ??? MauiProgram.cs
??? MyApp.Tests

Step-by-Step Migration

Step 1: Create New MAUI Project

dotnet new maui -n MyApp

Or use Visual Studio:

  • 1. File ? New ? Project
  • 2. Select ".NET MAUI App"
  • 3. Name your project

Step 2: Update Package References

Remove Xamarin Packages

<!-- Remove these -->
<PackageReference Include="Xamarin.Forms" />
<PackageReference Include="Xamarin.Essentials" />
<PackageReference Include="ReactiveUI.XamForms" />

Add MAUI Packages

<!-- Add these -->
<PackageReference Include="Microsoft.Maui.Controls" Version="*" />
<PackageReference Include="ReactiveUI.Maui" Version="*" />
<PackageReference Include="ReactiveUI.SourceGenerators" Version="*" PrivateAssets="all" />
<PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="*" PrivateAssets="all" />

Step 3: Migrate Application Setup

Xamarin.Forms App.xaml.cs

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
        
        // Xamarin initialization
        Locator.CurrentMutable.RegisterLazySingleton<IScreen>(() => new MainViewModel());
        Locator.CurrentMutable.Register<IViewFor<MainViewModel>>(() => new MainPage());
        
        MainPage = new NavigationPage(new MainPage());
    }
}

.NET MAUI MauiProgram.cs

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
        
        // Initialize ReactiveUI with RxAppBuilder
        var app = RxAppBuilder.CreateReactiveUIBuilder()
            .WithMaui()
            .WithViewsFromAssembly(typeof(App).Assembly)
            .WithRegistration(locator =>
            {
                locator.RegisterLazySingleton<IScreen>(() => new MainViewModel());
                locator.RegisterLazySingleton<IDataService>(() => new DataService());
            })
            .BuildApp();
        
        return builder.Build();
    }
}

MAUI App.xaml.cs

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
        
        MainPage = new AppShell(); // or NavigationPage
    }
}

Step 4: Migrate Pages/Views

Xamarin.Forms ContentPage

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyApp.MainPage">
    <StackLayout>
        <Label Text="Welcome to Xamarin.Forms!" />
    </StackLayout>
</ContentPage>
using Xamarin.Forms;
using ReactiveUI;
using ReactiveUI.XamForms;

public partial class MainPage : ReactiveContentPage<MainViewModel>
{
    public MainPage()
    {
        InitializeComponent();
        
        this.WhenActivated(disposables =>
        {
            // Bindings
        });
    }
}

.NET MAUI ContentPage

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyApp.MainPage">
    <VerticalStackLayout>
        <Label Text="Welcome to .NET MAUI!" />
    </VerticalStackLayout>
</ContentPage>
using ReactiveUI;
using ReactiveUI.Maui;

namespace MyApp;

public partial class MainPage : ReactiveContentPage<MainViewModel>
{
    public MainPage()
    {
        InitializeComponent();
        
        ViewModel = new MainViewModel();
        
        this.WhenActivated(disposables =>
        {
            // Bindings
        });
    }
}

Step 5: Update ViewModels

ViewModels typically require minimal changes, but you should adopt modern patterns:

Xamarin Era ViewModel

public class MainViewModel : ReactiveObject
{
    private string _text;
    public string Text
    {
        get => _text;
        set => this.RaiseAndSetIfChanged(ref _text, value);
    }
    
    public ReactiveCommand<Unit, Unit> SaveCommand { get; }
    
    public MainViewModel()
    {
        SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync);
    }
    
    private async Task SaveAsync() { }
}

Modern MAUI ViewModel

using ReactiveUI;
using ReactiveUI.SourceGenerators;

public partial class MainViewModel : ReactiveObject
{
    [Reactive]
    private string _text = string.Empty;
    
    [ReactiveCommand]
    private async Task Save()
    {
        // Save logic
    }
}

Step 6: Migrate Navigation

Xamarin Navigation

// Push
await Navigation.PushAsync(new DetailsPage());

// Pop
await Navigation.PopAsync();

// Modal
await Navigation.PushModalAsync(new ModalPage());

MAUI Shell Navigation

// Register routes in AppShell.xaml.cs
public AppShell()
{
    InitializeComponent();
    
    Routing.RegisterRoute("details", typeof(DetailsPage));
    Routing.RegisterRoute("modal", typeof(ModalPage));
}

// Navigate
await Shell.Current.GoToAsync("details");
await Shell.Current.GoToAsync("details", new Dictionary<string, object>
{
    ["ItemId"] = itemId
});

// Go back
await Shell.Current.GoToAsync("..");

// Modal
await Shell.Current.GoToAsync("modal", new Dictionary<string, object>
{
    ["Animated"] = true
});

Step 7: Migrate Platform-Specific Code

Xamarin DependencyService

// Xamarin
[assembly: Dependency(typeof(MyService))]
public class MyService : IMyService
{
    public void DoSomething() { }
}

// Usage
DependencyService.Get<IMyService>().DoSomething();

MAUI Dependency Injection

// Register in MauiProgram.cs
builder.Services.AddSingleton<IMyService, MyService>();

// Inject in constructor
public MainPage(IMyService myService)
{
    _myService = myService;
}

Platform-Specific Code Location

Xamarin:

??? MyApp.Android/
?   ??? MainActivity.cs
??? MyApp.iOS/
?   ??? AppDelegate.cs

MAUI:

??? Platforms/
?   ??? Android/
?   ?   ??? MainActivity.cs
?   ??? iOS/
?   ?   ??? AppDelegate.cs
?   ??? MacCatalyst/
?   ??? Windows/

Step 8: Update Resource Files

Images

Xamarin: Different folders per platform

??? Android/Resources/drawable/
??? iOS/Resources/

MAUI: Single Resources folder

??? Resources/
?   ??? Images/
?       ??? myimage.png

Usage remains the same:

<Image Source="myimage.png" />

Fonts

Xamarin: Platform-specific configuration

MAUI: Configure in MauiProgram.cs

builder.ConfigureFonts(fonts =>
{
    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
    fonts.AddFont("MaterialIcons-Regular.ttf", "MaterialIcons");
});

Step 9: Update Xamarin.Essentials Usage

Good news: Xamarin.Essentials is now built into MAUI!

// Both Xamarin and MAUI (no change needed)
var location = await Geolocation.GetLocationAsync();
var connected = Connectivity.NetworkAccess == NetworkAccess.Internet;
await Share.RequestAsync(new ShareTextRequest { Text = "Hello" });

Platform-Specific Migration

Android Migration

Update MainActivity

Xamarin.Forms:

[Activity(Label = "MyApp", Theme = "@style/MainTheme", MainLauncher = true)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        
        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
        
        LoadApplication(new App());
    }
}

MAUI:

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
    // Initialization handled by MAUI
}

Update AndroidManifest.xml

Permissions and configuration move to Platforms/Android/AndroidManifest.xml

iOS Migration

Update AppDelegate

Xamarin.Forms:

[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        global::Xamarin.Forms.Forms.Init();
        LoadApplication(new App());
        
        return base.FinishedLaunching(app, options);
    }
}

MAUI:

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

Update Info.plist

Configuration moves to Platforms/iOS/Info.plist

Common Migration Challenges

Challenge 1: Custom Renderers

Xamarin Custom Renderer:

[assembly: ExportRenderer(typeof(MyControl), typeof(MyControlRenderer))]
public class MyControlRenderer : ViewRenderer<MyControl, NativeControl>
{
    // Implementation
}

MAUI Handler:

public partial class MyControlHandler : ViewHandler<MyControl, NativeControl>
{
    public static IPropertyMapper<MyControl, MyControlHandler> PropertyMapper =
        new PropertyMapper<MyControl, MyControlHandler>(ViewMapper)
        {
            [nameof(MyControl.MyProperty)] = MapMyProperty
        };
    
    public MyControlHandler() : base(PropertyMapper)
    {
    }
    
    protected override NativeControl CreatePlatformView()
    {
        return new NativeControl();
    }
    
    public static void MapMyProperty(MyControlHandler handler, MyControl view)
    {
        // Update native control
    }
}

Challenge 2: Effects

Xamarin Effect:

public class ShadowEffect : PlatformEffect
{
    protected override void OnAttached() { }
    protected override void OnDetached() { }
}

MAUI Platform Behavior:

public class ShadowBehavior : PlatformBehavior<View>
{
    protected override void OnAttachedTo(View bindable) { }
    protected override void OnDetachedFrom(View bindable) { }
}

Challenge 3: MessagingCenter

Xamarin MessagingCenter:

MessagingCenter.Send(this, "Message", data);
MessagingCenter.Subscribe<T>(this, "Message", callback);

MAUI Alternatives:

Use ReactiveUI MessageBus (recommended):

MessageBus.Current.SendMessage(data);
MessageBus.Current.Listen<DataType>().Subscribe(callback);

Or use WeakReferenceMessenger:

WeakReferenceMessenger.Default.Send(new MyMessage(data));
WeakReferenceMessenger.Default.Register<MyMessage>(this, (r, m) => callback(m));

ReactiveUI-Specific Changes

Namespace Changes

// Xamarin
using ReactiveUI.XamForms;

// MAUI
using ReactiveUI.Maui;

Base Classes Remain the Same

// Both work the same
ReactiveContentPage<TViewModel>
ReactiveContentView<TViewModel>
ReactiveShell<TViewModel>

Routing Support

ReactiveUI routing works seamlessly with MAUI Shell:

public class AppViewModel : ReactiveObject, IScreen
{
    public RoutingState Router { get; } = new RoutingState();
    
    public AppViewModel()
    {
        // Navigate using ReactiveUI routing
        Router.Navigate.Execute(new PageViewModel(this))
            .Subscribe();
    }
}

Testing Considerations

Unit Tests

No changes needed for ViewModels:

[Fact]
public async Task ViewModel_LoadsData()
{
    var vm = new MainViewModel();
    await vm.LoadCommand.Execute();
    
    Assert.NotNull(vm.Data);
}

UI Tests

Update from Xamarin.UITest to .NET MAUI compatible testing:

  • Use Appium for cross-platform testing
  • Update selectors and automation IDs
  • Test on newer OS versions

Performance Optimization

MAUI-Specific Optimizations

<!-- Enable compilation -->
<PropertyGroup>
    <UseInterpreter>false</UseInterpreter>
    <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

Startup Performance

// MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
    // Only register handlers you use
    handlers.AddHandler<Entry, EntryHandler>();
});

Migration Checklist

Pre-Migration

  • Update to latest Xamarin.Forms (if possible)
  • Document custom renderers and effects
  • Review DependencyService usage
  • List platform-specific code
  • Backup current solution

During Migration

  • Create new MAUI project
  • Copy and update shared code
  • Migrate ViewModels to modern patterns
  • Update XAML namespaces
  • Migrate navigation to Shell
  • Convert custom renderers to handlers
  • Update platform-specific code
  • Migrate resources (images, fonts)
  • Update package references

Post-Migration

  • Test on all platforms
  • Update CI/CD pipelines
  • Review and optimize performance
  • Update documentation
  • Train team on MAUI differences
  • Remove Xamarin projects

Troubleshooting

Build Errors

Error: The name 'Forms' does not exist

Solution: Update namespaces from Xamarin.Forms to Microsoft.Maui.Controls

Runtime Errors

Error: Could not load assembly

Solution: Ensure all packages are MAUI-compatible versions

Layout Issues

Problem: Layouts don't look the same

Solution: Review layout changes in MAUI:

  • StackLayout ? VerticalStackLayout/HorizontalStackLayout
  • Update spacing and padding values
  • Test on actual devices

Resources and Tools

Official Tools

Microsoft Documentation

Community Resources

Timeline and Effort

Small App (5-10 screens):

  • Migration: 1-2 weeks
  • Testing: 1 week

Medium App (10-30 screens):

  • Migration: 3-4 weeks
  • Testing: 2 weeks

Large App (30+ screens):

  • Migration: 6-12 weeks
  • Testing: 3-4 weeks

Add time for:

  • Complex custom renderers
  • Extensive platform-specific code
  • Third-party package compatibility

Getting Help

If you encounter issues during migration:


Migration Difficulty: Moderate to Complex Recommended Timeline: Plan 2-12 weeks depending on app size Long-term Benefits: Modern platform, better performance, ongoing support