Dotnet-skills 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.
install
source · Clone the upstream repo
git clone https://github.com/managedcode/dotnet-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/managedcode/dotnet-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/catalog/Frameworks/MAUI/skills/dotnet-maui" ~/.claude/skills/managedcode-dotnet-skills-dotnet-maui && rm -rf "$T"
manifest:
catalog/Frameworks/MAUI/skills/dotnet-maui/SKILL.mdsource content
.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 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