Claude
Skill
dotnet-maui
Build, review, or migrate .NET MAUI applications across Android, iOS, macOS, and Windows with correct cross-platform UI, platform integration, and native packaging assumptions.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-maui-bfa4ebd.zip · 12 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-maui
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
.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
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 platformAvoid 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
Files (dotnet-skills)
-
references
-
anti-patterns.md 17.6 KB
# MAUI Anti-Patterns ## Navigation Anti-Patterns ### Coupling Views to Navigation ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```xml <!-- 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 ```csharp // 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>(); #endif ``` ### Direct Platform API Calls in Shared Code ```csharp // 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 ```csharp // 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 ```csharp // 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 ```xml <!-- 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```xml <!-- 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 ```xml <!-- 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> ``` -
patterns.md 16.9 KB
# MAUI Patterns ## Shell Navigation ### Hierarchical Navigation ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```xml <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 ```csharp // 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 ```csharp // 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp // 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 ```xml <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 ```csharp // 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; } } ``` ```xml <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 ```xml <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> ```
-
-
SKILL.md 9.1 KB
--- name: dotnet-maui version: "1.0.0" category: "Cross-Platform UI" description: "Build, review, or migrate .NET MAUI applications across Android, iOS, macOS, and Windows with correct cross-platform UI, platform integration, and native packaging assumptions." compatibility: "Requires .NET MAUI workload (.NET 8+)." --- # .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 - [.NET MAUI Overview](https://learn.microsoft.com/en-us/dotnet/maui/what-is-maui) - [Enterprise Patterns](https://learn.microsoft.com/en-us/dotnet/architecture/maui/) - [MVVM Pattern](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm) - [Controls Reference](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/) - [Platform Integration](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/) ### References - [patterns.md](references/patterns.md) - Shell navigation, platform-specific code, messaging, lifecycle, data binding, and CollectionView patterns - [anti-patterns.md](references/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 ## 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 ```csharp 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 ```xml <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 ```csharp 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 ```csharp // 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 ```csharp 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 ```csharp // 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 ```csharp 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:** ```xml <ContentPage x:DataType="vm:ProductsViewModel"> ``` 2. **Virtualize long lists:** ```xml <CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem" /> ``` 3. **Optimize images:** ```csharp var image = ImageSource.FromFile("image.png"); // Use appropriate resolution for platform ``` 4. **Avoid synchronous work on UI thread:** ```csharp // Bad var data = service.GetData(); // Blocks UI // Good var data = await service.GetDataAsync(); ``` ## Testing ```csharp [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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.