- A Xamarin.Forms multi-project solution collapses into a single Uno Platform project with a
Platformsfolder for each target. - XAML control names change (
Label→TextBlock,Entry→TextBox,StackLayout→StackPanel), but the layout concepts remain the same. - Data binding syntax is nearly identical;
BindingContextbecomesDataContext, andx:Bindoffers compiled binding as an upgrade path. DependencyServiceis replaced by Microsoft.Extensions.DependencyInjection through the Uno Extensions host builder.- Navigation migrates from
NavigationPage/Shell to WinUIFramenavigation or the Uno Navigation extension for route-based patterns. - Platform-specific code moves from
Device.RuntimePlatformchecks to#ifconditional compilation or platform-specific XAML prefixes.
Who this is for: Xamarin.Forms developers performing the hands-on migration work. You should be comfortable with XAML, C#, and the general structure of a Xamarin.Forms solution.
Xamarin.Forms reached end-of-life in May 2024. If your app is still running on it, every day without a migration plan is a day closer to security gaps, missing OS updates, and an increasingly painful upgrade. The good news: you do not need to rewrite everything from scratch. Your XAML knowledge, your ViewModels, your data binding patterns, and most of your C# business logic carry over to Uno Platform with targeted, mechanical changes.
This article is the hands-on walkthrough. It assumes you have already evaluated Uno Platform and decided it is the right target. Here, you will translate a Xamarin.Forms solution into an Uno Platform 6.5 project running on .NET 10, migrate XAML page by page, rewire navigation and dependency injection, and verify the result on mobile and WebAssembly.
1. Project Structure Translation
The Xamarin.Forms Shape
MyApp.sln
├── MyApp/ # Shared project
│ ├── App.xaml / App.xaml.cs
│ ├── Views/
│ ├── ViewModels/
│ ├── Models/
│ ├── Services/
│ └── Converters/
├── MyApp.Android/ # Android head
├── MyApp.iOS/ # iOS head
└── MyApp.UWP/ # UWP headThe Uno Platform Shape
MyApp.sln
├── MyApp/ # Single cross-platform project
│ ├── MyApp.csproj # Uno.Sdk-based project file
│ ├── App.xaml / App.xaml.cs
│ ├── MainPage.xaml
│ ├── Presentation/
│ ├── ViewModels/
│ ├── Models/
│ ├── Services/
│ ├── Converters/
│ ├── Platforms/
│ │ ├── Android/
│ │ ├── iOS/
│ │ ├── Desktop/
│ │ └── WebAssembly/
│ ├── Assets/
│ └── Strings/What Moves Where
| Xamarin.Forms Location | Uno Platform Location |
|---|---|
| Shared project (Views, VMs, Models, Services) | Root of the single project |
App.xaml / App.xaml.cs | App.xaml / App.xaml.cs (rewritten for WinUI) |
Android MainActivity.cs | Platforms/Android/MainActivity.Android.cs |
iOS AppDelegate.cs | Platforms/iOS/Main.iOS.cs |
| UWP head project | Removed (Windows runs natively via WinUI) |
| NuGet refs across multiple .csproj files | Single .csproj with UnoFeatures |
Create the target project:
dotnet new unoapp -n MyApp --preset recommendedFrom here, copy your shared code (ViewModels, Models, Services, Converters) into the new project and begin translating your XAML views.
2. XAML Dialect Changes
Xamarin.Forms XAML and WinUI XAML share the same conceptual foundation, but the control names, property names, and namespace URIs differ.
Control Mapping Table
| Xamarin.Forms | Uno Platform (WinUI) | Notes |
|---|---|---|
ContentPage | Page | Root element for each view |
StackLayout | StackPanel | Same concept, different name |
AbsoluteLayout | Canvas | Position with Canvas.Left / Canvas.Top |
FlexLayout | Grid or AutoLayout | AutoLayout from Uno Toolkit |
Grid | Grid | Syntax changes for row/column definitions |
Label | TextBlock | Text property remains the same |
Entry | TextBox | Placeholder → PlaceholderText |
Editor | TextBox + AcceptsReturn="True" | Multi-line text input |
Button | Button | Text → Content |
ListView | ListView | Different API surface; template syntax changes |
CollectionView | ItemsRepeater or ListView | ItemsRepeater for custom layouts |
Shell | Uno Navigation extension or Frame | See Navigation section |
TabbedPage | NavigationView with PaneDisplayMode="Top" | Tab-based navigation |
FlyoutPage | NavigationView | Sidebar pattern |
CarouselPage | FlipView | Swipe-based navigation |
Grid Definition Syntax
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,2*"><Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
</Grid>XAML Namespace Changes
| Xamarin.Forms | Uno Platform (WinUI) |
|---|---|
xmlns="http://xamarin.com/schemas/2014/forms" | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
3. Data Binding Migration
Data binding is where Xamarin.Forms developers get the most relief during migration. The core patterns port with minimal changes.
| Xamarin.Forms | Uno Platform (WinUI) | Notes |
|---|---|---|
{Binding Prop} | {Binding Prop} | Works identically |
{Binding Prop} | {x:Bind VM.Prop, Mode=OneWay} | Compiled binding (recommended upgrade) |
BindingContext | DataContext | Same role, different property name |
INotifyPropertyChanged | INotifyPropertyChanged | No changes needed |
ObservableCollection<T> | ObservableCollection<T> | No changes needed |
ICommand | ICommand | No changes needed |
StringFormat='{0:F2}' | Use x:Bind with function or converter | WinUI Binding doesn't support StringFormat |
A ViewModel That Works in Both
The following ViewModel requires zero changes when moving from Xamarin.Forms to Uno Platform:
public class ItemListViewModel : INotifyPropertyChanged
{
private string _searchQuery;
private bool _isLoading;
public ItemListViewModel()
{
Items = new ObservableCollection<ItemModel>();
SearchCommand = new RelayCommand(ExecuteSearch);
}
public ObservableCollection<ItemModel> Items { get; }
public ICommand SearchCommand { get; }
public string SearchQuery
{
get => _searchQuery;
set { _searchQuery = value; OnPropertyChanged(); }
}
// INotifyPropertyChanged implementation identical
}Setting the DataContext
<ContentPage.BindingContext>
<vm:ItemListViewModel />
</ContentPage.BindingContext><Page.DataContext>
<vm:ItemListViewModel />
</Page.DataContext>The change is mechanical: ContentPage to Page, BindingContext to DataContext.
Upgrading to x:Bind
If you want compiled bindings (better performance, build-time error checking), add a strongly-typed ViewModel property in code-behind and reference it in XAML:
<TextBlock Text="{x:Bind ViewModel.SearchQuery, Mode=OneWay}" />
<ListView ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}" />Note: x:Bind defaults to OneTime mode, so you must explicitly set Mode=OneWay or Mode=TwoWay for properties that change at runtime.
4. Dependency Injection Migration
Xamarin.Forms ships with DependencyService, a simple service locator. Uno Platform replaces this with Microsoft.Extensions.DependencyInjection through the Uno Extensions host builder.
// Registration (via attribute)
[assembly: Dependency(typeof(PlatformNotificationService))]
// Resolution
var service = DependencyService.Get<INotificationService>();
service.ShowNotification("Hello");// In App.xaml.cs host builder
host.ConfigureServices((context, services) =>
{
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IDataService, DataService>();
services.AddTransient<ItemListViewModel>();
});
// Constructor injection in ViewModels
public class ItemListViewModel : INotifyPropertyChanged
{
private readonly IDataService _dataService;
public ItemListViewModel(IDataService dataService)
{
_dataService = dataService;
}
}This is the standard .NET pattern. If you have used ASP.NET Core or any other modern .NET host, this will feel familiar.
5. Navigation Migration
Navigation is typically the area requiring the most structural change. Xamarin.Forms navigation is instance-based and asynchronous. Uno Platform navigation is type-based and synchronous (for Frame), or route-based with the Uno Navigation extension.
| Operation | Xamarin.Forms | Uno Platform (Frame) |
|---|---|---|
| Navigate forward | await Navigation.PushAsync(new DetailPage()) | Frame.Navigate(typeof(DetailPage)) |
| Navigate back | await Navigation.PopAsync() | Frame.GoBack() |
| Pass parameter | new DetailPage(itemId) | Frame.Navigate(typeof(DetailPage), itemId) |
| Receive parameter | Constructor parameter | OnNavigatedTo(e) with e.Parameter |
| Page appeared | OnAppearing() | OnNavigatedTo() |
| Page disappeared | OnDisappearing() | OnNavigatedFrom() |
Shell to Uno Navigation Extension
If your Xamarin.Forms app uses Shell with URI-based routing, the Uno Navigation extension provides a similar model:
await Shell.Current.GoToAsync($"details?id={item.Id}");await navigator.NavigateRouteAsync(this, $"details?id={item.Id}");6. Migrating Page by Page
The most effective strategy is to translate one page at a time, verify it works, and move on. Here is a complete before-and-after for a contact detail page.
<ContentPage Title="Contact Details">
<ContentPage.BindingContext>
<vm:ContactViewModel />
</ContentPage.BindingContext>
<StackLayout Padding="16">
<Label Text="{Binding FullName}" FontSize="24" FontAttributes="Bold" />
<Label Text="{Binding Email}" TextColor="Gray" />
<Entry Text="{Binding PhoneNumber, Mode=TwoWay}"
Placeholder="Phone number" Keyboard="Telephone" />
<Button Text="Save" Command="{Binding SaveCommand}"
BackgroundColor="#512BD4" TextColor="White" />
</StackLayout>
</ContentPage><Page>
<Page.DataContext>
<vm:ContactViewModel />
</Page.DataContext>
<StackPanel Padding="16">
<TextBlock Text="{Binding FullName}" FontSize="24" FontWeight="Bold" />
<TextBlock Text="{Binding Email}" Foreground="Gray" />
<TextBox Text="{Binding PhoneNumber, Mode=TwoWay}"
PlaceholderText="Phone number" InputScope="TelephoneNumber" />
<Button Content="Save" Command="{Binding SaveCommand}"
Background="#512BD4" Foreground="White" />
</StackPanel>
</Page>Key Changes Applied
| Change | What Happened |
|---|---|
ContentPage → Page | Root element swap |
BindingContext → DataContext | Property name change |
StackLayout → StackPanel | Control name swap |
Label → TextBlock | Control name swap |
FontAttributes="Bold" → FontWeight="Bold" | Property name change |
TextColor → Foreground | Property name change |
Entry → TextBox | Control name swap |
Placeholder → PlaceholderText | Property name change |
Keyboard="Telephone" → InputScope="TelephoneNumber" | Input type mechanism change |
Button Text → Button Content | Property name change |
BackgroundColor → Background | Property name change |
7. Platform-Specific Code
Replacing Device.RuntimePlatform
if (Device.RuntimePlatform == Device.Android) { /* ... */ }
else if (Device.RuntimePlatform == Device.iOS) { /* ... */ }Approach 1: Conditional Compilation
#if __ANDROID__
// Android-specific logic
#elif __IOS__
// iOS-specific logic
#elif __WASM__
// WebAssembly-specific logic
#elif HAS_UNO_SKIA
// Desktop (Skia) specific logic
#endifFor cleaner separation, use file-naming conventions. The Uno SDK automatically includes .Android.cs files only when building for Android, .iOS.cs files only for iOS, and so on.
Approach 2: Platform-Specific XAML Prefixes
<TextBlock Text="Welcome"
android:FontSize="18"
ios:FontSize="17"
win:FontSize="16" />
<!-- Entire element only on iOS -->
<ios:TextBlock Text="This only appears on iOS" />Runtime Platform Detection
For shared ViewModels where you can't use #if directives:
if (OperatingSystem.IsAndroid()) { /* ... */ }
else if (OperatingSystem.IsIOS()) { /* ... */ }
else if (OperatingSystem.IsBrowser()) { /* ... */ }8. Run, Connect App MCP, and Verify
Build and Run
# WebAssembly (fast iteration)
dotnet build -f net10.0-browserwasm
dotnet run -f net10.0-browserwasm
# Android
dotnet build -f net10.0-android
dotnet run -f net10.0-androidVerify with AI Agent and App MCP
If you're using an AI coding agent with the Uno Platform App MCP connected:
- Screenshot the running app. The agent captures a screenshot and compares it against the original Xamarin.Forms version.
- Inspect the visual tree. The App MCP exposes the live visual tree to identify binding errors, missing DataContext assignments, or controls that failed to render.
- Check for binding errors. The agent scans for debug output lines like
Error: BindingExpression path error. - Iterate. Fix each issue, apply via Hot Reload where possible, and re-verify.
What You Should Have Running
- The main page, fully translated from Xamarin.Forms XAML to WinUI XAML
- At least one secondary page (detail or settings)
- Working navigation between the two pages
- Data binding connected to your existing ViewModels
- DI wired through the Uno Extensions host builder
- The app running on at least two targets: a mobile platform and WebAssembly
Checklist for Each Migrated Page
ContentPage to PageBindingContext replaced with DataContextStringFormat bindings replaced with converters or x:Bind functionsViewCell wrappers removed from list templatesRowDefinition/ColumnDefinition elementsPushAsync/PopAsync to Frame.Navigate/Frame.GoBackDevice.RuntimePlatform to #if directives or XAML prefixesFAQ
Can I migrate incrementally?
Yes — one page at a time. Create the Uno Platform project, copy your shared code, and then translate XAML views one by one.
Do my ViewModels need to change?
In most cases, no. ViewModels built with INotifyPropertyChanged, ObservableCollection<T>, and ICommand work without modification. Only change if your VMs reference Xamarin.Forms-specific types.
What about third-party controls?
Check whether the vendor offers WinUI or Uno Platform versions. Many major vendors now ship WinUI-compatible libraries.
Is x:Bind required?
{Binding} works fine in Uno Platform. x:Bind is an optional upgrade that provides compiled bindings, better performance, and build-time error detection.
What replaces Xamarin.Forms Effects?
WinUI does not have an Effects system. Replace with custom attached properties, control styles, or by extending controls directly. The Uno Toolkit provides common behaviors as attached properties.
What about MessagingCenter?
Replace MessagingCenter with WeakReferenceMessenger from the CommunityToolkit.Mvvm package. Similar API, better memory management.
What about custom renderers?
Custom renderers do not exist in WinUI. Replace with templated controls, custom controls that extend existing WinUI controls, or platform-specific code using conditional compilation with partial classes.
Can I target all six platforms from one project?
Yes. Uno Platform 6.5 supports Android, iOS, macOS, Windows, Linux (Skia), and WebAssembly from one project.
Subscribe to Our Blog
Subscribe via RSS
Back to Top