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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-maui-bfa4ebd.zip · 12 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

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

  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

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 platform
    
  4. 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
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.

No comments yet.

Reviews (0)

No reviews yet.

Related