
Maui
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
maui is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- maui
- AI & Agent Building
- AI-coding skill
Maui by the numbers
- 17 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,886 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill mauiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
.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
1. Confirm target platforms — behavior differs across Android, iOS, Mac, Windows 2. Separate shared UI and platform code — use handlers and DI 3. Follow MVVM pattern — keep views dumb, logic in ViewModels 4. Handle lifecycle and permissions — platform contracts need testing 5. Test on real devices — emulators don't catch everything
Current Upstream Notes
.NET MAUI10.0.71is a servicing release for the 10.0 line. It includes fixes around HybridWebView/WebView rendering, modal navigation and tab behavior, SafeArea listeners in recycler items, MapPool retention, and platform-specific navigation regressions.- After upgrading MAUI packages, smoke-test Shell modal navigation, tabs, keyboard interactions, SafeArea layout, maps, WebView/HybridWebView, and accessibility narration on the target platforms.
- The current
.NET MAUILearn overview fornet-maui-10.0remains the source for supported platforms, single-project structure, native API access, Blazor Hybrid, and migration positioning.
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
1. Use compiled bindings:
<ContentPage x:DataType="vm:ProductsViewModel">2. Virtualize long lists:
<CollectionView ItemsSource="{Binding Items}"
ItemSizingStrategy="MeasureFirstItem" />3. Optimize images:
var image = ImageSource.FromFile("image.png");
// Use appropriate resolution for platform4. 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
{
"version": "1.0.1",
"category": "Cross-Platform UI",
"package_prefix": "Microsoft.Maui"
}
MAUI Anti-Patterns
Navigation Anti-Patterns
Coupling Views to Navigation
// BAD: Navigation logic in code-behind
public partial class ProductsPage : ContentPage
{
private async void OnProductTapped(object sender, EventArgs e)
{
var product = (sender as View).BindingContext as Product;
await Navigation.PushAsync(new ProductDetailPage(product)); // Direct coupling
}
}
// GOOD: Navigation through ViewModel with service
public partial class ProductsViewModel : ObservableObject
{
private readonly INavigationService _navigation;
[RelayCommand]
private async Task SelectProduct(Product product)
{
await _navigation.NavigateToAsync<ProductDetailViewModel>(product.Id);
}
}Hardcoded Route Strings Everywhere
// BAD: Magic strings scattered across codebase
await Shell.Current.GoToAsync("ProductDetailPage?id=123");
await Shell.Current.GoToAsync("productdetail?id=123"); // Inconsistent casing
await Shell.Current.GoToAsync("product-detail?id=123"); // Different format
// GOOD: Centralized route constants
public static class Routes
{
public const string ProductDetail = nameof(ProductDetailPage);
public const string Checkout = nameof(CheckoutPage);
public const string OrderConfirmation = nameof(OrderConfirmationPage);
}
// Usage
await Shell.Current.GoToAsync($"{Routes.ProductDetail}?id={product.Id}");Navigation State Leaks
// BAD: Not cleaning up when navigating away
public partial class CameraPage : ContentPage
{
private CameraPreview _camera;
protected override void OnAppearing()
{
_camera.Start(); // Camera keeps running when navigating away
}
}
// GOOD: Proper lifecycle management
public partial class CameraPage : ContentPage
{
private CameraPreview _camera;
protected override void OnAppearing()
{
base.OnAppearing();
_camera.Start();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_camera.Stop();
}
}MVVM Anti-Patterns
God ViewModel
// BAD: One ViewModel doing everything
public class MainViewModel : ObservableObject
{
public ObservableCollection<Product> Products { get; }
public ObservableCollection<CartItem> CartItems { get; }
public User CurrentUser { get; }
public ObservableCollection<Order> Orders { get; }
public Settings AppSettings { get; }
// Hundreds of commands and properties for all features
public ICommand LoadProductsCommand { get; }
public ICommand AddToCartCommand { get; }
public ICommand CheckoutCommand { get; }
public ICommand LoginCommand { get; }
public ICommand UpdateSettingsCommand { get; }
// ... 50 more commands
}
// GOOD: Focused ViewModels
public partial class ProductsViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<Product> _products;
[RelayCommand]
private async Task LoadProducts() { }
[RelayCommand]
private async Task SelectProduct(Product product) { }
}Logic in Code-Behind
// BAD: Business logic in code-behind
public partial class CheckoutPage : ContentPage
{
private async void OnCheckoutClicked(object sender, EventArgs e)
{
var total = _items.Sum(i => i.Price * i.Quantity);
if (total > 1000)
{
total *= 0.9; // Apply discount
}
var order = new Order { Total = total, Items = _items };
await _orderService.CreateOrderAsync(order);
await DisplayAlert("Success", "Order placed!", "OK");
await Navigation.PopToRootAsync();
}
}
// GOOD: Logic in ViewModel, code-behind is thin
public partial class CheckoutPage : ContentPage
{
public CheckoutPage(CheckoutViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
public partial class CheckoutViewModel : ObservableObject
{
[RelayCommand]
private async Task Checkout()
{
var order = _orderService.CalculateOrder(Items);
await _orderService.CreateOrderAsync(order);
await _navigation.NavigateToAsync<OrderConfirmationViewModel>(order.Id);
}
}Not Using Compiled Bindings
<!-- BAD: Reflection-based binding (slow, no compile-time checking) -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui">
<Label Text="{Binding ProductName}" />
<Label Text="{Binding Price}" /> <!-- Typo won't be caught -->
</ContentPage>
<!-- GOOD: Compiled bindings (fast, compile-time checked) -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:DataType="vm:ProductViewModel">
<Label Text="{Binding Name}" />
<Label Text="{Binding Price, StringFormat='{0:C}'}" />
</ContentPage>Platform Code Anti-Patterns
Preprocessor Directives Everywhere
// BAD: Conditional compilation scattered throughout codebase
public class ProductService
{
public async Task<string> GetDeviceToken()
{
#if ANDROID
var token = await FirebaseMessaging.Instance.GetToken();
return token.ToString();
#elif IOS
var settings = await UNUserNotificationCenter.Current.GetNotificationSettingsAsync();
if (settings.AuthorizationStatus == UNAuthorizationStatus.Authorized)
{
return UIApplication.SharedApplication.ValueForKey(
new NSString("deviceToken")).ToString();
}
return null;
#elif WINDOWS
var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
return channel.Uri;
#endif
}
// This pattern repeated in dozens of methods...
}
// GOOD: Platform abstraction with DI
public interface IPushNotificationService
{
Task<string> GetDeviceTokenAsync();
}
// Platforms/Android/PushNotificationService.cs
public class AndroidPushNotificationService : IPushNotificationService
{
public async Task<string> GetDeviceTokenAsync()
{
var token = await FirebaseMessaging.Instance.GetToken();
return token.ToString();
}
}
// Register in MauiProgram.cs
#if ANDROID
builder.Services.AddSingleton<IPushNotificationService, AndroidPushNotificationService>();
#endifDirect Platform API Calls in Shared Code
// BAD: Android-specific code in shared ViewModel
public partial class SettingsViewModel : ObservableObject
{
[RelayCommand]
private void OpenAppSettings()
{
var intent = new Android.Content.Intent(
Android.Provider.Settings.ActionApplicationDetailsSettings);
intent.SetData(Android.Net.Uri.Parse("package:" +
Android.App.Application.Context.PackageName));
Android.App.Application.Context.StartActivity(intent);
}
}
// GOOD: Use MAUI Essentials or abstraction
public partial class SettingsViewModel : ObservableObject
{
[RelayCommand]
private async Task OpenAppSettings()
{
await Launcher.OpenAsync(new Uri("app-settings:"));
}
}Performance Anti-Patterns
Synchronous Operations on UI Thread
// BAD: Blocking UI thread
public partial class ProductsViewModel : ObservableObject
{
[RelayCommand]
private void LoadProducts()
{
var json = File.ReadAllText("products.json"); // Blocks UI
var products = JsonSerializer.Deserialize<List<Product>>(json);
Products = new ObservableCollection<Product>(products);
}
}
// GOOD: Async operations
public partial class ProductsViewModel : ObservableObject
{
[RelayCommand]
private async Task LoadProducts()
{
await using var stream = await FileSystem.OpenAppPackageFileAsync("products.json");
var products = await JsonSerializer.DeserializeAsync<List<Product>>(stream);
Products = new ObservableCollection<Product>(products);
}
}Creating New Collections Instead of Updating
// BAD: Replacing entire collection (causes full UI refresh)
public partial class ProductsViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<Product> _products;
[RelayCommand]
private async Task RefreshProducts()
{
var items = await _service.GetProductsAsync();
Products = new ObservableCollection<Product>(items); // Full rebind
}
}
// GOOD: Update existing collection
public partial class ProductsViewModel : ObservableObject
{
public ObservableCollection<Product> Products { get; } = new();
[RelayCommand]
private async Task RefreshProducts()
{
var items = await _service.GetProductsAsync();
Products.Clear();
foreach (var item in items)
{
Products.Add(item);
}
}
// Or use batch updates for large collections
[RelayCommand]
private async Task RefreshProductsBatched()
{
var items = await _service.GetProductsAsync();
MainThread.BeginInvokeOnMainThread(() =>
{
Products.Clear();
foreach (var item in items)
{
Products.Add(item);
}
});
}
}Not Virtualizing Large Lists
<!-- BAD: StackLayout doesn't virtualize -->
<ScrollView>
<StackLayout BindableLayout.ItemsSource="{Binding Products}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Name}" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ScrollView>
<!-- GOOD: CollectionView virtualizes items -->
<CollectionView ItemsSource="{Binding Products}"
ItemSizingStrategy="MeasureFirstItem">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Label Text="{Binding Name}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>Loading Full-Size Images
// BAD: Loading original high-res images
<Image Source="{Binding ImageUrl}" />
// GOOD: Use appropriate size and caching
<Image>
<Image.Source>
<UriImageSource Uri="{Binding ThumbnailUrl}"
CacheValidity="7"
CachingEnabled="True" />
</Image.Source>
</Image>
// Or resize in code
public static ImageSource GetOptimizedImage(string url, int width, int height)
{
// Use image CDN or resize parameter
return ImageSource.FromUri(new Uri($"{url}?w={width}&h={height}"));
}Lifecycle Anti-Patterns
Not Handling App Lifecycle
// BAD: Ignoring lifecycle events
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
// No lifecycle handling
}
// GOOD: Proper lifecycle management
public partial class App : Application
{
private readonly IAppStateService _stateService;
public App(IAppStateService stateService)
{
InitializeComponent();
_stateService = stateService;
MainPage = new AppShell();
}
protected override void OnSleep()
{
base.OnSleep();
_stateService.SaveState();
}
protected override void OnResume()
{
base.OnResume();
_stateService.RestoreState();
}
}Memory Leaks from Event Handlers
// BAD: Event handler not unsubscribed
public partial class ProductsPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<CartViewModel>(this, "CartUpdated", OnCartUpdated);
}
// OnDisappearing never called or handler not removed
}
// GOOD: Proper subscription management
public partial class ProductsPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
WeakReferenceMessenger.Default.Register<CartUpdatedMessage>(this, OnCartUpdated);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
WeakReferenceMessenger.Default.Unregister<CartUpdatedMessage>(this);
}
}Timer Not Disposed
// BAD: Timer keeps running forever
public partial class DashboardPage : ContentPage
{
public DashboardPage()
{
InitializeComponent();
var timer = new System.Timers.Timer(5000);
timer.Elapsed += async (s, e) => await RefreshData();
timer.Start(); // Never stopped
}
}
// GOOD: Timer properly managed
public partial class DashboardPage : ContentPage
{
private IDispatcherTimer _timer;
protected override void OnAppearing()
{
base.OnAppearing();
_timer = Dispatcher.CreateTimer();
_timer.Interval = TimeSpan.FromSeconds(5);
_timer.Tick += OnTimerTick;
_timer.Start();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_timer?.Stop();
_timer = null;
}
private async void OnTimerTick(object sender, EventArgs e)
{
await RefreshData();
}
}Dependency Injection Anti-Patterns
Service Locator Pattern
// BAD: Service locator anti-pattern
public class ProductsViewModel
{
private readonly IProductService _service;
public ProductsViewModel()
{
_service = App.Services.GetService<IProductService>(); // Hidden dependency
}
}
// GOOD: Constructor injection
public partial class ProductsViewModel : ObservableObject
{
private readonly IProductService _service;
public ProductsViewModel(IProductService service)
{
_service = service; // Explicit dependency
}
}Not Registering Pages
// BAD: Creating pages manually
public class NavigationService
{
public async Task NavigateToProductDetail(int productId)
{
var viewModel = App.Services.GetService<ProductDetailViewModel>();
var page = new ProductDetailPage { BindingContext = viewModel };
await Shell.Current.Navigation.PushAsync(page);
}
}
// GOOD: Register pages and ViewModels
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
// Register ViewModels
builder.Services.AddTransient<ProductDetailViewModel>();
// Register Pages
builder.Services.AddTransient<ProductDetailPage>();
// Register routes
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
return builder.Build();
}
}Testing Anti-Patterns
ViewModels Dependent on Platform
// BAD: ViewModel uses platform APIs directly
public class SettingsViewModel
{
public string DeviceId => DeviceInfo.Current.Idiom.ToString(); // Hard to test
}
// GOOD: Abstract platform dependencies
public interface IDeviceInfoService
{
string GetDeviceIdiom();
}
public class SettingsViewModel
{
private readonly IDeviceInfoService _deviceInfo;
public SettingsViewModel(IDeviceInfoService deviceInfo)
{
_deviceInfo = deviceInfo;
}
public string DeviceId => _deviceInfo.GetDeviceIdiom();
}
// In tests
var mockDeviceInfo = new Mock<IDeviceInfoService>();
mockDeviceInfo.Setup(d => d.GetDeviceIdiom()).Returns("Phone");
var viewModel = new SettingsViewModel(mockDeviceInfo.Object);Not Testing Commands
// BAD: Commands that are hard to test
public class ProductsViewModel
{
public ICommand LoadCommand => new Command(async () =>
{
var products = await _service.GetProductsAsync();
Products = new ObservableCollection<Product>(products);
});
}
// GOOD: Use RelayCommand with testable methods
public partial class ProductsViewModel : ObservableObject
{
[RelayCommand]
private async Task LoadProducts()
{
var products = await _service.GetProductsAsync();
Products = new ObservableCollection<Product>(products);
}
}
// Test
[Fact]
public async Task LoadProducts_PopulatesCollection()
{
var mockService = new Mock<IProductService>();
mockService.Setup(s => s.GetProductsAsync())
.ReturnsAsync(new[] { new Product { Name = "Test" } });
var viewModel = new ProductsViewModel(mockService.Object);
await viewModel.LoadProductsCommand.ExecuteAsync(null);
Assert.Single(viewModel.Products);
}Resource Anti-Patterns
Hardcoded Colors and Sizes
<!-- BAD: Hardcoded values -->
<Button BackgroundColor="#512BD4"
TextColor="White"
FontSize="16"
Padding="16,10"
CornerRadius="8" />
<Button BackgroundColor="#512BD4"
TextColor="White"
FontSize="16"
Padding="16,10"
CornerRadius="8" />
<!-- GOOD: Use resources and styles -->
<ContentPage.Resources>
<Color x:Key="PrimaryColor">#512BD4</Color>
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource PrimaryColor}" />
<Setter Property="TextColor" Value="White" />
<Setter Property="FontSize" Value="16" />
<Setter Property="Padding" Value="16,10" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</ContentPage.Resources>
<Button Style="{StaticResource PrimaryButton}" Text="Submit" />
<Button Style="{StaticResource PrimaryButton}" Text="Save" />Not Supporting Dark Mode
<!-- BAD: Fixed colors that don't adapt -->
<ContentPage BackgroundColor="White">
<Label TextColor="Black" Text="Hello" />
</ContentPage>
<!-- GOOD: Theme-aware colors -->
<ContentPage BackgroundColor="{AppThemeBinding Light=White, Dark=#1E1E1E}">
<Label TextColor="{AppThemeBinding Light=Black, Dark=White}" Text="Hello" />
</ContentPage>
<!-- Or use semantic colors from resources -->
<ContentPage BackgroundColor="{DynamicResource PageBackgroundColor}">
<Label TextColor="{DynamicResource PrimaryTextColor}" Text="Hello" />
</ContentPage>MAUI Patterns
Shell Navigation
Hierarchical Navigation
// AppShell.xaml
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell">
<FlyoutItem Title="Home" Icon="home.png">
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="Products" Icon="products.png">
<Tab Title="All">
<ShellContent ContentTemplate="{DataTemplate views:ProductsPage}" />
</Tab>
<Tab Title="Favorites">
<ShellContent ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>
</FlyoutItem>
<FlyoutItem Title="Settings" Icon="settings.png">
<ShellContent ContentTemplate="{DataTemplate views:SettingsPage}" />
</FlyoutItem>
</Shell>Route Registration and Navigation
// Register routes in AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Register detail pages not in visual hierarchy
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
Routing.RegisterRoute(nameof(OrderDetailPage), typeof(OrderDetailPage));
Routing.RegisterRoute(nameof(CheckoutPage), typeof(CheckoutPage));
}
}Navigation with Complex Parameters
// Pass complex objects using Dictionary
public async Task NavigateToProductDetail(Product product)
{
var parameters = new Dictionary<string, object>
{
{ "Product", product },
{ "Source", "ProductList" }
};
await Shell.Current.GoToAsync(nameof(ProductDetailPage), parameters);
}
// Receive complex parameters
[QueryProperty(nameof(Product), "Product")]
[QueryProperty(nameof(Source), "Source")]
public partial class ProductDetailViewModel : ObservableObject
{
[ObservableProperty]
private Product _product;
[ObservableProperty]
private string _source;
partial void OnProductChanged(Product value)
{
// Initialize view with product data
LoadProductDetails(value);
}
}Navigation with URI-Style Routes
// Absolute navigation (replaces stack)
await Shell.Current.GoToAsync("//home/products");
// Relative navigation (pushes onto stack)
await Shell.Current.GoToAsync("productDetail");
// Navigate back
await Shell.Current.GoToAsync("..");
// Navigate back multiple levels
await Shell.Current.GoToAsync("../..");
// Navigate back to root
await Shell.Current.GoToAsync("//");Back Button Handling
public partial class CheckoutPage : ContentPage
{
protected override bool OnBackButtonPressed()
{
// Show confirmation dialog
Dispatcher.Dispatch(async () =>
{
bool answer = await DisplayAlert(
"Leave Checkout?",
"Your cart will be saved.",
"Leave", "Stay");
if (answer)
{
await Shell.Current.GoToAsync("..");
}
});
return true; // Prevent default back navigation
}
}Platform-Specific Code Patterns
Handler Customization
// Customize Entry handler for all platforms
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
"CustomEntry", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
#elif IOS || MACCATALYST
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#elif WINDOWS
handler.PlatformView.BorderThickness = new Microsoft.UI.Xaml.Thickness(0);
#endif
});
return builder.Build();
}
}Platform-Specific Services with DI
// Shared interface
public interface INotificationService
{
Task<bool> RequestPermissionAsync();
Task ShowLocalNotificationAsync(string title, string message);
}
// Platform implementation registration
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS
builder.Services.AddSingleton<INotificationService, iOSNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#endif
return builder.Build();
}
}Multi-Targeting with Partial Classes
// Services/BiometricService.cs (shared definition)
public partial class BiometricService : IBiometricService
{
public partial Task<bool> AuthenticateAsync(string reason);
public partial bool IsAvailable { get; }
}
// Platforms/Android/BiometricService.cs
public partial class BiometricService
{
public partial bool IsAvailable =>
BiometricManager.From(Platform.CurrentActivity)
.CanAuthenticate(BiometricManager.Authenticators.BiometricStrong)
== BiometricManager.BiometricSuccess;
public partial async Task<bool> AuthenticateAsync(string reason)
{
var executor = ContextCompat.GetMainExecutor(Platform.CurrentActivity);
var callback = new BiometricCallback();
var promptInfo = new BiometricPrompt.PromptInfo.Builder()
.SetTitle("Authenticate")
.SetSubtitle(reason)
.SetNegativeButtonText("Cancel")
.Build();
var biometricPrompt = new BiometricPrompt(
Platform.CurrentActivity as FragmentActivity,
executor,
callback);
biometricPrompt.Authenticate(promptInfo);
return await callback.Task;
}
}
// Platforms/iOS/BiometricService.cs
public partial class BiometricService
{
private readonly LAContext _context = new();
public partial bool IsAvailable =>
_context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out _);
public partial async Task<bool> AuthenticateAsync(string reason)
{
var (success, _) = await _context.EvaluatePolicyAsync(
LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
reason);
return success;
}
}OnPlatform and OnIdiom in XAML
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Platform-specific values -->
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="iOS" Value="0,20,0,0" />
<On Platform="Android" Value="0" />
<On Platform="WinUI" Value="10" />
</OnPlatform>
</ContentPage.Padding>
<!-- Device idiom-specific values -->
<Label Text="Welcome">
<Label.FontSize>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>16</OnIdiom.Phone>
<OnIdiom.Tablet>24</OnIdiom.Tablet>
<OnIdiom.Desktop>20</OnIdiom.Desktop>
</OnIdiom>
</Label.FontSize>
</Label>
<!-- Combined platform and idiom -->
<Grid>
<Grid.ColumnDefinitions>
<OnIdiom x:TypeArguments="ColumnDefinitionCollection">
<OnIdiom.Phone>
<ColumnDefinition Width="*" />
</OnIdiom.Phone>
<OnIdiom.Tablet>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="*" />
</OnIdiom.Tablet>
</OnIdiom>
</Grid.ColumnDefinitions>
</Grid>
</ContentPage>Messaging Patterns
WeakReferenceMessenger
// Define message types
public record ProductAddedMessage(Product Product);
public record CartUpdatedMessage(int ItemCount);
public record UserLoggedInMessage(User User);
// Subscribe in ViewModel constructor
public partial class CartViewModel : ObservableObject
{
public CartViewModel()
{
WeakReferenceMessenger.Default.Register<ProductAddedMessage>(this, (r, m) =>
{
// Handle product added
AddToCart(m.Product);
});
}
// Clean up when ViewModel is disposed
public void Dispose()
{
WeakReferenceMessenger.Default.UnregisterAll(this);
}
}
// Send message from another ViewModel
public partial class ProductDetailViewModel : ObservableObject
{
[RelayCommand]
private void AddToCart()
{
WeakReferenceMessenger.Default.Send(new ProductAddedMessage(CurrentProduct));
}
}Request/Response Messages
// Define request message
public class CartCountRequestMessage : RequestMessage<int> { }
// Register handler
WeakReferenceMessenger.Default.Register<CartViewModel, CartCountRequestMessage>(
this, (r, m) => m.Reply(r.Items.Count));
// Send request and get response
var count = WeakReferenceMessenger.Default.Send<CartCountRequestMessage>();
if (count.HasReceivedResponse)
{
UpdateBadge(count.Response);
}Lifecycle Patterns
Application Lifecycle
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
protected override void OnStart()
{
// App started or returned from background (cold start)
Analytics.TrackEvent("app_started");
}
protected override void OnSleep()
{
// App going to background
SaveAppState();
}
protected override void OnResume()
{
// App returning from background (warm start)
RefreshTokenIfNeeded();
}
}Page Lifecycle
public partial class ProductsPage : ContentPage
{
private readonly ProductsViewModel _viewModel;
public ProductsPage(ProductsViewModel viewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
}
protected override void OnAppearing()
{
base.OnAppearing();
// Load data when page appears
_viewModel.LoadProductsCommand.Execute(null);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// Clean up resources
_viewModel.CancelPendingOperations();
}
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
// Page is now the active page
}
protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
{
base.OnNavigatedFrom(args);
// Page is no longer active
}
}Data Binding Patterns
Value Converters
public class BoolToColorConverter : IValueConverter
{
public Color TrueColor { get; set; } = Colors.Green;
public Color FalseColor { get; set; } = Colors.Red;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b && b ? TrueColor : FalseColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Usage in XAML
<ContentPage.Resources>
<converters:BoolToColorConverter x:Key="BoolToColor"
TrueColor="Green"
FalseColor="Gray" />
</ContentPage.Resources>
<Label Text="{Binding Status}"
TextColor="{Binding IsActive, Converter={StaticResource BoolToColor}}" />Multi-Binding
public class FullNameConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length >= 2 &&
values[0] is string firstName &&
values[1] is string lastName)
{
return $"{firstName} {lastName}";
}
return string.Empty;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Usage in XAML
<Label>
<Label.Text>
<MultiBinding Converter="{StaticResource FullNameConverter}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</Label.Text>
</Label>Behaviors
public class NumericValidationBehavior : Behavior<Entry>
{
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnTextChanged;
base.OnDetachingFrom(entry);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is Entry entry)
{
bool isValid = double.TryParse(e.NewTextValue, out _);
entry.TextColor = isValid ? Colors.Black : Colors.Red;
}
}
}
// Usage in XAML
<Entry Placeholder="Enter amount">
<Entry.Behaviors>
<behaviors:NumericValidationBehavior />
</Entry.Behaviors>
</Entry>Resource and Theme Patterns
Dynamic Resources and Themes
// App.xaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Theme-aware colors -->
<Color x:Key="PrimaryColor">
<AppThemeBinding Light="#512BD4" Dark="#B39DDB" />
</Color>
<Color x:Key="BackgroundColor">
<AppThemeBinding Light="White" Dark="#1E1E1E" />
</Color>
</ResourceDictionary>
</Application.Resources>
// Programmatic theme switching
public void SetTheme(AppTheme theme)
{
Application.Current.UserAppTheme = theme; // Light, Dark, or Unspecified
}Style Inheritance
<Style x:Key="BaseButtonStyle" TargetType="Button">
<Setter Property="FontSize" Value="16" />
<Setter Property="Padding" Value="16,10" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style x:Key="PrimaryButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="BackgroundColor" Value="{DynamicResource PrimaryColor}" />
<Setter Property="TextColor" Value="White" />
</Style>
<Style x:Key="SecondaryButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{DynamicResource PrimaryColor}" />
<Setter Property="BorderColor" Value="{DynamicResource PrimaryColor}" />
<Setter Property="BorderWidth" Value="1" />
</Style>CollectionView Patterns
Grouping
// ViewModel
public ObservableCollection<ProductGroup> GroupedProducts { get; } = new();
public class ProductGroup : ObservableCollection<Product>
{
public string Category { get; }
public ProductGroup(string category, IEnumerable<Product> products) : base(products)
{
Category = category;
}
}<CollectionView ItemsSource="{Binding GroupedProducts}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="vm:ProductGroup">
<Label Text="{Binding Category}"
FontAttributes="Bold"
FontSize="18"
BackgroundColor="LightGray"
Padding="10" />
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<VerticalStackLayout Padding="10">
<Label Text="{Binding Name}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>Selection and Commands
<CollectionView ItemsSource="{Binding Products}"
SelectionMode="Single"
SelectedItem="{Binding SelectedProduct}"
SelectionChangedCommand="{Binding ProductSelectedCommand}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Delete"
BackgroundColor="Red"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:ProductsViewModel}}, Path=DeleteProductCommand}"
CommandParameter="{Binding}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="10">
<Label Text="{Binding Name}" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>