Table of Contents

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 19.5.31+

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:

  1. Check MAUI GitHub Issues
  2. Ask on ReactiveUI Slack
  3. Review Migration Documentation
  4. Post on Stack Overflow with tags: maui, reactiveui

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