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
平台目标
| Platform | Build Host | Notes |
|---|---|---|
| Android | Windows/Mac | Emulator or device |
| iOS | Mac only | Requires Xcode |
| macOS | Mac only | Catalyst |
| Windows | Windows | WinUI 3 |
| 平台 | 构建主机 | 说明 |
|---|---|---|
| Android | Windows/Mac | 支持模拟器或真机 |
| iOS | 仅Mac | 需要安装Xcode |
| macOS | 仅Mac | 基于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
- 确认目标平台 — Android、iOS、Mac、Windows的行为存在差异
- 分离共享UI和平台代码 — 使用handlers和DI
- 遵循MVVM模式 — 保持视图层轻量化,业务逻辑放在ViewModels中
- 处理生命周期和权限 — 平台契约需要针对性测试
- 在真机上测试 — 模拟器无法覆盖所有问题场景
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-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 |
| 反模式 | 弊端 | 优化方案 |
|---|---|---|
| 上帝ViewModel | 难以维护 | 拆分为职责单一的ViewModel |
| 后置代码中编写业务逻辑 | 难以测试 | 使用MVVM和命令机制 |
| 平台代码散落在各处 | 丧失跨平台优势 | 使用handlers/DI管理 |
| 视图中直接调用服务 | 耦合度过高 | 逻辑下沉到ViewModel |
| 忽略生命周期处理 | 易出现崩溃、内存泄漏 | 针对性处理生命周期事件 |
Performance Best Practices
性能最佳实践
-
Use compiled bindings:xml
<ContentPage x:DataType="vm:ProductsViewModel"> -
Virtualize long lists:xml
<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem" /> -
Optimize images:csharp
var 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();
-
使用编译绑定:xml
<ContentPage x:DataType="vm:ProductsViewModel"> -
长列表使用虚拟化:xml
<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem" /> -
优化图片资源:csharp
var image = ImageSource.FromFile("image.png"); // 适配对应平台的合适分辨率 -
避免在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的构建规则明确无歧义
- 在目标设备上性能符合预期