.NET MAUI 组件与应用模式实践指南:基于 awesome-copilot 指令库的跨平台开发规范
导读
本文以 instructions/dotnet-maui.instructions.md 为核心,系统梳理 .NET MAUI 跨平台应用开发的组件与应用模式:从代码风格、命名约定、弃用 API 规避,到布局选型、Shell 导航、编译绑定、线程调度、SecureStorage 安全存储与测试策略,并引入仓库中 agents/dotnet-maui.agent.md(MAUI 专家 Agent)、instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md(版本升级指南)与 instructions/mvvm-toolkit.instructions.md(MVVM Toolkit 规范)作为源码级佐证。读完本文,你将掌握一套可直接落地到 Copilot 辅助开发流程的 .NET MAUI 编码标准,能够写出性能良好、可维护、符合现代 API 的跨平台应用。
一、这份指令文件是什么,以及如何生效
在 awesome-copilot 仓库中,指令(Instructions)是"按文件模式自动应用"的编码标准。本文件头部通过 YAML Front Matter 声明了生效范围:
---
description: '.NET MAUI component and application patterns'
applyTo: '**/*.xaml, **/*.cs'
---
applyTo: '**/*.xaml, **/*.cs':表示当 Copilot 处理项目中任意.xaml(XAML 界面)与.cs(C# 逻辑)文件时,这些规则都会被注入上下文,指导代码生成与审查。这保证了 MAUI 规范只在真正需要的文件上生效,避免污染其他类型代码。description字段则用于在指令库中快速检索,如 docs/README.instructions.md 中的条目所示。
使用方式(详见 docs/README.instructions.md):
- 将指令内容复制到工作区的
.github/copilot-instructions.md; - 或创建任务专属指令文件,如
.github/instructions/dotnet-maui.instructions.md; - 指令一旦安装即自动作用于 Copilot 行为,无需手动开关。
配套地,仓库还提供了 agents/dotnet-maui.agent.md(一个名为 "MAUI Expert" 的专用 Agent,面向控件、XAML、Handler 与性能的专家级支持),两者配合可让 Copilot 同时获得"规范约束"与"专家能力"。
二、代码风格与结构
2.1 总原则
- 编写地道的(idiomatic)、高效的 .NET MAUI 与 C# 代码,遵循 .NET 与 .NET MAUI 官方约定。
- UI(View)只负责布局与绑定:把业务逻辑放到 ViewModel 与服务中,保持视图纯净,这也是 MVVM 模式的核心要求(见 instructions/mvvm-toolkit.instructions.md 中关于 ViewModel 基类与命令生成的约定)。
- 所有 I/O 与耗时操作一律使用
async/await,保持 UI 线程响应,杜绝阻塞式同步调用。
2.2 命名约定
| 对象 | 约定 | 示例 |
|---|---|---|
| 组件名、方法名、公共成员 | PascalCase |
LoginPage、LoadDataAsync() |
| 私有字段、局部变量 | camelCase |
_isBusy、items |
| 接口 | 前缀 I |
IUserService、IAuthService |
这与 MVVM Toolkit 的属性生成规则高度一致:[ObservableProperty] 必须施加于 name、_name、m_name 形式的私有字段,由源生成器生成 PascalCase 公共属性;若字段写成 PascalCase 反而会与生成的属性冲突(详见 instructions/mvvm-toolkit.instructions.md)。
三、生命周期与 .NET 专属约定
3.1 组件生命周期
善用 .NET MAUI 内置生命周期钩子:
OnAppearing/OnDisappearing:页面出现/消失时执行初始化与清理;- 导航参数解析、消息订阅/退订、数据刷新等逻辑应放置在对应生命周期方法中,而不是依赖构造函数副作用。
3.2 数据绑定与 MVVM
- 有效利用
{Binding}与 MVVM 模式; - 组件与服务遵循关注点分离(Separation of Concerns):ViewModel 不直接操作控件,服务不依赖页面实例;
- 语言版本以仓库目标 .NET SDK 与项目设置为准:不要引入需要预览语言特性(preview language features)的语法,除非项目本身已配置支持——例如 MVVM Toolkit 的源生成器要求
LangVersion支持生成器(现代 SDK 默认即可)。
3.3 后台任务与 UI 更新
更新 UI 必须回到 UI 线程,优先级如下:
- 持有 Page、View 或其他
BindableObject引用时,优先使用BindableObject.Dispatcher; - 在服务或 ViewModel 中(无 BindableObject 直接访问权)时,通过依赖注入注入
IDispatcher; - 仅在没有任何 Dispatcher 可用时,回退到
MainThread.BeginInvokeOnMainThread(...); - 避免已过时的
Device.BeginInvokeOnMainThread模式。
// ViewModel 中通过 DI 注入 IDispatcher,避免直接依赖 UI 类型
public sealed partial class MainViewModel(IDispatcher dispatcher) : ObservableObject
{
public async Task RefreshAsync()
{
var data = await _service.LoadAsync(); // 后台线程
await dispatcher.DispatchAsync(() =>
{
Items = data; // 回到 UI 线程更新
});
}
}
四、临界规则(Critical Rules):绝不触碰的弃用 API
这是全篇最重要的部分,也是与 instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md 直接呼应的内容。规则简洁明确:
| 禁止(Deprecated) | 替代方案 |
|---|---|
ListView |
CollectionView |
TableView |
CollectionView 或 Grid / VerticalStackLayout 等布局 |
Frame |
Border |
*AndExpand 布局选项 |
Grid + 显式尺寸 |
ScrollView/CollectionView 嵌在 StackLayout 系内 |
用 Grid 作父布局 |
运行时引用 .svg 图片 |
使用 PNG/JPG 资源 |
混用 Shell 与 NavigationPage/TabbedPage/FlyoutPage |
统一使用 Shell |
| Renderers(渲染器) | Handlers(处理器) |
BackgroundColor |
Background(支持渐变/Brush) |
4.1 为什么这些是硬性规则
以 ListView 为例,升级指南明确指出其在 .NET 10 中已被标记为 obsolete,警告形如:
warning CS0618: 'ListView' is obsolete: 'ListView is deprecated. Please use CollectionView instead.'
它并非简单的 find-replace 迁移:事件模型(ItemSelected → SelectionChanged)、分组(GroupDisplayBinding → GroupHeaderTemplate)、上下文操作(ContextActions → SwipeView)、行高(HasUnevenRows → ItemSizingStrategy)全部不同。升级指南给出了完整的对照迁移示例:
<!-- Before: ListView -->
<ListView ItemsSource="{Binding Items}"
ItemSelected="OnItemSelected"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Title}"
Detail="{Binding Description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- After: CollectionView(注意需显式开启 SelectionMode) -->
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectionChanged="OnSelectionChanged">
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout Padding="10">
<Label Text="{Binding Title}" FontAttributes="Bold" />
<Label Text="{Binding Description}" FontSize="12" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
BackgroundColor 同理:Background 属性支持 SolidColorBrush、LinearGradientBrush 等画刷,是现代 API 的推荐入口;Frame 仅保留阴影用途(见 agents/dotnet-maui.agent.md 控件参考表)。
4.2 常见弃用 API 的迁移速查(P1 级)
除了上述 P0 级硬规则,升级指南还列出了仍可用但已弃用、未来将被移除的 API:
| 旧 API | 新 API | 示例 |
|---|---|---|
FadeTo() / ScaleTo() 等同步动画 |
FadeToAsync() / ScaleToAsync() |
await view.FadeToAsync(0, 500); |
DisplayAlert() / DisplayActionSheet() |
DisplayAlertAsync() / DisplayActionSheetAsync() |
await DisplayAlertAsync("Info", "msg", "OK"); |
Page.IsBusy |
自定义 ActivityIndicator 覆盖层(将 .NET 11 移除) |
<ActivityIndicator IsRunning="{Binding IsLoading}" /> |
MessagingCenter(.NET 10 已 internal) |
WeakReferenceMessenger.Default(CommunityToolkit.Mvvm) |
WeakReferenceMessenger.Default.Send(new UserLoggedInMessage(user)); |
其中 MessagingCenter 的迁移尤其值得注意:WeakReferenceMessenger 对同一接收者重复注册同一消息类型会抛出 InvalidOperationException,且必须显式 Unregister(或在 OnDisappearing 中 UnregisterAll)以防内存泄漏——这与核心文档"未退订事件导致内存泄漏"的告诫完全一致。
五、布局与控件选型
5.1 布局选择
- 优先
VerticalStackLayout/HorizontalStackLayout,而不是<StackLayout Orientation="...">(前者性能更好); - 复杂布局、需要细分空间时优先
Grid; - 带边框/背景的容器优先
Border而非Frame。
5.2 列表选择
- 小列表(≤20 项、不可滚动):
BindableLayout; - 大列表或可滚动列表:
CollectionView(自带虚拟化,绝不可放进 StackLayout 系)。
MAUI 专家 Agent 补充了更细的决策依据(agents/dotnet-maui.agent.md):
| 场景 | 控件 |
|---|---|
| 状态指示(不定量/定量进度) | ActivityIndicator(IsRunning)/ ProgressBar(Progress) |
| 多选文本 | Editor(AutoSize="TextChanges") |
| 下拉选择 | Picker |
| 搜索输入 | SearchBar |
| 轮播图/引导页/图片滑动 | CarouselView + IndicatorView |
| 下拉刷新 | RefreshView 包裹可滚动内容 |
| 滑动上下文操作 | SwipeView |
| 自定义绘制 | GraphicsView(通过 ICanvas) |
| 交互式地图 | Map |
典型推荐写法(来自 Agent 最佳实践):
<!-- 复杂布局用 Grid -->
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,*">
...
</Grid>
<!-- 用 Border 替代 Frame,支持圆角 StrokeShape -->
<Border Stroke="Black" StrokeThickness="1" StrokeShape="RoundRectangle 10">
...
</Border>
<!-- 用专门的 Stack 布局 -->
<VerticalStackLayout> <!-- 而非 <StackLayout Orientation="Vertical"> -->
...
</VerticalStackLayout>
六、Shell 导航
- 以 Shell 作为主导航宿主;
- 使用
Routing.RegisterRoute(...)注册路由,用Shell.Current.GoToAsync(...)导航; - 启动时只设置一次
MainPage,避免频繁更换引发导航异常; - 不要在 Shell 内部嵌套 Tabs。
// 注册路由(通常在 App 启动或 Shell 构造处)
Routing.RegisterRoute("details", typeof(DetailPage));
// 导航并携带查询参数
await Shell.Current.GoToAsync("details?id=123");
Agent 补充的说明:频繁更换 MainPage 是常见坑之一;若曾混用 Shell 与 NavigationPage/TabbedPage/FlyoutPage,必须统一收敛到 Shell。
七、错误处理与验证
- 为页面与 API 调用实现完善的错误处理;
- 应用级错误使用日志记录;可恢复的失败要记录日志并给用户展示友好提示;
- 表单验证使用 FluentValidation 或 DataAnnotations。
在 MVVM Toolkit 体系下(instructions/mvvm-toolkit.instructions.md),验证的推荐姿势是:
- ViewModel 继承
ObservableValidator(提供INotifyDataErrorInfo),配合[Required]、[Range]、[EmailAddress]、[MinLength]、[MaxLength]、[CustomValidation]等 DataAnnotation; - 提交前调用
ValidateAllProperties(),检查HasErrors为true则中止提交; - 成功提交或重置表单后调用
ClearAllErrors(); - 跨属性校验时,在被改属性的
OnXxxChanged钩子中调用ValidateProperty(value, nameof(Other))。
API 调用层面,用 try-catch 包裹,并在 UI 中给出恰当反馈:
public async Task SaveAsync()
{
try
{
ValidateAllProperties();
if (HasErrors) return;
await _apiService.SaveAsync(Item);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Save failed");
await _dispatcher.DispatchAsync(() =>
_page.DisplayAlertAsync("错误", "保存失败,请稍后重试", "确定"));
}
}
八、性能优化:编译绑定、布局深度与绑定模式
8.1 编译绑定(Compiled Bindings)
- 在页面/视图/模板上设置
x:DataType; - C# 中优先使用基于表达式的绑定(类型安全、可编译);
- 可在项目设置中开启更严格的 XAML 编译,例如
MauiStrictXamlCompilation=true,尤其在 CI 中强烈建议。
<!-- 始终为页面/视图/DataTemplate 指定 x:DataType -->
<ContentPage x:DataType="vm:MainViewModel">
<Label Text="{Binding Name}" />
</ContentPage>
Agent 指出编译绑定可带来数倍到十几倍的性能提升,而字符串绑定则会在运行时才报错、且没有 IntelliSense。C# 侧的对比:
// 推荐:基于表达式的绑定(类型安全、编译期校验)
label.SetBinding(Label.TextProperty, static (PersonViewModel vm) => vm.FullName?.FirstName);
// 不推荐:字符串绑定(运行时才报错,无智能提示)
label.SetBinding(Label.TextProperty, "FullName.FirstName");
8.2 布局深度与嵌套
- 避免深层布局嵌套(尤其是嵌套 StackLayout);复杂布局用
Grid; - 让绑定保持"有意为之":
| 场景 | 绑定模式 |
|---|---|
| 值不变 | OneTime |
| 默认只读展示 | OneWay |
| 仅可编辑值 | TwoWay |
| 静态常量 | 不要绑定,直接赋值 |
8.3 CollectionView 的额外优化
升级指南补充:若列表项高度一致,可用 ItemSizingStrategy="MeasureFirstItem" 只测量首项以提升大列表性能;RemainingItemsThreshold="5" + RemainingItemsThresholdReachedCommand 可实现无限滚动加载;内置 EmptyView 处理空列表状态。
<CollectionView ItemsSource="{Binding Items}"
ItemSizingStrategy="MeasureFirstItem"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
<CollectionView.EmptyView>
<Label Text="暂无数据" HorizontalTextAlignment="Center" />
</CollectionView.EmptyView>
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
</CollectionView.ItemsLayout>
</CollectionView>
8.4 平台定制:Handlers 而非 Renderers
跨平台定制必须走 Handler 通道,且优先使用条件编译(agents/dotnet-maui.agent.md):
// 在 MauiProgram.cs 的 ConfigureMauiHandlers 中
Microsoft.Maui.Handlers.ButtonHandler.Mapper.AppendToMapping("Custom", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.HotPink);
#elif IOS
handler.PlatformView.BackgroundColor = UIKit.UIColor.SystemPink;
#endif
});
平台代码同样用条件编译符号区分:#if ANDROID / #elif IOS / #elif WINDOWS / #elif MACCATALYST。
九、资源与资产
- 图片放
Resources/Images/,字体放Resources/Fonts/,原始资产放Resources/Raw/; - 引用图片一律使用 PNG/JPG,例如
<Image Source="logo.png" />,不要使用.svg(SVG 仅用于生成阶段); - 使用尺寸合适的图片,避免内存膨胀。
Agent 补充:即便原始素材是 SVG,也要先转为 PNG 再作为运行时资源引用;图片来源包括 Resources/Images/(PNG、JPG、SVG→PNG 转换结果)。
十、状态管理、API 集成与依赖注入
10.1 状态管理
- 共享状态与横切关注点使用 DI 管理的服务(Singleton 级别),如设置、文件/HTTP 服务、共享
IMessenger; - ViewModel 的生命周期与导航/页面一致(Transient,按页注册),避免全局持有页面状态。
MVVM Toolkit 规范给出了明确的 DI 生命周期建议(instructions/mvvm-toolkit.instructions.md):
| 生命周期 | 适用对象 |
|---|---|
AddSingleton<T>() |
Shell/主窗口 VM、设置、文件/HTTP 服务、共享 IMessenger |
AddTransient<T>() |
按页/按文档的 VM |
AddScoped<T>() |
仅配合显式 IServiceScope 使用,客户端应用很少需要 |
10.2 API 集成
- 使用
HttpClient或其他合适的服务与外部 API 或自有后端通信; - 对 API 调用实现 try-catch 错误处理,并在 UI 中给出正确的用户反馈;
- 全部网络通信使用 HTTPS,并确保后端配置合理的 CORS 策略。
十一、存储与机密:SecureStorage 的正确姿势
- 机密信息(token、refresh token)使用
SecureStorage; - 处理异常(设备不支持、密钥变更、数据损坏)时清除/重置并重新认证;
- 不要把机密存在
Preferences中——它是明文偏好存储,不具备机密保护能力。
// 写入(Agent 中的标准用法)
await SecureStorage.SetAsync("oauth_token", token);
// 读取
string token = await SecureStorage.GetAsync("oauth_token");
// 异常时清除并重新认证
try
{
var token = await SecureStorage.GetAsync("oauth_token");
if (string.IsNullOrEmpty(token)) throw new InvalidOperationException("令牌缺失");
}
catch (Exception)
{
SecureStorage.Remove("oauth_token"); // 清除损坏数据
// 引导用户重新登录
}
Agent 同时强调:绝不提交机密到版本库、校验所有输入、全程使用 HTTPS。
十二、测试与调试
- 使用 xUnit、NUnit 或 MSTest 测试组件与服务;
- 使用 Moq 或 NSubstitute 在测试中模拟依赖。
典型做法:为 ViewModel 与 Service 编写单元测试,通过构造函数注入 mock 的 IUserService、IHttpClientFactory 或 IDispatcher(VM 不应从静态服务定位器取依赖,否则难以测试——见 instructions/mvvm-toolkit.instructions.md 的"Things to avoid")。
[Fact]
public async Task Login_Success_SetsUser()
{
var authService = new Mock<IAuthService>();
authService.Setup(s => s.LoginAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(new User { Name = "Alice" });
var vm = new LoginViewModel(authService.Object, new Mock<IDispatcher>().Object);
await vm.LoginAsync();
Assert.Equal("Alice", vm.CurrentUser.Name);
}
十三、安全与认证
- 按需在 MAUI 应用中实现认证与授权:API 认证使用 OAuth 或 JWT token;
- 所有 Web 通信使用 HTTPS,并保证 CORS 策略正确;
- token 等凭据走
SecureStorage(见第十一章),不与普通偏好混存。
十四、常见陷阱清单(Common Pitfalls)
汇总核心文档与 Agent、升级指南共同强调的易错点:
- 频繁更换
MainPage:易引发导航状态异常; - 父子视图手势冲突:父/子同时挂手势识别器会互相干扰,用
InputTransparent = true屏蔽非目标视图; - 未退订的事件导致内存泄漏:订阅的事件/消息必须在
OnDisappearing或对应生命周期中退订、释放资源; - 深层嵌套布局:伤害渲染性能,应扁平化视觉层级、用 Grid 替代嵌套 StackLayout;
- 只在模拟器上测试:会漏掉真实设备的边界情况(通知、键盘、性能、权限、安全区等),必须在物理设备上验证;
- 在 StackLayout 系中放置可滚动/虚拟化控件:破坏滚动与虚拟化(核心文档与 Agent 的双重硬性禁令);
- 混用 Shell 与 NavigationPage/TabbedPage/FlyoutPage、嵌套 Tabs:导航行为不可预测;
- 用 Renderer 而非 Handler:渲染器已废弃;
- 部分 Xamarin.Forms API 尚未进入 MAUI:若遇到缺失 API,应查询官方 issue 跟踪而非强行移植。
十五、与版本升级、专家 Agent 和 MVVM Toolkit 的协同
- 版本升级场景:若项目正从 .NET MAUI 9 升级到 10,请同时加载 instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md。其中 P0 级破坏性变更(
MessagingCenter转 internal、ListView/TableView弃用)必须优先修复,P1 级弃用 API(动画、DisplayAlert、IsBusy、MediaPicker)尽快修复;升级还要求CommunityToolkit.Maui≥ 12.3.0,Linux/CI 环境可借条件化TargetFrameworks只在非 Linux 上引入 iOS/Mac Catalyst 目标。 - 专家能力场景:需要控件参考、Handler 定制、编译绑定示例时,直接使用 agents/dotnet-maui.agent.md(MAUI Expert Agent)。
- MVVM 工程化场景:ViewModel 基类(
ObservableObject/ObservableValidator/ObservableRecipient)、[ObservableProperty]、[RelayCommand]、WeakReferenceMessenger与 DI 生命周期,详见 instructions/mvvm-toolkit.instructions.md,其 XAML 绑定模式(显式Mode、命令直绑)与本文的编译绑定要求一脉相承。
三者与本文共同构成一套从"日常编码规范"到"版本迁移"再到"MVVM 工程化"的完整 .NET MAUI 实践体系。
小结
围绕 instructions/dotnet-maui.instructions.md 这一指令文件,本文完整继承了其全部规范要点,并结合仓库中的专家 Agent、升级指南与 MVVM Toolkit 规范做了源码级扩充:以"弃用 API 零容忍"为底线,以"编译绑定 + Grid 布局 + Shell 导航 + SecureStorage"为现代实践主干,以"生命周期管理与线程调度"保障响应性,以"单元测试与物理设备验证"守住质量关卡。将本文规则安装为 Copilot 指令后,即可让 AI 辅助生成的 .NET MAUI 代码自动符合这些现代、可维护、高性能的跨平台开发标准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00