Tutorial Xamarin.Forms Migration
Key Takeaways
  • A Xamarin.Forms multi-project solution collapses into a single Uno Platform project with a Platforms folder for each target.
  • XAML control names change (LabelTextBlock, EntryTextBox, StackLayoutStackPanel), but the layout concepts remain the same.
  • Data binding syntax is nearly identical; BindingContext becomes DataContext, and x:Bind offers compiled binding as an upgrade path.
  • DependencyService is replaced by Microsoft.Extensions.DependencyInjection through the Uno Extensions host builder.
  • Navigation migrates from NavigationPage/Shell to WinUI Frame navigation or the Uno Navigation extension for route-based patterns.
  • Platform-specific code moves from Device.RuntimePlatform checks to #if conditional 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.

Project Structure

1. Project Structure Translation

The Xamarin.Forms Shape

Xamarin.Forms
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 head

The Uno Platform Shape

Uno Platform
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 LocationUno Platform Location
Shared project (Views, VMs, Models, Services)Root of the single project
App.xaml / App.xaml.csApp.xaml / App.xaml.cs (rewritten for WinUI)
Android MainActivity.csPlatforms/Android/MainActivity.Android.cs
iOS AppDelegate.csPlatforms/iOS/Main.iOS.cs
UWP head projectRemoved (Windows runs natively via WinUI)
NuGet refs across multiple .csproj filesSingle .csproj with UnoFeatures

Create the target project:

Terminal
dotnet new unoapp -n MyApp --preset recommended

From here, copy your shared code (ViewModels, Models, Services, Converters) into the new project and begin translating your XAML views.

XAML

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.FormsUno Platform (WinUI)Notes
ContentPagePageRoot element for each view
StackLayoutStackPanelSame concept, different name
AbsoluteLayoutCanvasPosition with Canvas.Left / Canvas.Top
FlexLayoutGrid or AutoLayoutAutoLayout from Uno Toolkit
GridGridSyntax changes for row/column definitions
LabelTextBlockText property remains the same
EntryTextBoxPlaceholderPlaceholderText
EditorTextBox + AcceptsReturn="True"Multi-line text input
ButtonButtonTextContent
ListViewListViewDifferent API surface; template syntax changes
CollectionViewItemsRepeater or ListViewItemsRepeater for custom layouts
ShellUno Navigation extension or FrameSee Navigation section
TabbedPageNavigationView with PaneDisplayMode="Top"Tab-based navigation
FlyoutPageNavigationViewSidebar pattern
CarouselPageFlipViewSwipe-based navigation

Grid Definition Syntax

Xamarin.Forms
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,2*">
Uno Platform
<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.FormsUno 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"
Bindings

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.FormsUno Platform (WinUI)Notes
{Binding Prop}{Binding Prop}Works identically
{Binding Prop}{x:Bind VM.Prop, Mode=OneWay}Compiled binding (recommended upgrade)
BindingContextDataContextSame role, different property name
INotifyPropertyChangedINotifyPropertyChangedNo changes needed
ObservableCollection<T>ObservableCollection<T>No changes needed
ICommandICommandNo changes needed
StringFormat='{0:F2}'Use x:Bind with function or converterWinUI 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:

C#
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

Xamarin.Forms
<ContentPage.BindingContext>
  <vm:ItemListViewModel />
</ContentPage.BindingContext>
Uno Platform
<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:

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.

DI

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.

Xamarin.Forms
// Registration (via attribute)
[assembly: Dependency(typeof(PlatformNotificationService))]

// Resolution
var service = DependencyService.Get<INotificationService>();
service.ShowNotification("Hello");
Uno Platform
// 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.

Navigation

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.

OperationXamarin.FormsUno Platform (Frame)
Navigate forwardawait Navigation.PushAsync(new DetailPage())Frame.Navigate(typeof(DetailPage))
Navigate backawait Navigation.PopAsync()Frame.GoBack()
Pass parameternew DetailPage(itemId)Frame.Navigate(typeof(DetailPage), itemId)
Receive parameterConstructor parameterOnNavigatedTo(e) with e.Parameter
Page appearedOnAppearing()OnNavigatedTo()
Page disappearedOnDisappearing()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:

Xamarin.Forms
await Shell.Current.GoToAsync($"details?id={item.Id}");
Uno Platform
await navigator.NavigateRouteAsync(this, $"details?id={item.Id}");
Page by Page

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.

Xamarin.Forms
<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>
Uno Platform
<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

ChangeWhat Happened
ContentPagePageRoot element swap
BindingContextDataContextProperty name change
StackLayoutStackPanelControl name swap
LabelTextBlockControl name swap
FontAttributes="Bold"FontWeight="Bold"Property name change
TextColorForegroundProperty name change
EntryTextBoxControl name swap
PlaceholderPlaceholderTextProperty name change
Keyboard="Telephone"InputScope="TelephoneNumber"Input type mechanism change
Button TextButton ContentProperty name change
BackgroundColorBackgroundProperty name change
Platform Code

7. Platform-Specific Code

Replacing Device.RuntimePlatform

Xamarin.Forms
if (Device.RuntimePlatform == Device.Android) { /* ... */ }
else if (Device.RuntimePlatform == Device.iOS) { /* ... */ }

Approach 1: Conditional Compilation

Uno Platform
#if __ANDROID__
    // Android-specific logic
#elif __IOS__
    // iOS-specific logic
#elif __WASM__
    // WebAssembly-specific logic
#elif HAS_UNO_SKIA
    // Desktop (Skia) specific logic
#endif

For 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

XAML
<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:

C#
if (OperatingSystem.IsAndroid()) { /* ... */ }
else if (OperatingSystem.IsIOS()) { /* ... */ }
else if (OperatingSystem.IsBrowser()) { /* ... */ }
Verify

8. Run, Connect App MCP, and Verify

Build and Run

Terminal
# 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-android

Verify with AI Agent and App MCP

If you're using an AI coding agent with the Uno Platform App MCP connected:

  1. Screenshot the running app. The agent captures a screenshot and compares it against the original Xamarin.Forms version.
  2. 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.
  3. Check for binding errors. The agent scans for debug output lines like Error: BindingExpression path error.
  4. 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

Checklist for Each Migrated Page

Root element changed from ContentPage to Page
XAML namespaces updated to WinUI URIs
All control names translated (Label → TextBlock, Entry → TextBox, etc.)
Property names updated (TextColor → Foreground, BackgroundColor → Background, etc.)
BindingContext replaced with DataContext
StringFormat bindings replaced with converters or x:Bind functions
ViewCell wrappers removed from list templates
Grid definitions converted to explicit RowDefinition/ColumnDefinition elements
Navigation calls updated from PushAsync/PopAsync to Frame.Navigate/Frame.GoBack
Platform-specific code converted from Device.RuntimePlatform to #if directives or XAML prefixes
Page builds without errors on all target platforms
Page renders correctly on mobile and WebAssembly
Bindings resolve without errors in debug output
FAQ

FAQ

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.