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¶
- .NET Upgrade Assistant: Automated migration tool
- MAUI Migration Analyzer: Visual Studio analyzer
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