.NET MAUI
Trigger On
- working on cross-platform mobile or desktop UI in .NET MAUI
- integrating device capabilities, navigation, or platform-specific code
- migrating Xamarin.Forms or aligning a shared codebase across targets
- implementing MVVM patterns in mobile apps
Documentation
References
- patterns.md - Shell navigation, platform-specific code, messaging, lifecycle, data binding, and CollectionView patterns
- anti-patterns.md - Common MAUI mistakes and how to avoid them
Platform Targets
| Platform |
Build Host |
Notes |
| Android |
Windows/Mac |
Emulator or device |
| iOS |
Mac only |
Requires Xcode |
| macOS |
Mac only |
Catalyst |
| Windows |
Windows |
WinUI 3 |
Workflow
- Confirm target platforms — behavior differs across Android, iOS, Mac, Windows
- Separate shared UI and platform code — use handlers and DI
- Follow MVVM pattern — keep views dumb, logic in ViewModels
- Handle lifecycle and permissions — platform contracts need testing
- Test on real devices — emulators don't catch everything
Current Upstream Notes
.NET MAUI 10.0.100 is a broad quality release for the 10.0 line. It fixes Android WebView gestures inside SwipeView, RenderThread crashes and synthetic about:blank history; iOS WebView file/reload behavior; Shell regressions; XAML source-generation AOT paths; adaptive-trigger leaks; shared/custom Platforms mappings; status-bar contrast, window metrics, and SafeArea behavior.
- After upgrading MAUI packages, smoke-test grouped and virtualized
CollectionView flows, Shell/modal/back navigation, tabs, keyboard and SafeArea interactions, maps, WebView/HybridWebView lifecycle, memory retention, and accessibility narration on every shipped target.
- The August 2026
.NET MAUI Learn overview for net-maui-10.0 still frames the platform around a shared single-project app, native API access, handlers, and optional Blazor Hybrid UI. Verify each target platform rather than treating shared code as identical runtime behavior.
Project Structure
MyApp/
├── MyApp/ # Shared code
│ ├── App.xaml # Application entry
│ ├── MauiProgram.cs # DI and configuration
│ ├── Views/ # XAML pages
│ ├── ViewModels/ # MVVM ViewModels
│ ├── Models/ # Domain models
│ ├── Services/ # Business logic
│ └── Platforms/ # Platform-specific code
│ ├── Android/
│ ├── iOS/
│ ├── MacCatalyst/
│ └── Windows/
└── MyApp.Tests/
MVVM Pattern
ViewModel with MVVM Toolkit
public partial class ProductsViewModel(IProductService productService) : ObservableObject
{
[ObservableProperty]
private ObservableCollection<Product> _products = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))]
private bool _isLoading;
[RelayCommand(CanExecute = nameof(CanLoadProducts))]
private async Task LoadProductsAsync()
{
IsLoading = true;
try
{
var items = await productService.GetAllAsync();
Products = new ObservableCollection<Product>(items);
}
finally
{
IsLoading = false;
}
}
private bool CanLoadProducts() => !IsLoading;
}
View Binding
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Views.ProductsPage"
x:DataType="vm:ProductsViewModel">
<RefreshView Command="{Binding LoadProductsCommand}"
IsRefreshing="{Binding IsLoading}">
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<VerticalStackLayout Padding="10">
<Label Text="{Binding Name}" FontSize="18" />
<Label Text="{Binding Price, StringFormat='{0:C}'}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
</ContentPage>
Dependency Injection
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Services
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddSingleton<INavigationService, NavigationService>();
// ViewModels
builder.Services.AddTransient<ProductsViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
// Pages
builder.Services.AddTransient<ProductsPage>();
builder.Services.AddTransient<ProductDetailPage>();
return builder.Build();
}
}
Navigation
Shell Navigation
// Register routes
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
// Navigate with parameters
await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}?id={product.Id}");
// Receive parameters
[QueryProperty(nameof(ProductId), "id")]
public partial class ProductDetailViewModel : ObservableObject
{
[ObservableProperty]
private string _productId;
partial void OnProductIdChanged(string value)
{
LoadProduct(value);
}
}
Navigation Service
public interface INavigationService
{
Task NavigateToAsync<TViewModel>(object? parameter = null);
Task GoBackAsync();
}
public class NavigationService : INavigationService
{
public async Task NavigateToAsync<TViewModel>(object? parameter = null)
{
var route = typeof(TViewModel).Name.Replace("ViewModel", "Page");
var query = parameter is null ? "" : $"?id={parameter}";
await Shell.Current.GoToAsync($"{route}{query}");
}
public Task GoBackAsync() => Shell.Current.GoToAsync("..");
}
Platform-Specific Code
Using Partial Classes
// Services/DeviceService.cs (shared)
public partial class DeviceService
{
public partial string GetDeviceId();
}
// Platforms/Android/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceId()
{
return Android.Provider.Settings.Secure.GetString(
Android.App.Application.Context.ContentResolver,
Android.Provider.Settings.Secure.AndroidId);
}
}
// Platforms/iOS/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceId()
{
return UIKit.UIDevice.CurrentDevice.IdentifierForVendor?.ToString() ?? "";
}
}
Conditional Compilation
public string GetPlatformInfo()
{
#if ANDROID
return $"Android {Android.OS.Build.VERSION.Release}";
#elif IOS
return $"iOS {UIKit.UIDevice.CurrentDevice.SystemVersion}";
#elif MACCATALYST
return "macOS Catalyst";
#elif WINDOWS
return "Windows";
#else
return "Unknown";
#endif
}
Anti-Patterns to Avoid
| Anti-Pattern |
Why It's Bad |
Better Approach |
| God ViewModel |
Unmaintainable |
Split into focused ViewModels |
| Logic in code-behind |
Hard to test |
Use MVVM and commands |
| Platform code everywhere |
Defeats cross-platform |
Use handlers/DI |
| Direct service calls in Views |
Tight coupling |
Use ViewModel |
| Ignoring lifecycle |
Crashes, leaks |
Handle lifecycle events |
Performance Best Practices
Use compiled bindings:
<ContentPage x:DataType="vm:ProductsViewModel">
Virtualize long lists:
<CollectionView ItemsSource="{Binding Items}"
ItemSizingStrategy="MeasureFirstItem" />
Optimize images:
var image = ImageSource.FromFile("image.png");
// Use appropriate resolution for platform
Avoid synchronous work on UI thread:
// Bad
var data = service.GetData(); // Blocks UI
// Good
var data = await service.GetDataAsync();
Testing
[Fact]
public async Task LoadProducts_UpdatesCollection()
{
var mockService = new Mock<IProductService>();
mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync(new[] { new Product { Name = "Test" } });
var viewModel = new ProductsViewModel(mockService.Object);
await viewModel.LoadProductsCommand.ExecuteAsync(null);
Assert.Single(viewModel.Products);
Assert.Equal("Test", viewModel.Products[0].Name);
}
Deliver
- shared MAUI code with explicit platform seams
- MVVM pattern with testable ViewModels
- navigation and lifecycle behavior that fits each target
- a realistic build and deployment path for the chosen platforms
Validate
- cross-platform reuse is real, not superficial
- platform-specific behavior is isolated and testable
- MVVM pattern is followed consistently
- build assumptions for Mac/iOS and Windows are explicit
- performance is acceptable on target devices
1---2name: maui3description: Build, review, or migrate .NET MAUI applications across Android, iOS, macOS, and Windows with correct cross-platform UI, platform integration, and native packaging assumptions. USE FOR: working on cross-platform mobile or desktop UI in .NET MAUI; integrating device capabilities, navigation, or platform-specific code; migrating Xamarin.Forms or aligning. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or validation commands when changes are made.4---56# .NET MAUI78## Trigger On910- working on cross-platform mobile or desktop UI in .NET MAUI11- integrating device capabilities, navigation, or platform-specific code12- migrating Xamarin.Forms or aligning a shared codebase across targets13- implementing MVVM patterns in mobile apps1415## Documentation1617- [.NET MAUI Overview](https://learn.microsoft.com/en-us/dotnet/maui/what-is-maui)18- [Enterprise Patterns](https://learn.microsoft.com/en-us/dotnet/architecture/maui/)19- [MVVM Pattern](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm)20- [Controls Reference](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/)21- [Platform Integration](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/)2223### References2425- [patterns.md](references/patterns.md) - Shell navigation, platform-specific code, messaging, lifecycle, data binding, and CollectionView patterns26- [anti-patterns.md](references/anti-patterns.md) - Common MAUI mistakes and how to avoid them2728## Platform Targets2930| Platform | Build Host | Notes |31|----------|------------|-------|32| Android | Windows/Mac | Emulator or device |33| iOS | Mac only | Requires Xcode |34| macOS | Mac only | Catalyst |35| Windows | Windows | WinUI 3 |3637## Workflow38391. **Confirm target platforms** — behavior differs across Android, iOS, Mac, Windows402. **Separate shared UI and platform code** — use handlers and DI413. **Follow MVVM pattern** — keep views dumb, logic in ViewModels424. **Handle lifecycle and permissions** — platform contracts need testing435. **Test on real devices** — emulators don't catch everything4445## Current Upstream Notes4647- `.NET MAUI` `10.0.100` is a broad quality release for the 10.0 line. It fixes Android WebView gestures inside `SwipeView`, RenderThread crashes and synthetic `about:blank` history; iOS WebView file/reload behavior; Shell regressions; XAML source-generation AOT paths; adaptive-trigger leaks; shared/custom `Platforms` mappings; status-bar contrast, window metrics, and SafeArea behavior.48- After upgrading MAUI packages, smoke-test grouped and virtualized `CollectionView` flows, Shell/modal/back navigation, tabs, keyboard and SafeArea interactions, maps, WebView/HybridWebView lifecycle, memory retention, and accessibility narration on every shipped target.49- The August 2026 `.NET MAUI` Learn overview for `net-maui-10.0` still frames the platform around a shared single-project app, native API access, handlers, and optional Blazor Hybrid UI. Verify each target platform rather than treating shared code as identical runtime behavior.5051## Project Structure5253```54MyApp/55├── MyApp/ # Shared code56│ ├── App.xaml # Application entry57│ ├── MauiProgram.cs # DI and configuration58│ ├── Views/ # XAML pages59│ ├── ViewModels/ # MVVM ViewModels60│ ├── Models/ # Domain models61│ ├── Services/ # Business logic62│ └── Platforms/ # Platform-specific code63│ ├── Android/64│ ├── iOS/65│ ├── MacCatalyst/66│ └── Windows/67└── MyApp.Tests/68```6970## MVVM Pattern7172### ViewModel with MVVM Toolkit73```csharp74public partial class ProductsViewModel(IProductService productService) : ObservableObject75{76 [ObservableProperty]77 private ObservableCollection<Product> _products = [];7879 [ObservableProperty]80 [NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))]81 private bool _isLoading;8283 [RelayCommand(CanExecute = nameof(CanLoadProducts))]84 private async Task LoadProductsAsync()85 {86 IsLoading = true;87 try88 {89 var items = await productService.GetAllAsync();90 Products = new ObservableCollection<Product>(items);91 }92 finally93 {94 IsLoading = false;95 }96 }9798 private bool CanLoadProducts() => !IsLoading;99}100```101102### View Binding103```xml104<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"105 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"106 xmlns:vm="clr-namespace:MyApp.ViewModels"107 x:Class="MyApp.Views.ProductsPage"108 x:DataType="vm:ProductsViewModel">109110 <RefreshView Command="{Binding LoadProductsCommand}"111 IsRefreshing="{Binding IsLoading}">112 <CollectionView ItemsSource="{Binding Products}">113 <CollectionView.ItemTemplate>114 <DataTemplate x:DataType="models:Product">115 <VerticalStackLayout Padding="10">116 <Label Text="{Binding Name}" FontSize="18" />117 <Label Text="{Binding Price, StringFormat='{0:C}'}" />118 </VerticalStackLayout>119 </DataTemplate>120 </CollectionView.ItemTemplate>121 </CollectionView>122 </RefreshView>123</ContentPage>124```125126## Dependency Injection127128```csharp129public static class MauiProgram130{131 public static MauiApp CreateMauiApp()132 {133 var builder = MauiApp.CreateBuilder();134 builder135 .UseMauiApp<App>()136 .ConfigureFonts(fonts =>137 {138 fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");139 });140141 // Services142 builder.Services.AddSingleton<IProductService, ProductService>();143 builder.Services.AddSingleton<INavigationService, NavigationService>();144145 // ViewModels146 builder.Services.AddTransient<ProductsViewModel>();147 builder.Services.AddTransient<ProductDetailViewModel>();148149 // Pages150 builder.Services.AddTransient<ProductsPage>();151 builder.Services.AddTransient<ProductDetailPage>();152153 return builder.Build();154 }155}156```157158## Navigation159160### Shell Navigation161```csharp162// Register routes163Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));164165// Navigate with parameters166await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}?id={product.Id}");167168// Receive parameters169[QueryProperty(nameof(ProductId), "id")]170public partial class ProductDetailViewModel : ObservableObject171{172 [ObservableProperty]173 private string _productId;174175 partial void OnProductIdChanged(string value)176 {177 LoadProduct(value);178 }179}180```181182### Navigation Service183```csharp184public interface INavigationService185{186 Task NavigateToAsync<TViewModel>(object? parameter = null);187 Task GoBackAsync();188}189190public class NavigationService : INavigationService191{192 public async Task NavigateToAsync<TViewModel>(object? parameter = null)193 {194 var route = typeof(TViewModel).Name.Replace("ViewModel", "Page");195 var query = parameter is null ? "" : $"?id={parameter}";196 await Shell.Current.GoToAsync($"{route}{query}");197 }198199 public Task GoBackAsync() => Shell.Current.GoToAsync("..");200}201```202203## Platform-Specific Code204205### Using Partial Classes206```csharp207// Services/DeviceService.cs (shared)208public partial class DeviceService209{210 public partial string GetDeviceId();211}212213// Platforms/Android/DeviceService.cs214public partial class DeviceService215{216 public partial string GetDeviceId()217 {218 return Android.Provider.Settings.Secure.GetString(219 Android.App.Application.Context.ContentResolver,220 Android.Provider.Settings.Secure.AndroidId);221 }222}223224// Platforms/iOS/DeviceService.cs225public partial class DeviceService226{227 public partial string GetDeviceId()228 {229 return UIKit.UIDevice.CurrentDevice.IdentifierForVendor?.ToString() ?? "";230 }231}232```233234### Conditional Compilation235```csharp236public string GetPlatformInfo()237{238#if ANDROID239 return $"Android {Android.OS.Build.VERSION.Release}";240#elif IOS241 return $"iOS {UIKit.UIDevice.CurrentDevice.SystemVersion}";242#elif MACCATALYST243 return "macOS Catalyst";244#elif WINDOWS245 return "Windows";246#else247 return "Unknown";248#endif249}250```251252## Anti-Patterns to Avoid253254| Anti-Pattern | Why It's Bad | Better Approach |255|--------------|--------------|-----------------|256| God ViewModel | Unmaintainable | Split into focused ViewModels |257| Logic in code-behind | Hard to test | Use MVVM and commands |258| Platform code everywhere | Defeats cross-platform | Use handlers/DI |259| Direct service calls in Views | Tight coupling | Use ViewModel |260| Ignoring lifecycle | Crashes, leaks | Handle lifecycle events |261262## Performance Best Practices2632641. **Use compiled bindings:**265 ```xml266 <ContentPage x:DataType="vm:ProductsViewModel">267 ```2682692. **Virtualize long lists:**270 ```xml271 <CollectionView ItemsSource="{Binding Items}"272 ItemSizingStrategy="MeasureFirstItem" />273 ```2742753. **Optimize images:**276 ```csharp277 var image = ImageSource.FromFile("image.png");278 // Use appropriate resolution for platform279 ```2802814. **Avoid synchronous work on UI thread:**282 ```csharp283 // Bad284 var data = service.GetData(); // Blocks UI285286 // Good287 var data = await service.GetDataAsync();288 ```289290## Testing291292```csharp293[Fact]294public async Task LoadProducts_UpdatesCollection()295{296 var mockService = new Mock<IProductService>();297 mockService.Setup(s => s.GetAllAsync())298 .ReturnsAsync(new[] { new Product { Name = "Test" } });299300 var viewModel = new ProductsViewModel(mockService.Object);301302 await viewModel.LoadProductsCommand.ExecuteAsync(null);303304 Assert.Single(viewModel.Products);305 Assert.Equal("Test", viewModel.Products[0].Name);306}307```308309## Deliver310311- shared MAUI code with explicit platform seams312- MVVM pattern with testable ViewModels313- navigation and lifecycle behavior that fits each target314- a realistic build and deployment path for the chosen platforms315316## Validate317318- cross-platform reuse is real, not superficial319- platform-specific behavior is isolated and testable320- MVVM pattern is followed consistently321- build assumptions for Mac/iOS and Windows are explicit322- performance is acceptable on target devices