.NET MAUI (.NET Multi-platform App UI) is Microsoft's framework for building native Android, iOS, macOS and Windows apps from one C# and XAML codebase. It suits teams that already know .NET and want to ship mobile and desktop clients without maintaining separate Kotlin, Swift and WinUI projects. This guide explains how .NET MAUI works under the hood, how to structure an app with MVVM and Shell, how to reach platform APIs, how to make it fast, and what changed in .NET 10 and the upcoming .NET 11.
What Is .NET MAUI?#
.NET MAUI is the successor to Xamarin.Forms. It keeps the same idea, a cross-platform UI abstraction that renders real native controls, but rebuilds it on modern .NET with a single project, a new handler architecture and first-class dependency injection. Your code compiles against platform-specific target frameworks such as net10.0-android and net10.0-ios, so every app is a genuine native app that can call any platform API.
A Button in .NET MAUI becomes a MaterialButton on Android, a UIButton on iOS and Mac Catalyst, and a WinUI Button on Windows. That design gives you platform-appropriate behavior, accessibility and text input for free. The trade-off is that pixel-identical rendering across platforms is not a goal; if you need that, look at drawn-UI frameworks such as Avalonia or Uno Platform's Skia renderer, compared in our cross-platform UI comparison.
Supported targets for .NET MAUI 10 are Android 5.0 (API 21) or later, iOS 12.2 or later, macOS 12 or later through Mac Catalyst, and Windows 10 version 1809 or later through WinUI 3. Samsung maintains Tizen support. Linux and the browser are not supported targets.
How .NET MAUI Works: Single Project and Handlers#
One project, many target frameworks#
A .NET MAUI app is one project that multi-targets. Shared code lives at the root, and each platform has a folder under Platforms/ for its entry point, manifest and platform-only code. Files in Platforms/Android compile only for the Android target, which removes most of the #if clutter older Xamarin apps needed. Images, fonts, the app icon and the splash screen are declared once as MSBuild items, and the build generates every density and format each platform expects.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">
$(TargetFrameworks);net10.0-windows10.0.19041.0
</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationTitle>Field Tasks</ApplicationTitle>
<ApplicationId>com.contoso.fieldtasks</ApplicationId>
<ApplicationDisplayVersion>1.4.0</ApplicationDisplayVersion>
<ApplicationVersion>14</ApplicationVersion>
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
<ItemGroup>
<MauiIcon Include="Resources\AppIcon\appicon.svg"
ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<MauiImage Include="Resources\Images\*" />
<MauiFont Include="Resources\Fonts\*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="CommunityToolkit.Maui" Version="15.0.1" />
</ItemGroup>
</Project>Handlers and property mappers#
Xamarin.Forms used renderers: heavyweight classes that wrapped each native control and were hard to customize. .NET MAUI replaces them with handlers. A handler maps a cross-platform interface such as IEntry to a native view through a property mapper, a dictionary of small functions that each push one property to the native control. Because mappers are public, you can append, prepend or replace a mapping for every instance of a control without subclassing anything.
Each handler exposes the native control through PlatformView and the cross-platform element through VirtualView. Since .NET 9, handlers disconnect automatically when a page is popped, so native event subscriptions are released unless you set HandlerProperties.DisconnectPolicy to Manual.
Runtimes per platform#
.NET MAUI 10 runs on Mono on Android, iOS and Mac Catalyst, and on CoreCLR on Windows. iOS forbids JIT compilation, so Apple builds are ahead-of-time compiled, and Native AOT is an opt-in for iOS and Mac Catalyst since .NET 9. .NET 11 makes CoreCLR the default runtime on Android and Apple platforms as well, with Native AOT as an opt-in publishing mode, which unifies diagnostics and runtime behavior across every target.
Getting Started: Bootstrapping with MauiProgram#
Install the workload with dotnet workload install maui, then create a project with dotnet new maui -n FieldTasks. Adding --sample-content generates a richer starter app that already uses the MVVM Toolkit and a SQLite store. Everything starts in MauiProgram.CreateMauiApp, which configures a MauiAppBuilder that works like the .NET generic host: services, logging and configuration all use the familiar Microsoft.Extensions APIs.
using CommunityToolkit.Maui;
using FieldTasks.Services;
using FieldTasks.ViewModels;
using FieldTasks.Views;
using Microsoft.Extensions.Logging;
namespace FieldTasks;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts => fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"));
builder.Services.AddSingleton<IGeolocation>(Geolocation.Default);
builder.Services.AddSingleton<ITaskStore, SqliteTaskStore>();
builder.Services.AddSingleton<INavigationService, ShellNavigationService>();
builder.Services.AddSingleton<LocationService>();
builder.Services.AddTransient<TaskListViewModel>();
builder.Services.AddTransient<TaskListPage>();
builder.Services.AddTransient<TaskDetailViewModel>();
builder.Services.AddTransient<TaskDetailPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}Register platform services through their interfaces, as with IGeolocation here, rather than calling static Default properties from view models. The interface keeps view models testable. For container lifetimes and pitfalls such as captive dependencies, see the dependency injection guide.
Building the UI with XAML and C# Markup#
XAML with compiled bindings#
XAML remains the default way to describe .NET MAUI pages. The most important habit is setting x:DataType on every element where the binding context changes. With it, the XAML compiler turns each binding into strongly typed code that resolves roughly 8 times faster than a reflection-based binding (about 20 times for one-time bindings), and a typo in a property path becomes a build error instead of a silent blank label.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldTasks.ViewModels"
xmlns:models="clr-namespace:FieldTasks.Models"
x:Class="FieldTasks.Views.TaskListPage"
x:DataType="vm:TaskListViewModel"
Title="Open tasks">
<Grid RowDefinitions="Auto,*" Padding="16" RowSpacing="12">
<HorizontalStackLayout Spacing="8">
<Entry Text="{Binding NewTitle}" Placeholder="New task" WidthRequest="240" />
<Button Text="Add" Command="{Binding AddTaskCommand}" />
</HorizontalStackLayout>
<RefreshView Grid.Row="1"
Command="{Binding LoadCommand}"
IsRefreshing="{Binding IsRefreshing}">
<CollectionView ItemsSource="{Binding Tasks}" SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:TaskItem">
<Border Padding="12" StrokeShape="RoundRectangle 8">
<Border.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource
AncestorType={x:Type vm:TaskListViewModel}},
Path=OpenTaskCommand}"
CommandParameter="{Binding .}" />
</Border.GestureRecognizers>
<Label Text="{Binding Title}" FontSize="16" />
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
</Grid>
</ContentPage>.NET 10 adds an opt-in XAML source generator, enabled with the MauiXamlInflator property set to SourceGen, which emits C# for each XAML file at build time and improves tooling. It also introduces a global XML namespace, http://schemas.microsoft.com/dotnet/maui/global, that aggregates your own CLR namespaces through assembly-level XmlnsDefinition attributes, so pages no longer need a stack of xmlns: prefixes.
C# Markup#
Some teams prefer to build UI entirely in C#. Plain C# works, but the CommunityToolkit.Maui.Markup package adds fluent extension methods and lambda-based bindings that keep the code readable. Call UseMauiCommunityToolkitMarkup() on the builder, then write pages like this:
using CommunityToolkit.Maui.Markup;
using FieldTasks.Models;
using FieldTasks.ViewModels;
using static CommunityToolkit.Maui.Markup.GridRowsColumns;
namespace FieldTasks.Views;
public sealed class QuickAddPage : ContentPage
{
public QuickAddPage(TaskListViewModel viewModel)
{
BindingContext = viewModel;
Content = new Grid
{
RowDefinitions = Rows.Define(Auto, Star),
Padding = 16,
RowSpacing = 12,
Children =
{
new HorizontalStackLayout
{
Spacing = 8,
Children =
{
new Entry { WidthRequest = 240 }
.Placeholder("New task")
.Bind(Entry.TextProperty,
static (TaskListViewModel vm) => vm.NewTitle,
static (TaskListViewModel vm, string text) => vm.NewTitle = text),
new Button()
.Text("Add")
.BindCommand(static (TaskListViewModel vm) => vm.AddTaskCommand)
}
}.Row(0),
new CollectionView
{
ItemTemplate = new DataTemplate(() =>
new Label { Padding = 12 }
.Bind(Label.TextProperty, static (TaskItem item) => item.Title))
}
.Bind(ItemsView.ItemsSourceProperty, static (TaskListViewModel vm) => vm.Tasks)
.Row(1)
}
};
}
}Lambda bindings are compiled bindings, so this style is trim-safe by construction. XAML still has the better designer story, Hot Reload and a larger pool of samples, so choose per team preference rather than per page.
MVVM with CommunityToolkit.Mvvm#
The Model-View-ViewModel pattern fits .NET MAUI's binding engine naturally, and CommunityToolkit.Mvvm removes nearly all of its boilerplate through source generators. Since toolkit 8.4, [ObservableProperty] can annotate partial properties instead of fields. The generator relies on the C# 14 field keyword, so it needs C# 14, which is the default for net10.0 targets; toolkit 8.4.1 and later support it without preview language settings.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FieldTasks.Models;
using FieldTasks.Services;
namespace FieldTasks.ViewModels;
public partial class TaskListViewModel(ITaskStore store, INavigationService navigation)
: ObservableObject
{
public ObservableCollection<TaskItem> Tasks { get; } = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(AddTaskCommand))]
public partial string NewTitle { get; set; } = string.Empty;
[ObservableProperty]
public partial bool IsRefreshing { get; set; }
[RelayCommand]
private async Task LoadAsync(CancellationToken cancellationToken)
{
try
{
var items = await store.GetOpenTasksAsync(cancellationToken);
Tasks.Clear();
foreach (var item in items)
{
Tasks.Add(item);
}
}
finally
{
IsRefreshing = false;
}
}
[RelayCommand(CanExecute = nameof(CanAddTask))]
private async Task AddTaskAsync(CancellationToken cancellationToken)
{
var item = await store.AddAsync(NewTitle.Trim(), cancellationToken);
Tasks.Insert(0, item);
NewTitle = string.Empty;
}
private bool CanAddTask() => !string.IsNullOrWhiteSpace(NewTitle);
[RelayCommand]
private Task OpenTaskAsync(TaskItem item) =>
navigation.GoToAsync("taskdetail", "Task", item);
}The generator strips the Async suffix, so LoadAsync becomes LoadCommand. Async commands disable themselves while running by default, which prevents double taps from starting duplicate saves, and a CancellationToken parameter makes the command cancelable. For cross-view-model communication, use WeakReferenceMessenger from the same package: .NET 10 made the old MessagingCenter internal.
Performance: Compiled Bindings, Trimming and Startup#
Mobile users notice startup time and app size, and .NET MAUI gives you several levers to improve both.
- Compiled bindings everywhere. Since .NET 9 the XAML compiler warns (
XC0022) about bindings withoutx:DataType. Treat those warnings as errors in CI. In code, useSetBindingwith a lambda, such aslabel.SetBinding(Label.TextProperty, static (TaskItem t) => t.Title), instead of string paths. - Trimming. Release builds on Android and Mac Catalyst, and all iOS device builds, use partial trimming by default. Setting
TrimModetofullalso trims your own code and dependencies, which requires trim-safe code and zero warnings. - Native AOT on Apple platforms. Setting
PublishAotfor iOS and Mac Catalyst produces packages that Microsoft measured as typically up to 2.5 times smaller, with up to 2 times faster startup on iOS devices for the default template. - Lean startup. Keep the application-level resource dictionary small, defer work with lazy initialization, avoid deep layout nesting, prefer
Gridover nested stacks, and use Shell so pages are created on demand.
<PropertyGroup>
<MauiXamlInflator>SourceGen</MauiXamlInflator>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
<WarningsAsErrors>$(WarningsAsErrors);XC0022;XC0023;XC0025</WarningsAsErrors>
<!-- Run trimming and AOT analyzers for every target -->
<IsAotCompatible>true</IsAotCompatible>
<PublishAot Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
true
</PublishAot>
</PropertyGroup>Do not condition PublishAot or TrimMode on the build configuration: feature switches depend on them, and Debug and Release should behave the same. Also note that dotnet build -t:Publish is not equivalent to dotnet publish when you validate AOT warnings. For the underlying mechanics, read Native AOT and trimming in .NET.
.NET 10 also instruments layout. The Microsoft.Maui ActivitySource and meter record measure and arrange counts and durations (maui.layout.measure_duration and related metrics), so an OpenTelemetry exporter or dotnet-counters can reveal pages with runaway layout passes.
What's New in .NET MAUI 10 (and What .NET 11 Brings)#
Microsoft described the .NET 10 release of .NET MAUI as focused on product quality, but it contains several changes worth planning for:
- XAML: the opt-in source generator (
MauiXamlInflator=SourceGen) and global XML namespaces, with an opt-in implicit default namespace. - .NET Aspire integration: a template for a service defaults project, so
builder.AddServiceDefaults()wires OpenTelemetry and service discovery into the app, which is handy when the client talks to an Aspire-orchestrated backend. See the .NET Aspire guide. - Controls: the optimized
CollectionViewandCarouselViewhandlers for iOS and Mac Catalyst, optional in .NET 9, became the default.DatePicker.DateandTimePicker.Timeare nullable.SafeAreaEdgesgives precise control over notch, bar and keyboard insets. - Deprecations:
ListView,TableViewand the cell types are deprecated in favor ofCollectionView.MessagingCenteris internal.DisplayAlertand the animation methods such asFadeTogive way toDisplayAlertAsyncandFadeToAsync, andPage.IsBusyis obsolete. - Android: API 36 is the default for
net10.0-android, JDK 21 is supported, marshal methods are on by default for faster startup,dotnet runcan target a device or emulator, and templates now default to a minimum of API 24. - iOS and Mac Catalyst: trimmer warnings are enabled by default, and the trimmer runs in more simulator and Mac Catalyst configurations.
- Web content:
HybridWebViewandBlazorWebViewcan intercept web requests throughWebResourceRequested.
.NET 11 reached Release Candidate 1 in September 2026 and is due in November. Its .NET MAUI release makes CoreCLR the default runtime on Android and Apple platforms, raises the Android minimum to API 24, enables implicit XAML namespaces by default, stops shipping the Microsoft.Maui.Controls.Compatibility package, and adds device test templates, Shell route templates, a passkeys API and incremental XAML Hot Reload that works with dotnet watch.
Best Practices#
- Target the current major version. Apple and Google tooling moves quickly, and only the latest servicing release of a supported version gets fixes.
- Put
x:DataTypeeverywhere and fail the build onXC0022,XC0023andXC0025. - Use partial properties with the MVVM Toolkit so other generators and analyzers see the generated members.
- Keep view models platform-agnostic. Wrap navigation, dialogs and device APIs behind interfaces.
- Prefer
CollectionView,Borderand Shell over the deprecatedListView,FrameandMessagingCenter. - Test on real devices, especially low-end Android hardware, and measure startup in Release builds only.
- Set
AutomationIdand semantic properties for accessibility and UI automation at the same time.
Common Pitfalls#
- Measuring Debug builds. Debug builds use the interpreter or JIT and skip trimming; their startup numbers mean little.
- Blocking the UI thread. Calling
.Resultor.Wait()on the main thread freezes the app and can deadlock. Stay async end to end. - Updating UI from background threads. Marshal back with
MainThread.BeginInvokeOnMainThreadorMainThread.InvokeOnMainThreadAsyncafter work on a background thread. - Relying on
QueryPropertywith full trimming. It uses reflection; implementIQueryAttributableinstead. - Ignoring trim warnings. A single trim or AOT warning means the published app may fail at runtime.
- Assuming LTS support. Staying on .NET MAUI 10 until 2028 is not an option under the MAUI support policy.
When to Use .NET MAUI#
| Scenario | .NET MAUI fit | Notes |
|---|---|---|
| Line-of-business app for iOS and Android built by a C# team | Strong | Shared view models, services and most UI code |
| Consumer app that must feel native on each platform | Good | Native controls; budget for per-platform polish |
| Windows and macOS desktop app | Good | WinUI 3 on Windows, Mac Catalyst on macOS |
| Linux desktop or browser targets | Poor | Not supported; consider Avalonia, Uno Platform or Blazor |
| Pixel-identical custom design system | Weak | Drawn-UI frameworks give tighter control |
| Existing Blazor or web UI to reuse in a native shell | Strong | Host it with Blazor Hybrid in a BlazorWebView |
| Windows-only desktop app with heavy data grids | Weak | WPF or WinUI 3 have richer desktop ecosystems |
Frequently Asked Questions#
Is .NET MAUI production-ready in 2026?#
Yes. .NET MAUI 10 is a mature release, and Microsoft's focus since .NET 9 has been quality, performance and removing legacy APIs rather than new abstractions. Large enterprise apps ship on it, provided teams keep up with the yearly upgrade cadence and test on real devices.
Does .NET MAUI support Linux or WebAssembly?#
No. Official targets are Android, iOS, macOS through Mac Catalyst and Windows, with Tizen support provided by Samsung. For Linux desktops or the browser, consider Avalonia UI, Uno Platform or Blazor, and share view models and services with the .NET MAUI app.
Should I write .NET MAUI UI in XAML or C#?#
Both are first-class. XAML offers Hot Reload, the widest sample base and, from .NET 10, source-generated inflation. C# Markup suits teams that prefer refactoring tools and compile-time checking everywhere. Whichever you choose, use compiled bindings.
How long is .NET MAUI 10 supported?#
Until May 11, 2027. A .NET MAUI major version is supported for at least six months after the next major version ships, so .NET MAUI 10 support ends long before .NET 10 LTS support ends in November 2028. Upgrade to .NET MAUI 11 after it ships in November 2026.
How do I migrate from Xamarin.Forms to .NET MAUI?#
Move to SDK-style projects and a single multi-targeted project, update namespaces, convert custom renderers to handlers or mapper customizations, and replace Xamarin.Essentials calls with the built-in platform APIs. Remove any dependency on the Compatibility package before moving to .NET 11, because it no longer ships there.
Summary#
- .NET MAUI renders native controls on Android, iOS, macOS and Windows from one C# project, using handlers and property mappers instead of renderers.
- Use MVVM with CommunityToolkit.Mvvm partial properties, Shell navigation with
IQueryAttributable, and interfaces around platform APIs. - Compiled bindings, trimming, Native AOT on Apple platforms and lean startup work are the main performance levers.
- .NET 10 brought XAML source generation, Aspire integration, new default handlers and many deprecations; .NET 11 moves mobile apps to CoreCLR.
- The support window is short, so plan a yearly upgrade.