dotnet-maui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

.NET MAUI

.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
  • 基于.NET MAUI开发跨平台移动端或桌面端UI
  • 集成设备能力、导航功能或编写平台特定代码
  • 迁移Xamarin.Forms项目,或对齐多端目标的共享代码库
  • 在移动应用中实现MVVM模式

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
  • patterns.md - Shell导航、平台特定代码、消息通信、生命周期、数据绑定和CollectionView使用模式
  • anti-patterns.md - 常见的MAUI开发错误及规避方案

Platform Targets

平台目标

PlatformBuild HostNotes
AndroidWindows/MacEmulator or device
iOSMac onlyRequires Xcode
macOSMac onlyCatalyst
WindowsWindowsWinUI 3
平台构建主机说明
AndroidWindows/Mac支持模拟器或真机
iOS仅Mac需要安装Xcode
macOS仅Mac基于Catalyst实现
WindowsWindows基于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
  1. 确认目标平台 — Android、iOS、Mac、Windows的行为存在差异
  2. 分离共享UI和平台代码 — 使用handlers和DI
  3. 遵循MVVM模式 — 保持视图层轻量化,业务逻辑放在ViewModels中
  4. 处理生命周期和权限 — 平台契约需要针对性测试
  5. 在真机上测试 — 模拟器无法覆盖所有问题场景

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/
MyApp/
├── MyApp/                    # 共享代码
│   ├── App.xaml              # 应用入口
│   ├── MauiProgram.cs        # DI和配置
│   ├── Views/                # XAML页面
│   ├── ViewModels/           # MVVM ViewModels
│   ├── Models/               # 领域模型
│   ├── Services/             # 业务逻辑
│   └── Platforms/            # 平台特定代码
│       ├── Android/
│       ├── iOS/
│       ├── MacCatalyst/
│       └── Windows/
└── MyApp.Tests/

MVVM Pattern

MVVM模式

ViewModel with MVVM Toolkit

基于MVVM Toolkit实现ViewModel

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;
}
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>
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();
    }
}
csharp
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // 服务注册
        builder.Services.AddSingleton<IProductService, ProductService>();
        builder.Services.AddSingleton<INavigationService, NavigationService>();

        // ViewModels注册
        builder.Services.AddTransient<ProductsViewModel>();
        builder.Services.AddTransient<ProductDetailViewModel>();

        // 页面注册
        builder.Services.AddTransient<ProductsPage>();
        builder.Services.AddTransient<ProductDetailPage>();

        return builder.Build();
    }
}

Navigation

导航

Shell Navigation

Shell导航

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);
    }
}
csharp
// 注册路由
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));

// 带参数跳转
await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}?id={product.Id}");

// 接收参数
[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("..");
}
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() ?? "";
    }
}
csharp
// Services/DeviceService.cs (共享层)
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
}
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-PatternWhy It's BadBetter Approach
God ViewModelUnmaintainableSplit into focused ViewModels
Logic in code-behindHard to testUse MVVM and commands
Platform code everywhereDefeats cross-platformUse handlers/DI
Direct service calls in ViewsTight couplingUse ViewModel
Ignoring lifecycleCrashes, leaksHandle lifecycle events
反模式弊端优化方案
上帝ViewModel难以维护拆分为职责单一的ViewModel
后置代码中编写业务逻辑难以测试使用MVVM和命令机制
平台代码散落在各处丧失跨平台优势使用handlers/DI管理
视图中直接调用服务耦合度过高逻辑下沉到ViewModel
忽略生命周期处理易出现崩溃、内存泄漏针对性处理生命周期事件

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();
  1. 使用编译绑定:
    xml
    <ContentPage x:DataType="vm:ProductsViewModel">
  2. 长列表使用虚拟化:
    xml
    <CollectionView ItemsSource="{Binding Items}"
                    ItemSizingStrategy="MeasureFirstItem" />
  3. 优化图片资源:
    csharp
    var image = ImageSource.FromFile("image.png");
    // 适配对应平台的合适分辨率
  4. 避免在UI线程执行同步任务:
    csharp
    // 错误写法
    var data = service.GetData(); // 会阻塞UI
    
    // 正确写法
    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);
}
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
  • 带有清晰平台边界的MAUI共享代码
  • 遵循MVVM模式、可测试的ViewModels
  • 适配各端目标的导航和生命周期行为
  • 针对所选平台的可落地构建部署路径

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
  • 跨平台代码复用真实有效,而非表面复用
  • 平台特定行为逻辑隔离且可测试
  • MVVM模式全程一致遵循
  • Mac/iOS和Windows的构建规则明确无歧义
  • 在目标设备上性能符合预期