Agent 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.
Install this agent skill to your Project
npx add-skill https://github.com/managedcode/dotnet-skills/tree/main/catalog/Frameworks/MAUI/skills/dotnet-maui
SKILL.md
.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:
xml<ContentPage x:DataType="vm:ProductsViewModel"> -
Virtualize long lists:
xml<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem" /> -
Optimize images:
csharpvar image = ImageSource.FromFile("image.png"); // Use appropriate resolution for platform -
Avoid synchronous work on UI thread:
csharp// 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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
dotnet-project-setup
Create or reorganize .NET solutions with clean project boundaries, repeatable SDK settings, and a maintainable baseline for libraries, apps, tests, CI, and local development.
csharp-scripts
Run single-file C# programs as scripts (file-based apps) for quick experimentation, prototyping, and concept testing. Use when the user wants to write and execute a small C# program without creating a full project.
dotnet-pinvoke
Correctly call native (C/C++) libraries from .NET using P/Invoke and LibraryImport. Covers function signatures, string marshalling, memory lifetime, SafeHandle, and cross-platform patterns. USE FOR: writing new P/Invoke or LibraryImport declarations, reviewing or debugging existing native interop code, wrapping a C or C++ library for use in .NET, diagnosing crashes, memory leaks, or corruption at the managed/native boundary. DO NOT USE FOR: COM interop, C++/CLI mixed-mode assemblies, or pure managed code with no native dependencies.
nuget-trusted-publishing
Set up NuGet trusted publishing (OIDC) on a GitHub Actions repo — replaces long-lived API keys with short-lived tokens. USE FOR: trusted publishing, NuGet OIDC, keyless NuGet publish, migrate from NuGet API key, NuGet/login, secure NuGet publishing. DO NOT USE FOR: publishing to private feeds or Azure Artifacts (OIDC is nuget.org only). INVOKES: shell (powershell or bash), edit, create, ask_user for guided repo setup.
dotnet-legacy-aspnet
Maintain classic ASP.NET applications on .NET Framework, including Web Forms, older MVC, and legacy hosting patterns, while planning realistic modernization boundaries.
dotnet-code-review
Review .NET changes for bugs, regressions, architectural drift, missing tests, incorrect async or disposal behavior, and platform-specific pitfalls before you approve or merge them.
Didn't find tool you were looking for?