名称: dotnet-maui-development 描述: “构建 .NET MAUI 移动应用。项目结构、XAML/MVVM、平台服务、注意事项。” 用户可调用: false
dotnet-maui-development
.NET MAUI 跨平台开发:单一项目结构,XAML 数据绑定与 MVVM(CommunityToolkit.Mvvm),Shell 导航,平台服务通过部分类和条件编译,依赖注入,每个平台的热重载,以及 .NET 11 改进(XAML 源生成,Android 的 CoreCLR,dotnet run 设备选择)。包括当前状态评估和迁移选项。
版本假设: .NET 8.0+ 基准(MAUI 随 .NET 8+ 发布)。.NET 11 预览版 1 内容明确标记。示例使用最新稳定 API。
范围
- 单一项目结构,带平台文件夹
- XAML 数据绑定与 MVVM(CommunityToolkit.Mvvm)
- Shell 导航和平台服务
- 依赖注入和热重载
- 当前状态评估和迁移选项
- .NET 11 改进(XAML 源生成,Android 的 CoreCLR)
超出范围
- MAUI iOS/Mac Catalyst 的 Native AOT —— 参见 [skill:dotnet-maui-aot]
- MAUI 测试(Appium, XHarness) —— 参见 [skill:dotnet-maui-testing]
- 一般 Native AOT 模式 —— 参见 [skill:dotnet-native-aot]
- UI 框架选择决策树 —— 参见 [skill:dotnet-ui-chooser]
交叉引用: [skill:dotnet-maui-aot] 用于 iOS/Mac Catalyst 的 Native AOT,[skill:dotnet-maui-testing] 用于测试模式,[skill:dotnet-version-detection] 用于 TFM 检测,[skill:dotnet-native-aot] 用于一般 AOT 模式,[skill:dotnet-ui-chooser] 用于框架选择,[skill:dotnet-accessibility] 用于可访问性模式(SemanticProperties, 屏幕阅读器)。
项目结构
MAUI 使用单一项目架构。一个 .csproj 通过多目标针对所有平台,平台特定代码在平台文件夹中。
<!-- MyApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">
$(TargetFrameworks);net8.0-windows10.0.19041.0
</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<RootNamespace>MyApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
<PackageReference Include="CommunityToolkit.Maui" Version="9.*" />
</ItemGroup>
</Project>
项目布局
MyApp/
MyApp/
App.xaml / App.xaml.cs # 应用程序入口,资源字典
AppShell.xaml / AppShell.xaml.cs # Shell 导航定义
MauiProgram.cs # 主机构建器,DI,服务注册
MainPage.xaml / MainPage.xaml.cs # 初始页面
ViewModels/ # MVVM ViewModels
Views/ # XAML 页面
Models/ # 数据模型
Services/ # 服务接口和实现
Resources/
Fonts/ # 自定义字体 (.ttf/.otf)
Images/ # SVG/PNG 图像(每个平台自动调整大小)
Styles/ # 共享样式,颜色,资源字典
Raw/ # 原始资产(JSON 等)
Splash/ # 启动屏幕图像
Platforms/
Android/ # AndroidManifest.xml, MainActivity.cs
iOS/ # Info.plist, AppDelegate.cs
MacCatalyst/ # Info.plist, AppDelegate.cs
Windows/ # Package.appxmanifest, App.xaml
Properties/
launchSettings.json
MyApp.Tests/ # 单元测试
资源管理
MAUI 声明式处理资源文件。图像从单一源为每个平台自动调整大小:
<!-- 资源在 .csproj ItemGroups 中配置 -->
<ItemGroup>
<!-- SVG/PNG 图像:MAUI 为每个平台密度调整大小 -->
<MauiImage Include="Resources\Images\*" />
<!-- 字体:自动注册 -->
<MauiFont Include="Resources\Fonts\*" />
<!-- 启动屏幕 -->
<MauiSplashScreen Include="Resources\Splash\splash.svg"
Color="#512BD4" BaseSize="128,128" />
<!-- 应用图标 -->
<MauiIcon Include="Resources\AppIcon\appicon.svg"
ForegroundFile="Resources\AppIcon\appiconfg.svg"
Color="#512BD4" />
</ItemGroup>
XAML 模式
数据绑定基础
MAUI XAML 数据绑定连接 UI 元素到 ViewModel 属性。使用 {Binding} 与正确的 BindingContext 设置。
<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.ProductListPage"
x:DataType="vm:ProductListViewModel">
<VerticalStackLayout Padding="16" Spacing="12">
<SearchBar Text="{Binding SearchTerm}"
SearchCommand="{Binding SearchCommand}" />
<CollectionView ItemsSource="{Binding Products}"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectProductCommand}"
SelectionChangedCommandParameter="{Binding SelectedItem,
Source={RelativeSource Self}}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Product">
<Frame Padding="12" Margin="0,4">
<HorizontalStackLayout Spacing="12">
<Image Source="{Binding ImageUrl}"
HeightRequest="60" WidthRequest="60" />
<VerticalStackLayout>
<Label Text="{Binding Name}"
FontAttributes="Bold" />
<Label Text="{Binding Price, StringFormat='{0:C}'}"
TextColor="Gray" />
</VerticalStackLayout>
</HorizontalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
编译绑定: 在页面和数据模板上使用 x:DataType 启用编译绑定。编译绑定在构建时进行类型检查,运行时比反射绑定更快。
使用 CommunityToolkit.Mvvm 的 MVVM
CommunityToolkit.Mvvm(Microsoft MVVM Toolkit)是 MAUI 推荐的 MVVM 框架。它使用源生成器消除样板代码。
// ViewModels/ProductListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
private readonly INavigationService _navigationService;
public ProductListViewModel(
IProductService productService,
INavigationService navigationService)
{
_productService = productService;
_navigationService = navigationService;
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SearchCommand))]
private string _searchTerm = "";
[ObservableProperty]
private ObservableCollection<Product> _products = [];
[ObservableProperty]
private bool _isLoading;
[RelayCommand]
private async Task LoadProductsAsync(CancellationToken ct)
{
IsLoading = true;
try
{
var items = await _productService.GetProductsAsync(ct);
Products = new ObservableCollection<Product>(items);
}
finally
{
IsLoading = false;
}
}
[RelayCommand(CanExecute = nameof(CanSearch))]
private async Task SearchAsync(CancellationToken ct)
{
var results = await _productService.SearchAsync(SearchTerm, ct);
Products = new ObservableCollection<Product>(results);
}
private bool CanSearch() => !string.IsNullOrWhiteSpace(SearchTerm);
[RelayCommand]
private async Task SelectProductAsync(Product? product)
{
if (product is null) return;
await _navigationService.GoToAsync(
nameof(ProductDetailPage),
new Dictionary<string, object> { ["Product"] = product });
}
}
关键源生成器属性:
[ObservableProperty]—— 从后台字段生成具有INotifyPropertyChanged的属性[RelayCommand]—— 从方法生成ICommand(支持异步、取消、CanExecute)[NotifyPropertyChangedFor]—— 为依赖属性引发PropertyChanged[NotifyCanExecuteChangedFor]—— 当属性更改时重新评估命令CanExecute
Shell 导航
Shell 定义视觉层次和导航结构。支持弹出菜单、标签页和基于 URI 的导航。
<!-- 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">
<!-- 基于标签页的导航 -->
<TabBar>
<ShellContent Title="产品"
Icon="products.png"
ContentTemplate="{DataTemplate views:ProductListPage}" />
<ShellContent Title="购物车"
Icon="cart.png"
ContentTemplate="{DataTemplate views:CartPage}" />
<ShellContent Title="个人资料"
Icon="profile.png"
ContentTemplate="{DataTemplate views:ProfilePage}" />
</TabBar>
</Shell>
// 为不在 Shell 视觉层次中的页面注册路由
// (推送到导航堆栈,非直接 Shell 标签页)
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
Routing.RegisterRoute(nameof(CheckoutPage), typeof(CheckoutPage));
}
}
// 带参数导航
await Shell.Current.GoToAsync(nameof(ProductDetailPage),
new Dictionary<string, object>
{
["Product"] = selectedProduct
});
// 通过 QueryProperty 接收参数
[QueryProperty(nameof(Product), "Product")]
public partial class ProductDetailViewModel : ObservableObject
{
[ObservableProperty]
private Product _product = null!;
}
ContentPage 生命周期
public partial class ProductListPage : ContentPage
{
private readonly ProductListViewModel _viewModel;
public ProductListPage(ProductListViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
BindingContext = viewModel;
}
protected override async void OnAppearing()
{
base.OnAppearing();
// 当页面出现时加载数据(不在构造函数中)
await _viewModel.LoadProductsCommand.ExecuteAsync(null);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// 取消待处理操作,取消订阅事件
}
}
平台服务
部分类用于平台特定代码
使用部分类,平台特定实现在 Platforms 文件夹中。构建系统为每个目标编译正确的实现。
// Services/IDeviceService.cs(共享接口)
public interface IDeviceService
{
string GetDeviceIdentifier();
Task<bool> HasBiometricAsync();
}
// Services/DeviceService.cs(共享部分类)
public partial class DeviceService : IDeviceService
{
public partial string GetDeviceIdentifier();
public partial Task<bool> HasBiometricAsync();
}
// Platforms/Android/Services/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceIdentifier()
{
return Android.Provider.Settings.Secure.GetString(
Android.App.Application.Context.ContentResolver,
Android.Provider.Settings.Secure.AndroidId) ?? "unknown";
}
public partial Task<bool> HasBiometricAsync()
{
var manager = BiometricManager.From(Android.App.Application.Context);
var result = manager.CanAuthenticate(
BiometricManager.Authenticators.BiometricStrong);
return Task.FromResult(
result == BiometricManager.BiometricSuccess);
}
}
// Platforms/iOS/Services/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceIdentifier()
{
return UIKit.UIDevice.CurrentDevice.IdentifierForVendor?.ToString()
?? "unknown";
}
public partial Task<bool> HasBiometricAsync()
{
var context = new LocalAuthentication.LAContext();
return Task.FromResult(context.CanEvaluatePolicy(
LocalAuthentication.LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
out _));
}
}
条件编译
对于较小的平台差异,使用 #if 指令而非部分类:
public void ConfigurePlatformDefaults()
{
#if ANDROID
// Android 特定:请求权限
Platform.CurrentActivity?.RequestPermissions(
[Android.Manifest.Permission.Camera]);
#elif IOS || MACCATALYST
// iOS/Mac Catalyst:无需为相机进行运行时权限请求
// (通过 Info.plist NSCameraUsageDescription 处理)
#elif WINDOWS
// Windows:WinUI 特定配置
#endif
}
何时使用每种方法:
- 部分类: 大型平台实现,多个方法,复杂逻辑
- 条件编译: 单行差异,小分支,常量
依赖注入
MAUI 使用 Microsoft.Extensions.DependencyInjection。在 MauiProgram.cs 中注册服务、ViewModels 和页面。
// MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// 服务
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddSingleton<INavigationService, NavigationService>();
builder.Services.AddTransient<IDeviceService, DeviceService>();
// HTTP 客户端
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
// ViewModels(瞬态,每个页面获得新实例)
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
// 页面(瞬态,通过 DI 注入 ViewModels 解析)
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailPage>();
// 对于超出 MAUI 特定注册的 DI 模式,参见 [skill:dotnet-csharp-dependency-injection]
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
当前状态评估(2026 年 2 月)
最后验证:2026-02-13
生产就绪性
.NET MAUI 是具有注意事项的生产就绪。该框架具有强大的企业吸引力和活跃的社区投资,但开发人员应了解已知的工具和平台差距。
增长指标:
- 36% 年同比用户增长
- 557% 社区拉取请求增长
- 针对业务线应用的强大企业采用
已知问题
Visual Studio 2026 工具错误:
- Android 工具链在 IDE 更新后首次构建时偶尔失败;清理和重新构建解决大多数问题
- 热重载在某些 Android 模拟器配置上间歇性无法连接
- XAML IntelliSense 可能对有效编译绑定显示错误;尽管有红色波浪线,构建成功
iOS 平台差距:
- iOS 26.x 兼容性需要使用最新 MAUI 服务补丁进行测试
- 一些 iOS 特定控件(例如,带自定义格式的
DatePicker)有渲染不一致 - Xcode 更新可能破坏构建工具链,直到 MAUI 发布服务更新
诚实评估
当您需要单一的 C#/.NET 代码库,以原生 UI 渲染针对 Android、iOS、macOS(Catalyst)和 Windows 时,MAUI 是正确选择。它在业务线应用、企业场景和具有现有 .NET 专业知识的团队中表现出色。
MAUI 不是最佳选择当:
- 您需要 Web 浏览器目标(考虑 Blazor 或 Uno Platform)
- 您需要 Linux 桌面支持(考虑 Uno Platform 或 Avalonia)
- 您需要跨平台的像素完美自定义渲染(考虑游戏引擎或基于 Skia 的框架)
- 您的团队没有 .NET 经验(评估原生或 React Native 替代方案)
迁移选项
当 MAUI 不是最佳选择时,考虑这些 .NET 替代方案:
WinUI 3(仅限 Windows)
如果您的应用程序仅针对 Windows,WinUI 3 提供更丰富的 Windows 原生体验,无需跨平台抽象层。
<!-- WinUI 3 项目 -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<UseWinUI>true</UseWinUI>
</PropertyGroup>
</Project>
何时选择 WinUI 而非 MAUI:
- 仅限 Windows 的应用,无移动/macOS 要求
- 需要深度 Windows 集成(任务栏、通知、小部件)
- 需要未通过 MAUI 暴露的 WinAppSDK 功能
- 从 UWP 迁移
Uno Platform(Web + Linux + 所有平台)
如果您需要在移动和 Windows 之外添加 Web(WASM)或 Linux 桌面支持,Uno Platform 提供更广泛的目标覆盖,具有 WinUI 兼容的 API 表面。
何时选择 Uno 而非 MAUI:
- 需要 Web 浏览器部署(WASM)
- 需要 Linux 桌面支持(GTK/Framebuffer)
- 需要嵌入式设备目标
- 团队偏好 WinUI API 表面与 MVUX 反应模式
- 需要 Figma 到 XAML 设计工作流
参见 [skill:dotnet-uno-platform] 用于 Uno Platform 开发模式和 [skill:dotnet-uno-targets] 用于每个目标部署。
决策总结
| 要求 | MAUI | WinUI 3 | Uno Platform |
|---|---|---|---|
| Android + iOS | 是 | 否 | 是 |
| Windows 桌面 | 是 | 是(最佳) | 是 |
| macOS(Catalyst) | 是 | 否 | 是 |
| Web(WASM) | 否 | 否 | 是 |
| Linux 桌面 | 否 | 否 | 是 |
| 原生 UI 渲染 | 是 | 是 | Skia + 原生 |
| MVVM Toolkit | 是 | 是 | 是 |
| MVUX 反应 | 否 | 否 | 是 |
有关完整框架决策树,参见 [skill:dotnet-ui-chooser]。
.NET 11 改进
最后验证:2026-02-13
XAML 源生成(在 .NET 11 预览版 1 中默认启用)
.NET 11 预览版 1 使 XAML 源生成成为默认的 XAML 编译模式,取代传统的 XAMLC(XamlCompilationAttribute)方法。源生成的 XAML 是 AOT 友好的,产生更好的诊断,并实现更快的启动。
<!-- .NET 11:XAML 源生成默认开启 -->
<!-- 恢复为传统 XAMLC(如果源生成引起问题): -->
<PropertyGroup>
<MauiXamlInflator>XamlC</MauiXamlInflator>
</PropertyGroup>
更改内容: XAML 页面在构建时转换为 C# 源代码,而不是通过 XamlCompilationAttribute 编译为 IL。这产生类型安全的初始化代码,适用于 Native AOT。
何时恢复: 如果您遇到自定义标记扩展、依赖运行时 XAML 加载的第三方控件或旧版 LoadFromXaml 使用时的源生成问题,则恢复为 XamlC。
Android 的 CoreCLR(在 .NET 11 预览版 1 中默认启用)
.NET 11 预览版 1 用 CoreCLR 替换 Mono 作为 Android 发布构建的默认运行时。CoreCLR 提供更好的性能、改进的诊断和与服务器/桌面运行时的对齐。
<!-- .NET 11:CoreCLR 是 Android 发布构建的默认 -->
<!-- 选择退出并继续使用 Mono: -->
<PropertyGroup>
<UseMonoRuntime>true</UseMonoRuntime>
</PropertyGroup>
更改内容: 发布构建使用 CoreCLR(与 ASP.NET Core 和桌面应用相同的运行时)而非 Mono。调试构建继续使用 Mono 以支持热重载。
何时选择退出: 如果您依赖 Mono 特定行为、在较旧 Android 设备(API < 26)上遇到 CoreCLR 兼容性问题,或使用专门针对 Mono 内部结构的库,则选择退出。
dotnet run 设备选择
.NET 11 预览版 1 为 MAUI 项目添加了交互式目标框架和设备选择到 dotnet run。
# .NET 11:交互式设备选择
dotnet run --project MyApp/MyApp.csproj
# 提示:选择目标框架:
# 1. net11.0-android
# 2. net11.0-ios
# 3. net11.0-maccatalyst
# 然后:选择设备:
# 1. Pixel 7 API 34(模拟器)
# 2. Samsung Galaxy S24(物理设备)
# 通过明确 TFM 跳过交互式选择
dotnet run --project MyApp/MyApp.csproj -f net11.0-android
这取代了手动指定 -f 与精确 TFM 字符串的需要,简化了开发者的内部循环。
热重载
MAUI 支持 XAML 热重载和 C# 热重载,但能力因平台而异。
支持矩阵
| 更改类型 | Android | iOS | macOS | Windows |
|---|---|---|---|---|
| XAML 布局/样式 | 是 | 是 | 是 | 是 |
| C# 方法体 | 是 | 是 | 是 | 是 |
| 新实例方法(非泛型类) | 部分(.NET 9+) | 部分(.NET 9+) | 部分(.NET 9+) | 部分(.NET 9+) |
| 新静态方法 / 泛型类型成员 | 重新构建 | 重新构建 | 重新构建 | 重新构建 |
| 资源字典 | 是 | 是 | 是 | 是 |
| 添加新 XAML 页面 | 重新构建 | 重新构建 | 重新构建 | 重新构建 |
| CSS 更改 | 是 | 是 | 是 | 是 |
启用热重载
# CLI:热重载在调试配置中自动启用
dotnet run --project MyApp/MyApp.csproj -f net8.0-android
# Visual Studio:热重载默认开启(工具栏中的火图标)
# VS Code:使用启用热重载的 MAUI 扩展
陷阱:
- 热重载需要调试构建配置;发布构建不支持
- XAML 热重载可能不会反映自定义渲染器或处理程序的更改,直到重新构建
- 在 Android 上,热重载使用
MetadataUpdateHandler机制;静态字段或构造函数的更改需要重启 - 在 iOS 模拟器上,热重载有效,但物理设备热重载需要稳定的 USB/WiFi 连接
代理注意事项
- 不要创建单独的平台项目。 MAUI 使用单一项目结构。平台特定代码在同一项目的
Platforms/文件夹中,而不是在单独的 Android/iOS 项目中(那是 Xamarin.Forms)。 - 不要混合 MVVM Toolkit 属性与手动
INotifyPropertyChanged。 一致使用[ObservableProperty]。混合源生成和手写的属性更改实现会导致细微的绑定错误。 - 不要在构造函数中调用异步方法。 使用
OnAppearing()或加载命令触发数据加载。构造函数中的异步调用导致未观察的异常和绑定上下文初始化的竞争条件。 - 不要使用
Device.BeginInvokeOnMainThread。 它已弃用。使用来自Microsoft.Maui.ApplicationModel的MainThread.BeginInvokeOnMainThread()或MainThread.InvokeOnMainThreadAsync()。 - 不要使用
RuntimeInformation硬编码平台检查。 使用DeviceInfo.Platform比较(DevicePlatform.Android、DevicePlatform.iOS),这是 MAUI 的平台检测跨平台抽象。 - 不要使用没有
x:DataType的{Binding}。 始终在页面和数据模板上设置x:DataType以启用编译绑定。反射绑定较慢且不会在构建时捕获。 - 页面通常应为瞬态,而非单例。 单例页面导致陈旧数据和保留绑定的内存泄漏。如果需要状态保留(例如,标签页),使用具有瞬态页面的单例 ViewModel。
- 不要忘记为非标签页注册 Shell 路由。 通过
GoToAsync推送到导航堆栈的页面必须在AppShell构造函数中通过Routing.RegisterRoute注册,否则导航抛出RouteNotFoundException。
先决条件
- .NET 8.0+(.NET MAUI 随 .NET 8+ 发布)
- MAUI 工作负载:
dotnet workload install maui - 平台 SDK:Android SDK(API 21+)、Xcode(仅 macOS,用于 iOS/Mac Catalyst)、Windows App SDK(用于 Windows)
- Visual Studio 2022+ 带 MAUI 工作负载、VS Code 带 .NET MAUI 扩展,或 JetBrains Rider 2024.2+