暂时更改一部分Skill
This commit is contained in:
1 parent
84b28f9672
commit
6d4629e4ac
53 files changed
+5293
-10
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# Store.Management 项目架构
|
||||
|
||||
## 定位与实现边界
|
||||
|
||||
Store.Management 是需要登录并访问远端 API 的 WPF 门店管理客户端。当前实现包含登录、用户 API、框架能力示例以及管理、商品上架模块的启动骨架;不要把骨架或示例描述为已完成的业务功能。
|
||||
|
||||
## 项目划分
|
||||
|
||||
| 路径 | 职责 |
|
||||
|---|---|
|
||||
| `Source/Store.Management/` | 应用宿主、登录窗口、主窗口、示例页面与容器注册 |
|
||||
| `Source/Store.Management.Framework/` | WPF 基础控件、应用生命周期、配置目录、配置服务、日志与工具 |
|
||||
| `Source/Store.Management.Api/` | RestSharp API 客户端、认证会话、用户接口、DTO 与传输错误 |
|
||||
| `Source/Core/MAX.Core/` | 通用工具、Autofac 注册、类型发现与通知集成 |
|
||||
| `Source/Core/MAX.Framework/` | Prism/Autofac 适配、命令、弹窗基类与日志封装 |
|
||||
| `Source/Core/MAX.Notification/` | 通知契约、发布器与处理器分发 |
|
||||
| `Source/Modules/Store.Admin/` | 管理模块骨架,目前通过项目引用依赖 Framework |
|
||||
| `Source/Modules/Store.ListingProduct/` | 商品上架模块骨架,目前通过项目引用依赖 Framework |
|
||||
| `Tests/` | 可执行的冒烟测试项目 |
|
||||
|
||||
- 通用 Core 不反向依赖 Store 项目;API 层保持与 WPF 视图解耦。
|
||||
- 共用客户端基础设施放 Framework,接口通信放 Api,具体业务放对应模块。
|
||||
- 当前宿主没有引用上述两个业务模块;模块只有 `IStartup` 骨架。新增模块功能时核实程序集加载、注册和导航链路,不假设已自动接入。
|
||||
- 不为遵守规则而新增数据库、ORM、图表、视频或文档生成依赖;当前仓库没有这些业务实现。
|
||||
|
||||
## 技术与配置依据
|
||||
|
||||
- `Directory.Build.props` 默认 `net8.0-windows`、C# `14.0`、隐式 using、Nullable annotations;各项目可以覆盖。
|
||||
- Core 与 Notification 中有 `net8.0` 项目;业务模块和部分测试覆盖为 `net10.0` / `net10.0-windows`。依据各 `.csproj`,不将整个解决方案误写为同一目标框架,也不擅自统一。
|
||||
- UI 使用 Prism.Wpf 和仓库内的 Autofac 适配,控件使用官方 `HandyControl` 与 `MAX.Common.WPF`;具体版本以项目文件为准。
|
||||
- API 使用 RestSharp 与 System.Text.Json;JSON 配置服务使用 Newtonsoft.Json;日志使用 NLog。
|
||||
- 应用入口为 `App : FrameworkApplication`,后者继承 `Prism.Autofac.PrismApplication`。维护现有启动、登录成功切换主窗口及退出释放客户端的流程。
|
||||
|
||||
## 数据、目录与生命周期
|
||||
|
||||
- 所有 HTTP 请求复用 `StoreApiClient` / `ApiTransport`,不在 ViewModel 中另起传输实现。
|
||||
- 路径复用 `FolderConfig`,配置复用 `JsonSettingsServiceBase<T>`;读取路径不应触发不必要的文件创建。
|
||||
- 当前 `FolderConfig.AppName` 是 `StoreManagement`,应用数据目录实际为 `%LOCALAPPDATA%/StoreManagement`;不要根据类注释或程序集名猜目录,也不要无迁移方案改名。
|
||||
- 保持配置保存的串行化、同目录临时文件替换、失败保留原模型行为。
|
||||
- 普通业务失败交给调用方展示;致命错误复用 `ApplicationLogService` / `FrameworkApplication.Fatal`。不要吞掉错误或把取消当作致命故障。
|
||||
|
||||
## 验证方式
|
||||
|
||||
C#、XAML 或项目依赖变更后,构建受影响项目并运行相关冒烟测试。以下命令均在仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
dotnet build Store.Management.slnx
|
||||
dotnet run --project Tests/MAX.Core.SmokeTests/MAX.Core.SmokeTests.csproj
|
||||
dotnet run --project Tests/MAX.Notification.SmokeTests/MAX.Notification.SmokeTests.csproj
|
||||
dotnet run --project Tests/Store.Management.Api.SmokeTests/Store.Management.Api.SmokeTests.csproj
|
||||
dotnet run --project Tests/Store.Management.Framework.SmokeTests/Store.Management.Framework.SmokeTests.csproj
|
||||
dotnet run --project Tests/Store.Management.App.SmokeTests/Store.Management.App.SmokeTests.csproj
|
||||
```
|
||||
|
||||
- 这些测试是控制台可执行程序,不能仅以 `dotnet test` 无失败判断测试已运行。
|
||||
- Framework 和 App 冒烟测试未列入当前解决方案,按项目路径单独执行;WPF 测试需要 Windows,部分项目需要 .NET 10 SDK。
|
||||
- 纯规则文档修改检查引用和内容即可,无需启动应用或全量构建。
|
||||
- 遇到 SDK、还原或文件锁定问题,报告具体阻碍;不要擅自终止用户进程或把失败报告为通过。
|
||||
|
||||
## API 与业务契约
|
||||
|
||||
- 复用 `IAuthApi` 的登录、退出、当前用户接口及 `IUserApi` 的查询、新增、更新接口;保持现有路由大小写、HTTP 方法与参数位置。
|
||||
- 登录窗口当前按服务端契约对 UTF-8 密码计算 32 位大写 MD5 后提交,保留密码首尾空格。不把该转换推广为其他用途的密码存储策略。
|
||||
- TokenSession 只存内存,不将密码或 Token 写入配置、日志或用户资料 DTO;保持登录/退出串行及旧请求 401 不清除新会话的行为。
|
||||
- ApiResult 的业务成功码为 200;HTTP 成功不等于业务成功。分页使用 Page、PageSize 与 Total、Items;保留 DTO DataAnnotations 的长度及范围限制。
|
||||
- 用户名与昵称分别对应 UserName、NickName;积分与可空余额使用 DTO 的 decimal 类型,区分 null 与 0。不自行推断 GroupId、ParentUserName 的权限语义。
|
||||
- ApiTransport 是统一传输出口,负责相对路由、版本、认证头、校验、取消与错误分类;保留 ApiException 的类型、状态和追踪信息。
|
||||
- ProductSavedNotification 是框架示例,不证明商品业务已接入。新增商品字段、上架状态或权限须依据具体需求与服务端契约。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 当前可复用控件参考
|
||||
|
||||
| 类型 | 现有位置与职责 |
|
||||
|---|---|
|
||||
| UserControlBase | Framework/Controls,启用 Prism ViewModelLocator |
|
||||
| WindowBase | Framework/Controls,继承 HandyControl Window,读取应用窗口画刷 |
|
||||
| DialogWindow | Framework/Controls,实现 Prism IDialogWindow,承载弹窗与键盘行为 |
|
||||
| ConfirmDialogViewModel | Source/Core/MAX.Framework/Controls,确认对话框基类 |
|
||||
| DialogWindowProperty | Source/Core/MAX.Framework/Controls/Attach,弹窗尺寸与 ResizeMode 附加属性 |
|
||||
| DataGridHelper | Framework/Controls/Attach,NoDataTips、IsActive 附加属性;不保证单独设置就有完整视觉行为 |
|
||||
| DISource | Framework/Extensions,使用应用提供的解析器获取 XAML 服务 |
|
||||
| StringNullOrEmpty2VisibilityConverter | Framework/Converters,null 或空字符串可见,其余折叠,仅用于单向绑定 |
|
||||
|
||||
表中 Framework 指 `Source/Store.Management.Framework/`。
|
||||
|
||||
- 页面与弹窗使用示例见 `Source/Store.Management/Views/FrameworkTestPage.xaml`、`FrameworkTestDialog.xaml`。
|
||||
- 不假设已有 IconText、HorizontalIconText、TitleContent 或 LoadingCover 的本地实现。
|
||||
- MAX.Common.WPF 的可选组件先核实包版本和实际 API;只在具体任务需要时采用。
|
||||
- 新通用控件放 Framework,维护依赖方向,验证默认值、资源和生命周期。
|
||||
@@ -0,0 +1,41 @@
|
||||
# C# 编码规则
|
||||
|
||||
## 版本与组织
|
||||
|
||||
- `Directory.Build.props` 指定 C# 14;属性优先使用 `field` 关键字管理编译器生成的后备字段。目标框架以各 `.csproj` 的覆盖值为准。
|
||||
- 可使用 C# 14 及此前版本的语言特性,调用 API 仍须兼容目标框架。保留各项目 Nullable 设置;开发环境需要支持 C# 14 的 SDK。
|
||||
- 类型、方法、公开属性使用 PascalCase,参数和局部变量使用 camelCase;私有字段沿用文件现有风格。
|
||||
- 命名空间匹配项目与目录,例如 `Store.Management.Api.Users`;Core 项目保留 `MAX.*` 命名空间。
|
||||
- 新文件优先文件范围命名空间;不批量重排无关代码。
|
||||
- 新代码优先显式类型;修改已有代码时不批量替换 var。控制流使用大括号,每条语句独立一行,避免无意义空行。
|
||||
- 公共接口及复杂行为补充 XML 注释,解释约束和原因,不要求固定注释比例。
|
||||
- 对职责一致的重复逻辑适度提取,避免重复实现传输、配置和容器功能,不为单次用途增加多层抽象。
|
||||
|
||||
## MVVM、DI 与异步
|
||||
|
||||
- ViewModel 使用 Prism `BindableBase`,属性优先以 `field` 配合 `SetProperty(ref field, value)`;需要跨成员访问或特殊存储语义时保留显式字段,不引入 DevExpress MVVM。
|
||||
- 命令使用现有 DelegateCommand / AsyncDelegateCommand;服务优先构造注入,由现有 Prism/Autofac 容器管理生命周期。
|
||||
- 调用内部库 API 前核实当前项目的依赖和类型;不要为了简单工具让 API/Core 依赖 UI 包。
|
||||
- 异步返回 Task 并按需传递 CancellationToken;async void 限于 UI 事件。
|
||||
- 不在 UI 线程用 Result/Wait 阻塞网络或文件操作;库层可 ConfigureAwait(false),更新控件和绑定时回到 UI 线程。
|
||||
- 公共入口校验参数,API DTO 沿用 DataAnnotations;区分取消、业务失败和致命错误。
|
||||
- 复用现有 NLog 封装记录错误,保留异常类型和上下文,不强制每个 catch 调用 Debug.WriteLine,不记录密码或 Token。
|
||||
- 释放自有 IDisposable、取消源与事件订阅;注入的外部 HttpClient 由调用者管理。
|
||||
|
||||
## 序列化
|
||||
|
||||
API 层保持 System.Text.Json / RestSharp 协议;JSON 配置服务保持 Newtonsoft.Json 格式。不因统一库名而改变服务端字段、配置兼容性或增加依赖。
|
||||
|
||||
## field 属性示例
|
||||
|
||||
```csharp
|
||||
public string Keyword
|
||||
{
|
||||
get => field ?? string.Empty;
|
||||
set => SetProperty(ref field, value);
|
||||
}
|
||||
|
||||
public DelegateCommand RefreshCommand => field ??= new DelegateCommand(Refresh);
|
||||
```
|
||||
|
||||
属性的初始值、可空性与通知行为应按实际需求明确;不要仅为使用 field 改写无关代码。
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: 编辑器与文件格式约定,换行符与编码规范
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 编辑器与文件格式约定
|
||||
|
||||
## 开发环境
|
||||
|
||||
当前开发机器环境为 **Windows**,所有**新建文件**默认使用 **CRLF** 换行符。
|
||||
|
||||
## 已有文件:禁止修改格式
|
||||
|
||||
编辑已有文件时,**必须保持原文件的换行符与编码格式不变**:
|
||||
|
||||
- **不要**将 LF 改为 CRLF,或 CRLF 改为 LF
|
||||
- **不要**转换文件编码(如 GBK → UTF-8、UTF-8 → UTF-8 BOM 等)
|
||||
- 遵循文件本来的换行符和编码格式进行编辑
|
||||
|
||||
若检测到已有文件的编码为非 UTF-8 或非 UTF-8 BOM,在输出中**提示用户**即可,**不要**自动转换。
|
||||
|
||||
## 新建文件
|
||||
|
||||
新建文件时的编码优先级:
|
||||
|
||||
1. **UTF-8 BOM**(首选)
|
||||
2. **UTF-8**(次选)
|
||||
|
||||
换行符:**CRLF**。
|
||||
@@ -0,0 +1,25 @@
|
||||
# HandyControl 使用规则
|
||||
|
||||
## 当前依赖
|
||||
|
||||
Framework 项目当前引用官方 HandyControl 3.6.2.1 与 MAX.Common.WPF。准确版本以 `Source/Store.Management.Framework/Store.Management.Framework.csproj` 为准。
|
||||
|
||||
- 保持官方 HandyControl 包选择,不套用内部 fork 包名禁令,不因规则迁移替换包。
|
||||
- XAML 命名空间为 `https://handyorg.github.io/handycontrol`;程序集与 pack URI 使用 HandyControl。
|
||||
- 不将当前机器的 NuGet 缓存绝对路径推广为其他机器的固定路径。
|
||||
|
||||
## 主题与窗口
|
||||
|
||||
`Source/Store.Management/App.xaml` 当前按顺序合并:
|
||||
|
||||
1. `/Store.Management.Framework;component/Resources/HandyControlBasic.xaml`
|
||||
2. `/HandyControl;component/Themes/SkinDefault.xaml`
|
||||
3. `/HandyControl;component/Themes/Theme.xaml`
|
||||
|
||||
应用定义 PrimaryBrush、TextIconBrush、BackgroundBrush 和 DialogWindowStyle。WindowBase 构造读取窗口画刷,DialogWindow 读取对话框样式,调整时保持资源契约。
|
||||
|
||||
- 默认尺寸、内边距、字号集中在 HandyControlBasic.xaml;修改前检查覆盖顺序与使用范围。
|
||||
- 不引用未创建的 FrameworkTheme.xaml、CustomColors.xaml 或语义颜色清单。
|
||||
- 优先复用已有控件与样式,核实当前包版本的属性和资源键。
|
||||
- 消息提示沿用当前 WPF / HandyControl 用法;业务弹窗复用 Prism 链路,不假设存在 IMessageBoxServiceEx。
|
||||
- 组件参考见 controls-reference.md,绑定与输入约定见 wpf-xaml.md。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 图标使用规则
|
||||
|
||||
当前仓库没有独立 Resources 项目,也没有已建立的 IconNameEnum、IconGlyphEnum、IconHelper 或 FontGeometries.xaml 图标系统。
|
||||
|
||||
- 添加前搜索实际 XAML、资源和已引用控件库,优先复用已有资源。
|
||||
- 新资源选择与页面一致且许可明确的图标,放所属项目并配置 WPF Build Action 和 pack URI。
|
||||
- 不虚构枚举、扩展、字体、资源键;不把其他项目路径简单改名后当作现有实现。
|
||||
- ImageSource 使用 Image,字体图标遵循字体实际契约;不对所有资源强制使用一种控件。
|
||||
- 菜单图标避免同一个 UIElement 被多个父节点复用,可共享 ImageSource 并创建独立元素。
|
||||
- Foreground 能否改色取决于资源类型,不承诺可重染位图或固定色 DrawingImage。
|
||||
- 图标按钮提供可访问名称和必要的 ToolTip,关键操作不只靠颜色或图标区分。
|
||||
- 建立统一图标方案后再记录真实位置与 API;不为规则迁移新增图标依赖。
|
||||
@@ -0,0 +1,23 @@
|
||||
# MAX 内部库与源码定位
|
||||
|
||||
| 项目 | 当前源码位置 | 职责 |
|
||||
|---|---|---|
|
||||
| MAX.Core | `Source/Core/MAX.Core/` | Autofac、基础工具、类型发现与通知集成 |
|
||||
| MAX.Framework | `Source/Core/MAX.Framework/` | Prism/Autofac 适配、命令、弹窗、NLog 封装 |
|
||||
| MAX.Notification | `Source/Core/MAX.Notification/` | 通知契约、发布与处理器分发 |
|
||||
|
||||
- 上述为 ProjectReference 源码,优先阅读当前实现,不套用其他版本的类型和生命周期。
|
||||
- DI 标记位于 MAX.Core.DependencyInjection;实际扫描注册以 MAX.Framework.Startup 与 AutofacContainerExtension 为准。
|
||||
- 通知上游信息见 `Source/Core/MAX.Notification/UPSTREAM.md` 与 LICENSE.md,维护时尊重现有约束。
|
||||
- Framework 直接引用 MAX.Common.WPF 1.4.23;传递依赖以项目文件及还原后的 obj/project.assets.json 为准。
|
||||
- HandyControl 使用官方包,见 handycontrol-usage.md;未引用的绘图、表格、拖拽、映射库不是默认依赖。
|
||||
- 不假设开发机存在固定的外部源码根目录。先搜索本仓库,再查 NuGet 缓存或用户提供的路径,使用前确认存在。
|
||||
- MAX.Common / MAX.Common.WPF API 以当前包版本为准,必要时使用对应 skill 核实;规则提及不代表已经引用或注册。
|
||||
|
||||
搜索优先使用 rg,例如:
|
||||
|
||||
```powershell
|
||||
rg -n 'class AsyncDelegateCommand|class ConfirmDialogViewModel' Source/Core
|
||||
rg -n 'RegisterTypes|RegisterDialog|RegisterSingleton' Source/Store.Management
|
||||
rg -n 'IPublisher|INotificationHandler' Source Tests
|
||||
```
|
||||
@@ -0,0 +1,379 @@
|
||||
---
|
||||
description: WPF MVVM 核心行为约束(项目结构、命名、ViewModel、属性、命令、服务、DI、对话框、输入限长、Framework 基类等),始终生效
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# WPF 客户端核心规范
|
||||
|
||||
## 项目结构
|
||||
|
||||
### 主项目
|
||||
|
||||
```
|
||||
项目/
|
||||
├── Views/ # 视图层
|
||||
│ ├── MainWindow.xaml
|
||||
│ ├── AppSplashScreen.xaml
|
||||
│ └── Dialogs/
|
||||
├── ViewModels/ # 视图模型层
|
||||
│ └── MainWindowViewModel.cs
|
||||
├── App.xaml
|
||||
└── NLog.config
|
||||
```
|
||||
|
||||
### Framework 项目
|
||||
|
||||
```
|
||||
项目.Framework/
|
||||
├── Configs/ Controls/ Converters/ Services/
|
||||
├── ViewModelServices/ ViewModels/ Helpers/ MarkupExtensions/
|
||||
├── Mappers/ Utilities/ Interfaces/ Resources/
|
||||
└── Startup.cs
|
||||
```
|
||||
|
||||
### 模块目录
|
||||
|
||||
```
|
||||
项目.<模块名>/
|
||||
├── Startup.cs
|
||||
├── Views/ ├── <模块名>MainView.xaml └── Dialogs/
|
||||
└── ViewModels/
|
||||
```
|
||||
|
||||
## View / ViewModel 命名约定
|
||||
|
||||
以功能名称 `Xxx`(不含 View 后缀)为前缀:
|
||||
|
||||
| 文件类型 | 命名格式 |
|
||||
|---|---|
|
||||
| View 文件 | `XxxView.xaml` |
|
||||
| ViewModel 文件 | `XxxViewModel.cs` |
|
||||
| 对话框 View | `XxxDialog.xaml` |
|
||||
| 对话框 ViewModel | `XxxDialogViewModel.cs` |
|
||||
|
||||
**严禁**将 View 文件名中的 `View` 后缀保留到 ViewModel 文件名中:
|
||||
|
||||
| ❌ 错误 | ✅ 正确 |
|
||||
|---|---|
|
||||
| `AlarmLogViewViewModel.cs` | `AlarmLogViewModel.cs` |
|
||||
| `MainConsoleMainViewViewModel.cs` | `MainConsoleMainViewModel.cs` |
|
||||
|
||||
**推导规则**:从 `{Name}View.xaml` 推导 ViewModel 时,取 `{Name}` 再加 `ViewModel.cs`。
|
||||
|
||||
## ViewModel 继承
|
||||
|
||||
| 场景 | 基类 |
|
||||
|---|---|
|
||||
| 普通页面 | `ViewModelBaseEx` |
|
||||
| 对话框 | `DialogViewModelBase<TParameter>` |
|
||||
|
||||
`UserControlStyle` 与 `ViewModelBaseEx` 强相关,使用其中一个必须搭配另一个。
|
||||
|
||||
```csharp
|
||||
// 普通页面
|
||||
internal class XxxViewModel : ViewModelBaseEx
|
||||
{
|
||||
protected override void LoadedOnce()
|
||||
{
|
||||
// 首次加载初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框
|
||||
internal class XxxDialogViewModel : DialogViewModelBase<XxxDialogParameter>
|
||||
{
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Parameter 和 ParentViewModel 已就绪
|
||||
}
|
||||
|
||||
protected override void Closing(CancelEventArgs args)
|
||||
{
|
||||
// 关闭前校验和保存
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 初始化与主构造函数
|
||||
|
||||
实现接口`ViewModelBase` / `ViewModelBaseEx` 时,**必须优先使用 Primary Constructor** 注入依赖。
|
||||
|
||||
主构造函数没有函数体。当构造阶段还有默认值赋值、事件订阅、集合填充等初始化逻辑时,**禁止**因此退回传统构造函数并手工 `_field = param`;应 override `OnInitializeInRuntime`,把这些逻辑放进去(该方法由基类构造流程调用,时机等同于原构造函数体):
|
||||
|
||||
```csharp
|
||||
internal class XxxViewModel(
|
||||
IXxxService xxxService,
|
||||
IYyyService yyyService
|
||||
) : ViewModelBaseEx
|
||||
{
|
||||
protected override void OnInitializeInRuntime()
|
||||
{
|
||||
base.OnInitializeInRuntime();
|
||||
|
||||
// 原传统构造函数中「除依赖字段赋值外」的初始化逻辑放这里
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 时机 | 方法 | 用途 |
|
||||
|---|---|---|
|
||||
| 构造阶段(基类构造内) | `OnInitializeInRuntime` | 依赖就绪后的同步初始化;支撑主构造函数写法 |
|
||||
| 视图首次加载 | `LoadedOnce` | 依赖 View 已就绪的逻辑 |
|
||||
| 对话框 Parameter 就绪 | `OnInitialized`(Dialog) | 对话框专用,勿与 `OnInitializeInRuntime` 混淆 |
|
||||
|
||||
| |
|
||||
| --- |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
|
||||
## MVVM 属性定义
|
||||
|
||||
**禁止使用** **`field`** **关键字定义绑定属性**,必须用 Prism的 父类 `BindableBase `,`SetProperty`:
|
||||
|
||||
```csharp
|
||||
// ✅ 正确
|
||||
private string _title;
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Command 写法
|
||||
|
||||
使用 `field ??= new ...` 懒加载,handler 与定义共置,禁止分离到文件底部:
|
||||
|
||||
```csharp
|
||||
internal class XxxViewModel : BindableBase
|
||||
{
|
||||
//同步命令
|
||||
public ICommand SaveCommand => field?? new DelegateCommand(Save);
|
||||
|
||||
private void Save()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
// 异步命令
|
||||
public ICommand SaveAsyncCommand => field ?? new AsyncDelegateCommand(SaveAsync);
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DependencyProperty 模板
|
||||
|
||||
```csharp
|
||||
public static readonly DependencyProperty XxxProperty =
|
||||
DependencyProperty.Register(nameof(Xxx), typeof(string), typeof(MyControl), new PropertyMetadata(default(string)));
|
||||
|
||||
public string Xxx
|
||||
{
|
||||
get => (string)GetValue(XxxProperty);
|
||||
set => SetValue(XxxProperty, value);
|
||||
}
|
||||
```
|
||||
|
||||
确认类对话框优先 `IMessageBoxServiceEx.ShowOkCancelWarning` / `ShowOkCancelQuestion`(见 `handycontrol-usage.mdc`),不要默认用 Yes/No。
|
||||
|
||||
## AutoFac 依赖注入
|
||||
|
||||
### 生命周期接口
|
||||
|
||||
| 接口 | 用途 |
|
||||
|---|---|
|
||||
| `ISingletonDependency` | 单例服务 |
|
||||
| `IAppInitializeAsync` | 异步启动初始化 |
|
||||
| `IAppInitialize` | 同步启动初始化 |
|
||||
| `IAppClosedHandler` | 应用关闭时清理(已带单例生命周期;`AppClosed(AppCloseParameter)`,按 Priority 在关窗前调用) |
|
||||
|
||||
```csharp
|
||||
// 单例服务
|
||||
internal class XxxService(ILogger<XxxService> logger) : IXxxService, ISingletonDependency
|
||||
{
|
||||
}
|
||||
|
||||
// 带异步初始化的单例
|
||||
internal class XxxService(IStatusReport statusReport) : IXxxService, IAppInitializeAsync
|
||||
{
|
||||
string IAppInitializeAsync.Description { get; } = "初始化 Xxx";
|
||||
int IPriority.Priority { get; } = IntPriority.Normal;
|
||||
|
||||
async Task IAppInitializeAsync.AppInitializeAsync(IStatusReport statusReport)
|
||||
{
|
||||
// 初始化逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 应用关闭清理
|
||||
internal class XxxService : IXxxService, IAppClosedHandler
|
||||
{
|
||||
int IAppClosedHandler.Priority { get; } = IntPriority.Normal;
|
||||
string IAppClosedHandler.Description { get; } = "关闭 Xxx";
|
||||
|
||||
void IAppClosedHandler.AppClosed(AppCloseParameter parameter)
|
||||
{
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注入方式
|
||||
|
||||
优先使用 Primary Constructor。ViewModel 若有构造阶段初始化逻辑,放到 `OnInitializeInRuntime`(见「初始化与主构造函数」),不要为此改用传统构造函数:
|
||||
|
||||
```csharp
|
||||
internal class XxxViewModel(IConfigService configService) : ViewModelBaseEx
|
||||
{
|
||||
// 直接使用 configService
|
||||
}
|
||||
```
|
||||
|
||||
## IDialogService
|
||||
|
||||
```csharp
|
||||
|
||||
internal class XxxViewModel(IMessageBoxService messageBoxService) : ViewModelBaseEx
|
||||
{
|
||||
private void ShowWarning()
|
||||
{
|
||||
messageBoxService.Show("操作成功", "提示", MessageButton.OK, MessageIcon.Information);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
复杂对话框使用 `add-dialog` skill 创建自定义 DialogService。
|
||||
|
||||
## LoadingStatus
|
||||
|
||||
异步加载统一用 `LoadingStatus`,禁止自建 `IsBusy`/`HasError`:
|
||||
|
||||
```csharp
|
||||
internal class XxxViewModel : ViewModelBaseEx
|
||||
{
|
||||
public LoadingStatus LoadingStatus { get; } = new();
|
||||
|
||||
public IDelegateCommand LoadCommand => field ??= new AsyncCommand(Load);
|
||||
private async Task Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadingStatus.Initialize("读取文件...");
|
||||
await DoStuff();
|
||||
LoadingStatus.Done();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LoadingStatus.Reset();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
LoadingStatus.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
XAML 中用 `<f:LoadingCover LoadingStatus="{Binding LoadingStatus}" RefreshCommand="{Binding LoadCommand}"/>` 绑定。API 详情见 `reference-loading-status` skill。
|
||||
|
||||
## 表格控件选择
|
||||
|
||||
| 场景 | 控件 |
|
||||
|---|---|
|
||||
| 普通配置表格、数据库记录 | 常规 DataGrid |
|
||||
| 百万行级别大数据 | MAX.FastWpfGrid + FastGridModelBase |
|
||||
|
||||
MAX.FastWpfGrid 完整用法见 `reference-fast-wpf-grid` skill(**禁止**旧包名 `FastWpfGrid`)。
|
||||
|
||||
## 文件对话框服务
|
||||
|
||||
**禁止** `Microsoft.Win32.OpenFileDialog` / `SaveFileDialog`,用 DevExpress 服务:
|
||||
|
||||
```xml
|
||||
<!-- XAML 注册 -->
|
||||
<dxmvvm:Interaction.Behaviors>
|
||||
<dxmvvm:OpenFileDialogService Name="OpenFileDialog"/>
|
||||
<dxmvvm:SaveFileDialogService Name="SaveFileDialog"/>
|
||||
</dxmvvm:Interaction.Behaviors>
|
||||
```
|
||||
|
||||
```csharp
|
||||
// ViewModel 使用
|
||||
public IOpenFileDialogService OpenFileDialog => GetService<IOpenFileDialogService>(nameof(OpenFileDialog));
|
||||
public ISaveFileDialogService SaveFileDialog => GetService<ISaveFileDialogService>(nameof(SaveFileDialog));
|
||||
|
||||
private void OpenFile()
|
||||
{
|
||||
if (OpenFileDialog.ShowDialog(OnFileOpen))
|
||||
{
|
||||
// 文件已选择
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFileOpen(CancelEventArgs args, IFileInfo fileInfo)
|
||||
{
|
||||
string filePath = fileInfo.GetFullName();
|
||||
}
|
||||
```
|
||||
|
||||
## 文件夹选择
|
||||
|
||||
**必须使用** `AppFileHelper.PickFolder`,禁止 WinForms `FolderBrowserDialog`:
|
||||
|
||||
```csharp
|
||||
using PipeReviewReport.Framework.Helpers;
|
||||
|
||||
if (AppFileHelper.PickFolder("选择文件夹", Environment.GetFolderPath(Environment.SpecialFolder.Desktop), out string path))
|
||||
{
|
||||
FolderPath = path;
|
||||
}
|
||||
|
||||
// 简化重载
|
||||
if (AppFileHelper.PickFolder(out string path)) { FolderPath = path; }
|
||||
```
|
||||
|
||||
## 输入框长度限制
|
||||
|
||||
**强制遵守:** 用户能打字的控件都必须**显式**写 `MaxLength`,禁止省略。用 `{x:Static shared:InputLimits.Xxx}`,禁止写裸数字。
|
||||
|
||||
| 常量 | 值 | 何时用 |
|
||||
|---|---|---|
|
||||
| `InputLimits.Path` | 100 | 会变成文件名、文件夹名、路径一部分的名称 |
|
||||
| `InputLimits.Text` | 250 | 显示名、描述、备注 |
|
||||
|
||||
完整路径**不要手输**,用 `AppFileHelper.PickFolder`;展示用 `TextBlock` 或只读 `TextBox`,不要用 `MaxLength` 去卡系统路径长度(长路径远超 260)。
|
||||
|
||||
```xml
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Path}"
|
||||
Text="{Binding FolderName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Text}"
|
||||
Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
```
|
||||
|
||||
XAML 细则见 `wpf-xaml.mdc`。
|
||||
|
||||
## 数据映射(忽略)
|
||||
|
||||
使用 Mapster:
|
||||
|
||||
```csharp
|
||||
using PipeReviewReport.Shared;
|
||||
|
||||
var dto = model.Adapt<XxxDto>();
|
||||
var model = dto.Adapt<XxxModel>();
|
||||
```
|
||||
|
||||
## 资源引用(忽略)
|
||||
|
||||
```csharp
|
||||
var icon = ImageResources.Icon_XXX_PNG;
|
||||
var text = AppStrings.Xxx_Title;
|
||||
var color = CustomColors.AccentColor;
|
||||
```
|
||||
@@ -0,0 +1,381 @@
|
||||
---
|
||||
description: XAML 视图编写约定(头部模板、数据绑定、输入限长、BindingProxy、DRY、颜色、布局注释、枚举绑定等),编辑 .xaml 文件时生效
|
||||
alwaysApply: false
|
||||
globs: "**/*.xaml"
|
||||
---
|
||||
|
||||
# XAML 视图编写约定
|
||||
|
||||
## View XAML 头部模板
|
||||
|
||||
```XML
|
||||
<UserControl
|
||||
x:Class="xxxxx.(xxxx)?.Views.XxxView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:xxxx.(xxxx)?.Views"
|
||||
xmlns:core="MAX.Core"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
xmlns:s="clr-namespace:xxxx.xxx;assembly=xxx.xxx"
|
||||
d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"
|
||||
Style="{DynamicResource UserControlStyle}"
|
||||
DataContext="{core:DISource Type=local:XxxViewModel}"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
```
|
||||
|
||||
## Dialog View XAML
|
||||
|
||||
```XML
|
||||
<UserControl
|
||||
xmlns:core="MAX.Core"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
d:Background="White"
|
||||
d:DataContext="{d:DesignInstance Type=local:DialogViewModel}"
|
||||
Style="{StaticResource DialogStyle.Base}"
|
||||
DataContext="{core:DISource Type=local:DialogViewModel}">
|
||||
```
|
||||
|
||||
- `{core:DISource Type=local:XxxViewModel}` 绑定 ViewModel
|
||||
- `d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"` 提供设计时数据
|
||||
- `UserControlStyle` 与 `ViewModelBaseEx` 强相关,必须搭配使用
|
||||
|
||||
## BindingProxy + CommandProxy(DataTemplate/ContextMenu 绑定)
|
||||
|
||||
**在 `DataTemplate`、`ContextMenu`、`DataGridTemplateColumn.CellTemplate` 中禁止使用 `RelativeSource AncestorType`,必须用 `BindingProxy` + `CommandProxy`。**
|
||||
|
||||
### UserControl.Resources 注册
|
||||
|
||||
```xml
|
||||
<UserControl.Resources>
|
||||
<!-- ViewModel 代理:用于访问 ViewModel 的属性(如 SelectedItem) -->
|
||||
<wpf:BindingProxy x:Key="ViewModel" Data="{Binding}"/>
|
||||
<!-- 命令代理:用于在模板内引用 ViewModel 的命令 -->
|
||||
<wpf:CommandProxy x:Key="ToggleCurveCommand" Command="{Binding ToggleCurveCommand}"/>
|
||||
<wpf:CommandProxy x:Key="ShowAllCurvesCommand" Command="{Binding ShowAllCurvesCommand}"/>
|
||||
</UserControl.Resources>
|
||||
```
|
||||
|
||||
**`BindingProxy` 映射关系**:`Data="{Binding}"` 把 ViewModel 存到 `BindingProxy.Data`。引用时 `Path=Data.SelectedItem` 中 `Data` 对应 `BindingProxy.Data`(即 ViewModel)。
|
||||
|
||||
### DataTemplate(CellTemplate)中
|
||||
|
||||
Command 用 CommandProxy,行数据用 `{Binding}`:
|
||||
|
||||
```xml
|
||||
<Button Command="{StaticResource ToggleCurveCommand}" CommandParameter="{Binding}"/>
|
||||
```
|
||||
|
||||
### ContextMenu 中
|
||||
|
||||
Command 用 CommandProxy,CommandParameter 用 BindingProxy 取 ViewModel 属性:
|
||||
|
||||
```xml
|
||||
<MenuItem
|
||||
Command="{StaticResource ToggleCurveCommand}"
|
||||
CommandParameter="{Binding Source={StaticResource ViewModel}, Path=Data.SelectedItem}"
|
||||
Header="显示/隐藏"/>
|
||||
```
|
||||
|
||||
### DataGrid 右键菜单:两种模式
|
||||
|
||||
- **行右键菜单**(点击在某行上):通过 `DataGrid.RowStyle` 设置 `ContextMenu`,`CommandParameter="{Binding}"`
|
||||
- **DataGrid 通用右键菜单**(不要求点在行上):通过 `DataGrid.ContextMenu`,`CommandParameter` 用 `BindingProxy` 取 `SelectedItem`
|
||||
|
||||
```xml
|
||||
<!-- 行右键 -->
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow" BasedOn="{StaticResource DataGridRow}">
|
||||
<Setter Property="ContextMenu">
|
||||
<Setter.Value>
|
||||
<ContextMenu>
|
||||
<MenuItem Command="{StaticResource ToggleCurveCommand}" CommandParameter="{Binding}" Header="显示/隐藏"/>
|
||||
<MenuItem Command="{StaticResource ShowAllCurvesCommand}" Header="全部显示"/>
|
||||
</ContextMenu>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
|
||||
<!-- 通用右键 -->
|
||||
<DataGrid.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem
|
||||
Command="{StaticResource ToggleCurveCommand}"
|
||||
CommandParameter="{Binding Source={StaticResource ViewModel}, Path=Data.SelectedItem}"
|
||||
Header="显示/隐藏"/>
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
```
|
||||
|
||||
### 禁止写法
|
||||
|
||||
```xml
|
||||
<!-- ❌ 禁止 RelativeSource AncestorType -->
|
||||
<MenuItem Command="{Binding DataContext.XXX, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
||||
<Button Command="{Binding DataContext.XXX, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
||||
<MenuItem CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
```
|
||||
|
||||
## XAML DRY 原则
|
||||
|
||||
重复的 XAML 控件组合必须提取,按优先级:
|
||||
|
||||
| 重复类型 | 提取方式 |
|
||||
|---|---|
|
||||
| 相同控件 + 属性(多处) | `Style`(TargetType)或 `ControlTemplate` |
|
||||
| 相同布局块(多视图) | 独立 `UserControl` |
|
||||
| 仅当前 View 内重复 | `DataTemplate` + `ContentControl` |
|
||||
| 相同 Converter 组合 | 封装为单一 `IMultiValueConverter` |
|
||||
|
||||
## 颜色写法
|
||||
|
||||
**首选主题资源键**,而不是硬编码色值。主色、背景、边框、文字、语义色、缺陷等级色
|
||||
全部已在 `CustomColors.xaml` 中定义,完整键名表见 `handycontrol-usage.mdc` 的「配色覆盖机制」:
|
||||
|
||||
```xml
|
||||
<!-- ✅ 走主题资源键,改配色只需改一处 -->
|
||||
<Border Background="{DynamicResource RegionBrush}" BorderBrush="{DynamicResource BorderBrush}"/>
|
||||
<TextBlock Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Border Background="{DynamicResource TitleBarBrush}"/>
|
||||
|
||||
<!-- ❌ 硬编码主题色,改配色要满仓库搜替换 -->
|
||||
<Border Background="#c8ddee"/>
|
||||
```
|
||||
|
||||
确实需要写死色值时(一次性的装饰、遮罩等),用十六进制字符串,
|
||||
禁止 `Color.FromRgb()`/`Color.FromArgb()`:
|
||||
|
||||
```xml
|
||||
<!-- ✅ -->
|
||||
<Rectangle Fill="#80000000"/>
|
||||
<!-- AA 为 0xFF 时可省略 -->
|
||||
<Border Background="#FF5733"/>
|
||||
```
|
||||
|
||||
C# 侧:`"#FF5733".toColor()`、`"#80000000".toColor()`。
|
||||
|
||||
## 绑定模式(Mode)与更新触发(UpdateSourceTrigger)
|
||||
|
||||
### 双向绑定:必须加 UpdateSourceTrigger=PropertyChanged
|
||||
|
||||
**默认情况下 WPF 双向绑定在控件失去焦点时才更新源属性**,这会导致 ViewModel 属性值与 UI 不同步。所有双向绑定的属性必须在 Binding 中明确添加 `UpdateSourceTrigger=PropertyChanged`,确保输入内容即时同步:
|
||||
|
||||
```xml
|
||||
<!-- ✅ 双向绑定加 UpdateSourceTrigger -->
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Text}"
|
||||
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Text}"
|
||||
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<!-- ❌ 缺失 UpdateSourceTrigger,失焦后才更新 ViewModel -->
|
||||
<TextBox Text="{Binding UserName}"/>
|
||||
```
|
||||
|
||||
### 单向绑定:必须加 Mode=OneWay
|
||||
|
||||
如果设计上某绑定不需要回写数据,**必须显式加 `Mode=OneWay`**,避免双向绑定产生的警告和不必要开销:
|
||||
|
||||
```xml
|
||||
<!-- ✅ IsReadOnly 控件加 Mode=OneWay -->
|
||||
<TextBox Text="{Binding StatusText, Mode=OneWay}" IsReadOnly="True"/>
|
||||
<TextBlock Text="{Binding Description, Mode=OneWay}"/>
|
||||
```
|
||||
|
||||
### Run 绑定:强制 OneWay
|
||||
|
||||
**`<TextBlock>` 的 `<Run>` 绑定时必须加 `Mode=OneWay`**,否则会引发警告:
|
||||
|
||||
```xml
|
||||
<!-- ✅ 正确:Run 必须加 Mode=OneWay -->
|
||||
<TextBlock>
|
||||
<Run Text="{Binding UserName, Mode=OneWay}"/>
|
||||
<Run Text="{Binding Timestamp, Mode=OneWay}"/>
|
||||
</TextBlock>
|
||||
|
||||
<!-- ❌ 错误:Run 未指定 Mode,产生警告 -->
|
||||
<TextBlock>
|
||||
<Run Text="{Binding UserName}"/>
|
||||
<Run Text="{Binding Timestamp}"/>
|
||||
</TextBlock>
|
||||
```
|
||||
|
||||
## 输入框长度限制
|
||||
|
||||
**强制遵守:** 用户能打字的 `TextBox`、`PasswordBox`、`hc:TextBox`、`hc:PasswordBox`、`hc:SearchBar` 以及 DataGrid 文本编辑框**必须显式写 `MaxLength`**。禁止省略,禁止写裸数字。选择器选出的完整路径只读展示除外。
|
||||
|
||||
| 常量 | 值 | 何时用 |
|
||||
|---|---|---|
|
||||
| `InputLimits.Path` | 100 | 值会变成文件名、文件夹名、或拼进路径 |
|
||||
| `InputLimits.Text` | 250 | 显示名、描述、标题、备注、搜索关键字 |
|
||||
|
||||
完整路径不要手输、不要用 `MaxLength` 去卡:用 `AppFileHelper.PickFolder`,展示用 `TextBlock` 或 `IsReadOnly` 的 `TextBox`。
|
||||
|
||||
```xml
|
||||
<!-- ✅ 路径片段 -->
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Path}"
|
||||
Text="{Binding ProjectName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<!-- ✅ 普通文本 -->
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Text}"
|
||||
Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<!-- ✅ 选择器选出的完整路径:只读展示,不写 MaxLength -->
|
||||
<TextBox Text="{Binding FolderPath, Mode=OneWay}" IsReadOnly="True"/>
|
||||
|
||||
<!-- ✅ 密码框 / 搜索框同样要写 -->
|
||||
<PasswordBox MaxLength="{x:Static shared:InputLimits.Text}"/>
|
||||
<hc:SearchBar MaxLength="{x:Static shared:InputLimits.Text}" Text="{Binding Keyword, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<!-- ❌ 禁止:没有 MaxLength -->
|
||||
<TextBox Text="{Binding FolderName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<!-- ❌ 禁止:裸数字 -->
|
||||
<TextBox MaxLength="250" Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
```
|
||||
|
||||
`NumericUpDown` 已自带数字长度,不要再改成 250。可编辑 `ComboBox` 尽量改用 `TextBox`;若必须可编辑,给内部编辑框同样加上 `MaxLength`。日志 / JSON / 大段自由文本才允许不限长,且必须在旁边注释原因。
|
||||
|
||||
## Style/Triggers 使用原则
|
||||
|
||||
优先用 `Binding` 或 `MultiBinding+Converter` 控制可见性。仅以下场景用 `Style/Triggers`:
|
||||
|
||||
- 需同时设置多个 `Setter`(Visibility + Opacity + Background 等)
|
||||
- 颜色相关的条件控制
|
||||
- 不写 Style 就要新建 Converter 的复杂场景
|
||||
|
||||
```xml
|
||||
<!-- ✅ Binding -->
|
||||
<TextBlock Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<!-- ✅ 多属性场景用 Trigger -->
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Setter Property="Opacity" Value="1.0"/>
|
||||
<Setter Property="Background" Value="#FF5733"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
|
||||
## 布局注释
|
||||
|
||||
主要布局分块/区域前加 `<!-- -->` 注释标明用途。复杂视图(多面板、多 Tab、对话框)必须注释;简单单控件视图可省略:
|
||||
|
||||
```xml
|
||||
<Grid>
|
||||
<!-- 顶部工具栏 -->
|
||||
<Border Grid.Row="0"> ... </Border>
|
||||
<!-- 左侧导航树 -->
|
||||
<TreeView Grid.Column="0"/>
|
||||
<!-- 主内容区 -->
|
||||
<ContentControl Grid.Column="1" Content="{Binding CurrentView}"/>
|
||||
<!-- 底部状态栏 -->
|
||||
<StatusBar Grid.Row="2"/>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
## LoadingCover / LoadingStatusIndicator
|
||||
|
||||
```xml
|
||||
<!-- 在内容 Grid 之上放置 LoadingCover 作为覆盖层 -->
|
||||
<Grid>
|
||||
<ContentControl Content="{Binding ...}"/>
|
||||
<f:LoadingCover LoadingStatus="{Binding LoadingStatus}" RefreshCommand="{Binding LoadCommand}"/>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
`LoadingCover` 三态:
|
||||
- 加载中:半透明遮罩 + 环形进度 + Message 文字
|
||||
- 错误:半透明遮罩 + 警告图标 + ErrorMessage + 刷新按钮 + 复制错误按钮
|
||||
- 成功:完全隐藏
|
||||
|
||||
轻量级内联状态:
|
||||
|
||||
```xml
|
||||
<f:LoadingStatusIndicator LoadingStatus="{Binding LoadingStatus}"/>
|
||||
```
|
||||
|
||||
## 枚举绑定
|
||||
|
||||
### EnumDisplayConverter(XAML)
|
||||
|
||||
```xml
|
||||
<UserControl.Resources>
|
||||
<conv:EnumDisplayConverter x:Key="EnumDisplayConverter"/>
|
||||
</UserControl.Resources>
|
||||
|
||||
<!-- 直接绑定枚举值 -->
|
||||
<TextBlock Text="{Binding Status, Converter={StaticResource EnumDisplayConverter}}"/>
|
||||
```
|
||||
|
||||
### ComboBox 绑定枚举(推荐:ViewModel 提供列表)
|
||||
|
||||
```csharp
|
||||
// ViewModel
|
||||
public IReadOnlyList<MyStatus> StatusList { get; } = Enum.GetValues(typeof(MyStatus)).Cast<MyStatus>().ToList();
|
||||
|
||||
public MyStatus SelectedStatus
|
||||
{
|
||||
get => GetProperty(() => SelectedStatus);
|
||||
set => SetProperty(() => SelectedStatus, value);
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<ComboBox ItemsSource="{Binding StatusList}"
|
||||
SelectedItem="{Binding SelectedStatus}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding ., Converter={StaticResource EnumDisplayConverter}}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
```
|
||||
|
||||
### C# 获取枚举显示文本
|
||||
|
||||
```csharp
|
||||
string displayText = MyStatus.Active.GetEnumDisplay();
|
||||
```
|
||||
|
||||
EnumDisplayService 内部实现和自定义逻辑详见 `reference-enum-display` skill。
|
||||
|
||||
## MarkupExtension 示例
|
||||
|
||||
### IconEnumExtension(返回 ImageSource)
|
||||
|
||||
```csharp
|
||||
[MarkupExtensionReturnType(typeof(ImageSource))]
|
||||
public class IconEnumExtension : MarkupExtension
|
||||
{
|
||||
[ConstructorArgument("icon")]
|
||||
public IconNameEnum Icon { get; set; }
|
||||
|
||||
public IconEnumExtension() { }
|
||||
public IconEnumExtension(IconNameEnum icon) { Icon = icon; }
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return Icon.GetImageSource();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<Image Source="{f:IconEnumExtension Icon=Check1}" Width="20" Height="20"/>
|
||||
```
|
||||
|
||||
### MenuItem.Icon 专用扩展(详见 `icon-usage` 规则)
|
||||
|
||||
- **`ImageIconExtension`**:直接返回 `Image` 控件(彩色矢量图),用于 `MenuItem.Icon`
|
||||
- **`GlyphIconExtension`**:直接返回 `IconText` 控件(文字图标,支持 Foreground 调色),用于 `MenuItem.Icon`
|
||||
|
||||
```xml
|
||||
<MenuItem Header="HELLO" Icon="{f:ImageIcon Icon=Folder}"/>
|
||||
<MenuItem Header="WORLD" Icon="{f:GlyphIcon Icon=GuestAdd, Foreground=Red}"/>
|
||||
```
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: add-dialog
|
||||
description: >-
|
||||
在 PipeReviewReport 客户端中新建一个弹窗对话框,包含 Parameter record、`DialogViewModelOkCancel` 和调用方的注册写法。
|
||||
当用户需要新建弹窗、编辑对话框、确认框时使用。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# 新建弹窗对话框
|
||||
|
||||
## 文件组织
|
||||
|
||||
Dialog 的所有代码(UserControl、Parameter、ViewModel)写在**同一个 `.xaml.cs` 文件**中,放在 `Views/Dialogs/` 目录下。
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Dialog 文件(`.xaml.cs`)
|
||||
|
||||
```csharp
|
||||
namespace PipeReviewReport.<模块名>.Views.Dialogs;
|
||||
|
||||
// UserControl code-behind(保持最简)
|
||||
public partial class XxxDialog : UserControl
|
||||
{
|
||||
public XxxDialog() => InitializeComponent();
|
||||
}
|
||||
|
||||
// Parameter:record class,构造函数参数 = 传入值,属性 = 传出值
|
||||
internal record XxxDialogParameter(XxxItem Item)
|
||||
{
|
||||
public XxxItem? Result { get; set; }
|
||||
}
|
||||
|
||||
// ViewModel
|
||||
internal class XxxDialogViewModel(
|
||||
// 注入所需服务
|
||||
) : DialogViewModelOkCancel<XxxDialogParameter>
|
||||
{
|
||||
// 绑定到 View 的数据
|
||||
public SomeModel EditTarget { get; } = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Parameter 和 ParentViewModel 均已就绪,在此读取传入值
|
||||
ConfirmDialogCommand.Content = "保存";
|
||||
CancelDialogCommand.Content = "取消";
|
||||
if (Parameter.Item != null)
|
||||
{
|
||||
DialogTitle = "编辑";
|
||||
// 将 Parameter.Item 映射到 EditTarget
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogTitle = "新增";
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Validate(out string message)
|
||||
{
|
||||
// 校验;失败时 message 非空并 return false
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void ShowErrorDialog(string message)
|
||||
{
|
||||
// 基类默认用 System.Windows.MessageBox,业务弹窗请覆盖为 MessageBoxService
|
||||
MessageBoxService.ShowWarning(message);
|
||||
}
|
||||
|
||||
protected override bool OnConfirmed()
|
||||
{
|
||||
// 将结果写回 Parameter;唯一性等异步校验建议放调用方
|
||||
Parameter.Result = EditTarget;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Dialog View(XAML)
|
||||
|
||||
先读取 `Source/XAML_Preset.txt` 中 `-------------- Dialog --------------` 区段复制 xmlns,注意:
|
||||
- `Style="{StaticResource DialogStyle.Base}"`(`StaticResource`,不是 `DynamicResource`)
|
||||
- `DataContext="{core:DISource Type=local:XxxDialogViewModel}"`
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — 调用方 View(XAML)注册
|
||||
|
||||
在调用方 View 的 XAML 中,用 `f:DialogService` 注册弹窗:
|
||||
|
||||
```xml
|
||||
<dxmvvm:Interaction.Behaviors>
|
||||
<f:DialogService Name="XxxDialog" ContentType="{x:Type dialogs:XxxDialog}"/>
|
||||
</dxmvvm:Interaction.Behaviors>
|
||||
```
|
||||
|
||||
其中 `dialogs` 命名空间需在文件头声明:
|
||||
```xml
|
||||
xmlns:dialogs="clr-namespace:PipeReviewReport.<模块名>.Views.Dialogs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — 调用方 ViewModel 注册与调用
|
||||
|
||||
```csharp
|
||||
// 注册服务(属性名必须和 XAML 中 Name 一致)
|
||||
public IDialogServiceEx XxxDialog => GetService<IDialogServiceEx>(nameof(XxxDialog));
|
||||
|
||||
// 打开弹窗(OK / Cancel)
|
||||
public IDelegateCommand AddCommand => field ??= new AsyncCommand(Add);
|
||||
private async Task Add()
|
||||
{
|
||||
XxxDialogParameter parameter = new(null);
|
||||
if (XxxDialog.ShowOKCancel(this, parameter))
|
||||
{
|
||||
// 用户点了 OK,从 parameter.Result 取结果
|
||||
XxxItem result = parameter.Result;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用方法**(`IDialogServiceEx`):
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `ShowOKCancel(parentVM, parameter)` | OK / Cancel,返回 `bool`(true = OK) |
|
||||
| `ShowDialog(parentVM, parameter)` | 返回 `DialogResult`(含命令) |
|
||||
|
||||
---
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] Parameter 使用 `record` 类型,构造参数为传入值,属性为传出值
|
||||
- [ ] ViewModel 继承 `DialogViewModelOkCancel<TParameter>`(或 `DialogViewModelOk` / `DialogViewModel`)
|
||||
- [ ] 传出结果在 `OnConfirmed()` 中写入 `Parameter.Result`
|
||||
- [ ] 覆盖 `ShowErrorDialog`,走 `MessageBoxService` 而非 `System.Windows.MessageBox`
|
||||
- [ ] Dialog View 使用 `Style="{StaticResource DialogStyle.Base}"`
|
||||
- [ ] 调用方 XAML 已声明 `f:DialogService`,`Name` 与 ViewModel 属性名一致
|
||||
- [ ] 调用方 ViewModel 属性名与 `nameof(XxxDialog)` 一致
|
||||
- [ ] 异步唯一性校验放在调用方(弹窗 `OnConfirmed` 保持同步)
|
||||
- [ ] 对话框内输入框已显式 `MaxLength="{x:Static shared:InputLimits.Path/Text}"`(完整路径用选择器)
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: add-list-page
|
||||
description: >-
|
||||
在 PipeReviewReport 客户端中新建同构列表页(KPI + FilterBar + DataGrid + Pagination +
|
||||
列配置 + LoadingCover)。Round 5 项目管理页是标准样板;视频/缺陷列表复用同一骨架。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# 新建列表页(KPI + 筛选 + 表格)
|
||||
|
||||
本项目多个业务列表页结构同构。以 `PipeReviewReport.Projects` 的
|
||||
`ProjectsMainView` / `ProjectsMainViewModel` 为样板,**组装**而非重写。
|
||||
|
||||
## 骨架结构
|
||||
|
||||
```
|
||||
page-head(标题 + 刷新 / 导出 / 新建)
|
||||
KPI × N(f:KpiCard)
|
||||
f:FilterBar(左筛选 + 右列配置)
|
||||
卡片:DataGrid + hc:Pagination + f:LoadingCover
|
||||
```
|
||||
|
||||
## Framework 可复用件(禁止模块内再写一份)
|
||||
|
||||
| 组件 | 命名空间 / 路径 |
|
||||
|---|---|
|
||||
| `KpiCard` | `Framework/Controls/KpiCard` |
|
||||
| `FilterBar` | `Framework/Controls/FilterBar`(`Filters` / `Actions` 槽) |
|
||||
| `LoadingCover` | `Framework/Controls/LoadingCover` |
|
||||
| `ColumnConfigDialog` | `Framework/Views/Dialogs/ColumnConfigDialog` |
|
||||
| `IColumnConfigService` | `GetVisibleColumnsAsync` / `SaveVisibleColumnsAsync` |
|
||||
| `IExcelExportService` | 动态列头导出 xlsx |
|
||||
| `ColumnDefinition` / `ExcelColumn` | `Framework/Models` |
|
||||
| `hc:Pagination` / `hc:SearchBar` / `hc:Tag` / `hc:Empty` | HandyControl |
|
||||
|
||||
## ViewModel 约定
|
||||
|
||||
1. Primary Constructor 注入数据服务;`OnInitializeInRuntime` 填筛选项默认值;`LoadedOnce` 拉数。
|
||||
2. `LoadingStatus` + `f:LoadingCover`;禁止自建 `IsBusy`。
|
||||
3. 分页:`PageIndex` / `MaxPageCount` + `PagedQueryHelper`;写回页码时用 `suppressPageReload` 防递归。
|
||||
4. 筛选变更 → 页码归 1 → 重查;搜索用 `hc:SearchBar.Command`。
|
||||
5. 列可见性:模块内静态 `XxxListColumns.All` / `DefaultVisible` / `ResolveVisible`;VM 暴露 `ColXxxVisibility`;页面资源放 `f:BindingProxy Data="{Binding}"`,DataGrid 列用 `Visibility="{Binding Data.ColXxxVisibility, Source={StaticResource VmProxy}}"`(**禁止** `x:Reference` 绑到 Root,会循环依赖)。
|
||||
6. 行内命令:`RelativeSource AncestorType=DataGrid` 取页面 DataContext,不要用 `x:Reference Root`。
|
||||
7. 弹窗:`DialogViewModelOkCancel` + `f:DialogService` + `ShowOKCancel`(见 `add-dialog` skill)。
|
||||
8. 导出:`SaveFileDialogService` → 按当前筛选拉全量(设上限)→ 仅导出可见列。
|
||||
|
||||
## XAML 注册示例
|
||||
|
||||
```xml
|
||||
<UserControl x:Name="Root" Style="{DynamicResource UserControlStyle}" ...>
|
||||
<dxmvvm:Interaction.Behaviors>
|
||||
<f:DialogService x:Name="XxxEditDialog" ContentType="{x:Type dialogs:XxxEditDialog}"/>
|
||||
<f:DialogService x:Name="ColumnConfigDialog" ContentType="{x:Type f:ColumnConfigDialog}"/>
|
||||
<dxmvvm:SaveFileDialogService x:Name="SaveFileDialog"/>
|
||||
</dxmvvm:Interaction.Behaviors>
|
||||
...
|
||||
<f:FilterBar>
|
||||
<f:FilterBar.Filters><!-- SearchBar + ComboBox --></f:FilterBar.Filters>
|
||||
<f:FilterBar.Actions>
|
||||
<Button Command="{Binding ColumnConfigCommand}" Content="列配置"/>
|
||||
</f:FilterBar.Actions>
|
||||
</f:FilterBar>
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
## 列配置调用
|
||||
|
||||
```csharp
|
||||
ColumnConfigDialogParameter parameter = new(DataScope.Xxx, XxxListColumns.All, visibleKeys.ToList());
|
||||
if (ColumnConfigDialog.ShowOKCancel(this, parameter) && parameter.Result is not null)
|
||||
{
|
||||
List<string> resolved = XxxListColumns.ResolveVisible(parameter.Result);
|
||||
await columnConfigService.SaveVisibleColumnsAsync(DataScope.Xxx, resolved);
|
||||
ApplyVisibleColumns(resolved);
|
||||
}
|
||||
```
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 用了 `KpiCard` / `FilterBar` / `LoadingCover` / Framework `ColumnConfigDialog`
|
||||
- [ ] 列 Key 与导出 `ExcelColumn.Key` 一致
|
||||
- [ ] 空列表有 `hc:Empty`
|
||||
- [ ] 删除走 `IMessageBoxServiceEx.ShowOkCancelWarning`(确认框优先 OK/Cancel,见 `handycontrol-usage.md`)
|
||||
- [ ] 无 `var`;MVVM 属性用 `GetProperty`/`SetProperty`;Command 用 `field ??=`
|
||||
- [ ] 输入框已显式 `MaxLength="{x:Static shared:InputLimits.Path/Text}"`(搜索框用 Text)
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: add-module
|
||||
description: >-
|
||||
在 PipeReviewReport 客户端中添加一个新的功能模块,包含完整的目录结构、AppModule 元数据、
|
||||
主视图和 ViewModel。当用户需要新建客户端模块时使用。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# 添加客户端功能模块
|
||||
|
||||
## 目录结构
|
||||
|
||||
在 `Source/Modules/<模块名>/` 下创建:
|
||||
|
||||
```
|
||||
PipeReviewReport.<模块名>/
|
||||
├── PipeReviewReport.<模块名>.csproj
|
||||
├── AppModule.cs
|
||||
├── Views/
|
||||
│ ├── <模块名>MainView.xaml
|
||||
│ ├── <模块名>MainView.xaml.cs
|
||||
│ ├── Dialogs/ # 可选:弹窗(Parameter + ViewModel 同 .xaml.cs)
|
||||
│ └── Steps/ # 可选:向导步骤 UserControl(可共用父 VM)
|
||||
└── ViewModels/
|
||||
└── <模块名>MainViewModel.cs
|
||||
```
|
||||
|
||||
复杂业务服务优先放 `Framework/Services/`(模块间共享);仅本模块私有的可放模块 `Services/`。
|
||||
|
||||
## Step 1 — 创建 .csproj
|
||||
|
||||
参考现有模块的 `.csproj`,核心是引用 Framework 项目。
|
||||
|
||||
## Step 2 — AppModule.cs
|
||||
|
||||
实现 `IAppModule` 接口:
|
||||
|
||||
```csharp
|
||||
using PipeReviewReport.Shared;
|
||||
using PipeReviewReport.<模块名>.Views;
|
||||
using PipeReviewReport.Framework.Interfaces;
|
||||
using PipeReviewReport.Resources.Fonts;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace PipeReviewReport.<模块名>;
|
||||
|
||||
public class AppModule : IAppModule
|
||||
{
|
||||
public string Key { get; } = ConstKeys.SK_XXX;
|
||||
public string DisplayName { get; } = "XXX(简短说明)";
|
||||
public string FullName { get; } = "模块完整中文名";
|
||||
public string EnglishFullName { get; } = "Module English Name";
|
||||
public string Description { get; } = "模块功能描述。";
|
||||
|
||||
// 用字体图标:侧栏选中态可随 Foreground 变白
|
||||
public IconNameEnum Icon { get; } = IconNameEnum.Folder;
|
||||
public ContentControl Content { get; } = new <模块名>MainView();
|
||||
|
||||
public bool IsEnabled { get; } = true;
|
||||
public int Order { get; } = 10;
|
||||
}
|
||||
```
|
||||
|
||||
- `Key`:从 `PipeReviewReport.Shared.ConstKeys` 取对应的 `SK_XXX` 常量
|
||||
- `Icon`:用 `IconNameEnum` 字体图标(不要用 PNG,否则侧栏选中态无法变色)
|
||||
- `Order`:侧栏排序,数值越小越靠上;现有模块按 10/20/…/80 间隔,方便插入
|
||||
|
||||
## Step 3 — 主 ViewModel
|
||||
|
||||
```csharp
|
||||
using PipeReviewReport.Framework.ViewModels;
|
||||
|
||||
namespace PipeReviewReport.<模块名>.ViewModels;
|
||||
|
||||
internal class <模块名>MainViewModel : ViewModelBaseEx
|
||||
{
|
||||
protected override void LoadedOnce()
|
||||
{
|
||||
// 首次加载初始化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4 — 主 View(XAML)
|
||||
|
||||
先读取 `Source/XAML_Preset.txt` 中 `View` 模板,复制 xmlns 引用,然后:
|
||||
|
||||
```xml
|
||||
<UserControl
|
||||
x:Class="PipeReviewReport.<模块名>.Views.<模块名>MainView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PipeReviewReport.<模块名>.Views"
|
||||
xmlns:core="MAX.Core"
|
||||
...(其余 xmlns 从 XAML_Preset.txt 复制)
|
||||
d:DataContext="{d:DesignInstance Type=local:<模块名>MainViewModel}"
|
||||
Style="{DynamicResource UserControlStyle}"
|
||||
DataContext="{core:DISource Type=local:<模块名>MainViewModel}">
|
||||
<Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
## Step 5 — 添加到主程序
|
||||
|
||||
在 `Source/PipeReviewReport/PipeReviewReport.csproj` 中添加项目引用。
|
||||
`AppModule` 会被主程序自动扫描,无需额外注册。
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] `AppModule` 实现 `IAppModule`,包含所有必要属性
|
||||
- [ ] `Key` 使用 `ConstKeys.SK_XXX` 常量
|
||||
- [ ] ViewModel 继承 `ViewModelBaseEx`
|
||||
- [ ] View 使用 `DISource` 绑定 ViewModel,设置 `UserControlStyle`
|
||||
- [ ] 主程序 `.csproj` 已添加项目引用
|
||||
- [ ] 输入框已显式 `MaxLength="{x:Static shared:InputLimits.Path/Text}"`
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: add-service
|
||||
description: >-
|
||||
在 PipeReviewReport 客户端中新建一个带接口的 Service,并通过 AutoFac 自动注册。
|
||||
当用户需要添加新的业务服务、数据服务、或状态管理服务时使用。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# 新建客户端 Service
|
||||
|
||||
## 根据生命周期选择模式
|
||||
|
||||
| 需求 | 实现接口 |
|
||||
|------|---------|
|
||||
| 单例,无需初始化 | `ISingletonDependency` |
|
||||
| 单例,需要异步启动初始化 | `ISingletonDependency` + `IAppInitializeAsync` |
|
||||
| 单例,需要同步启动初始化 | `ISingletonDependency` + `IAppInitialize` |
|
||||
| 单例,需要在应用关闭时清理 | + `IAppClosedHandler` |
|
||||
|
||||
## 模板
|
||||
|
||||
### 最简单的单例 Service
|
||||
|
||||
```csharp
|
||||
using MAX.Core.DependencyInjections;
|
||||
|
||||
namespace PipeReviewReport.<模块名>.Services;
|
||||
|
||||
public interface IXxxService
|
||||
{
|
||||
void DoSomething();
|
||||
}
|
||||
|
||||
internal class XxxService : IXxxService, ISingletonDependency
|
||||
{
|
||||
public void DoSomething() { }
|
||||
}
|
||||
```
|
||||
|
||||
### 需要启动初始化的 Service
|
||||
|
||||
```csharp
|
||||
using MAX.Core.DependencyInjections;
|
||||
using MAX.Core.Interfaces;
|
||||
|
||||
namespace PipeReviewReport.<模块名>.Services;
|
||||
|
||||
public interface IXxxService { }
|
||||
|
||||
internal class XxxService(IGrpcService grpcService) : IXxxService, IAppInitializeAsync
|
||||
{
|
||||
string IAppInitializeAsync.Description { get; } = "初始化 Xxx 中";
|
||||
int IPriority.Priority { get; } = IntPriority.Normal;
|
||||
|
||||
async Task IAppInitializeAsync.AppInitializeAsync(IStatusReport statusReport)
|
||||
{
|
||||
// 异步初始化逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 需要初始化 + 关闭清理的 Service
|
||||
|
||||
```csharp
|
||||
internal class XxxService(IGrpcService grpcService) : IXxxService, IAppInitializeAsync, IAppClosedHandler
|
||||
{
|
||||
string IAppInitializeAsync.Description { get; } = "初始化 Xxx 中";
|
||||
int IPriority.Priority { get; } = IntPriority.Normal;
|
||||
|
||||
int IAppClosedHandler.Priority { get; } = IntPriority.Normal;
|
||||
string IAppClosedHandler.Description { get; } = "关闭 Xxx 中";
|
||||
|
||||
async Task IAppInitializeAsync.AppInitializeAsync(IStatusReport statusReport)
|
||||
{
|
||||
// 初始化
|
||||
}
|
||||
|
||||
void IAppClosedHandler.AppClosed(AppCloseParameter parameter)
|
||||
{
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 实现 `ISingletonDependency` 后 AutoFac **自动注册为单例**,无需手动注册
|
||||
- 实现 `IAppInitializeAsync` / `IAppInitialize` 时必须同时实现 `IPriority`(提供 `Priority` 属性)
|
||||
- 接口定义 `public`,实现类 `internal`
|
||||
- 优先级使用 `IntPriority` 常量(`VeryHigh`、`High`、`Normal`、`Lower`、`VeryLow`)
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 接口 `public`,实现类 `internal`
|
||||
- [ ] 根据需要选择了正确的生命周期接口组合
|
||||
- [ ] 有初始化/关闭逻辑时实现了 `Description` 和 `Priority`
|
||||
- [ ] 使用 Primary Constructor 注入依赖
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: add-view
|
||||
description: >-
|
||||
在 PipeReviewReport 客户端现有模块中新建一个 View + ViewModel 对。
|
||||
当用户需要在模块内添加新页面、子视图时使用。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# 新建 View + ViewModel
|
||||
|
||||
## 命名约定(关键)
|
||||
|
||||
View 和 ViewModel 的命名遵循统一规则:
|
||||
|
||||
- `Xxx` = 功能名称(不含 `View` 后缀),如 `AlarmLog`、`MonitorMain`、`ParameterConfig`
|
||||
- View 文件:`XxxView.xaml`(如 `AlarmLogView.xaml`)
|
||||
- ViewModel 类:`XxxViewModel`(如 `AlarmLogViewModel`)
|
||||
- ViewModel 文件(拆分时):`ViewModels/XxxViewModel.cs`
|
||||
|
||||
**命名反例(禁止):**
|
||||
|
||||
| ❌ 错误 | ✅ 正确 | 说明 |
|
||||
|---|---|---|
|
||||
| `AlarmLogViewViewModel.cs` | `AlarmLogViewModel.cs` | `Xxx` = `AlarmLog`,不要保留 View 后缀 |
|
||||
| `MainConsoleMainViewViewModel.cs` | `MainConsoleMainViewModel.cs` | 同理 |
|
||||
| `DataManagementViewViewModel.cs` | `DataManagementViewModel.cs` | 同理 |
|
||||
|
||||
**当用户提到的功能名已包含 "View" 后缀时,必须去掉它再添加 ViewModel 后缀。**
|
||||
|
||||
## Step 1 — 创建 ViewModel
|
||||
|
||||
**优先将 ViewModel 写在 `.xaml.cs` 文件末尾**(UserControl class 之后),视图不复杂时无需单独建文件:
|
||||
|
||||
```csharp
|
||||
// XxxView.xaml.cs
|
||||
|
||||
namespace PipeReviewReport.<模块名>.Views;
|
||||
|
||||
public partial class XxxView : UserControl
|
||||
{
|
||||
public XxxView() => InitializeComponent();
|
||||
}
|
||||
|
||||
internal class XxxViewModel : ViewModelBaseEx
|
||||
{
|
||||
protected override void LoadedOnce()
|
||||
{
|
||||
// 首次加载初始化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ViewModel 逻辑较多导致文件过长时,再拆到 `ViewModels/XxxViewModel.cs` 单独文件。
|
||||
|
||||
## Step 2 — 创建 View(XAML)
|
||||
|
||||
先读取 `Source/XAML_Preset.txt` 中 `-------------- View --------------` 区段,复制全部 xmlns 属性,然后创建:
|
||||
|
||||
```xml
|
||||
<UserControl
|
||||
x:Class="PipeReviewReport.<模块名>.Views.XxxView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PipeReviewReport.<模块名>.Views"
|
||||
xmlns:core="MAX.Core"
|
||||
...(其余 xmlns 从 XAML_Preset.txt 复制)
|
||||
d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"
|
||||
Style="{DynamicResource UserControlStyle}"
|
||||
DataContext="{core:DISource Type=local:XxxViewModel}"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
<Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
## Step 3 — code-behind
|
||||
|
||||
保持最简,不写业务逻辑:
|
||||
|
||||
```csharp
|
||||
namespace PipeReviewReport.<模块名>.Views;
|
||||
|
||||
public partial class XxxView : UserControl
|
||||
{
|
||||
public XxxView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] ViewModel 继承 `ViewModelBaseEx`
|
||||
- [ ] View 设置 `Style="{DynamicResource UserControlStyle}"`
|
||||
- [ ] View 使用 `DataContext="{core:DISource Type=local:XxxViewModel}"` 绑定
|
||||
- [ ] `d:DataContext` 设计时数据上下文已配置
|
||||
- [ ] code-behind 无业务逻辑
|
||||
- [ ] 输入框已显式 `MaxLength="{x:Static shared:InputLimits.Path/Text}"`(路径片段用 Path;完整路径用选择器,不要手输)
|
||||
@@ -0,0 +1,809 @@
|
||||
---
|
||||
name: initialize-new-project
|
||||
description: >-
|
||||
在基于模板创建新项目后,通过现状检查 + 两轮交互(技术栈 + 业务材料)收集需求,
|
||||
最终生成一份支持分模块多轮执行的项目初始化计划(Plan)。
|
||||
本 skill 只生成 plan,不执行任何代码或配置变更;用户确认后再由后续对话分模块逐步实现。
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 初始化新项目(生成 Plan)
|
||||
|
||||
本 skill 用于在基于模板创建新项目后,通过两轮交互收集需求,最终输出一份完整、可执行的项目初始化计划(plan)。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**本 skill 只生成 plan,绝不执行任何代码、配置或文件变更。**
|
||||
- 不写文件、不改 csproj、不加 NuGet 包
|
||||
- 不修改 App.xaml、Startup.cs 等代码
|
||||
- 不创建 rules / skills / 文档
|
||||
- 最终交付物是一份结构化的 plan(写入 `.cursor/plans/initialize-plan.md`)
|
||||
|
||||
执行阶段由用户确认 plan 后,在后续对话中单独进行(可使用 Cursor 的 Plan 模式或 Agent 模式)。
|
||||
|
||||
## 最终目标
|
||||
|
||||
**本 skill 的最终目标是产出一份可执行的项目初始化计划(Plan),该计划经过后续多轮对话逐一实现后,应能得到一个完整可用、与原型图高度一致的 WPF 软件。**
|
||||
|
||||
这意味着 Plan 必须:
|
||||
|
||||
- **以原型图为唯一真相源**:Plan 中的所有 UI 描述、模块拆分、页面清单必须直接来自用户提供的设计原型,而不是猜测。
|
||||
- **可迭代执行**:Plan 不是"一次性把所有代码写完"的指令,而是按模块、按页面拆分为多个独立可执行的任务块,每块可由后续对话单独实现并验证。
|
||||
- **充分暴露不确定性**:原型图中不清晰的部分、技术决策中未决的部分,必须在 Plan 中明确标注「待确认」,让用户参与决策——而不是 AI 自己编造。
|
||||
|
||||
## 整体流程
|
||||
|
||||
1. **Step 0 — 项目现状检查**(检查项目已有的配置、模块、模板代码)
|
||||
2. **Step 1 — 第一轮:收集技术栈需求**(AskQuestion,10 个问题)
|
||||
3. **Step 2 — 生成初步计划(Plan v1)**(仅基于技术栈 + 现状检查结果)
|
||||
4. **Step 3 — 第二轮:索要业务材料与设计原型**(AskQuestion + 自由对话)
|
||||
5. **Step 4 — 生成完整计划(Plan v2)**(最终交付,写入文件)
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — 项目现状检查
|
||||
|
||||
在收集用户需求之前,必须先了解项目模板的当前状态:**哪些已经配置好了、哪些可以直接用**。这个检查应在 Step 1 之前完成,检查结果会影响后续的技术栈询问策略(例如,已配置好的选项可以自动跳过或在问题中标注「已默认配置」)。
|
||||
|
||||
### 0.1 检查内容
|
||||
|
||||
| 检查项 | 检查方法 | 作用 |
|
||||
|--------|---------|------|
|
||||
| **解决方案结构** | 读取 `.slnx` 文件,列出所有已存在的项目 | 了解当前项目列表,后续新增模块时知道要往哪个 slnx 添加 |
|
||||
| **Framework 项目** | 检查 `Source/PipeReviewReport.Framework/` 下的 csproj,列出已有 NuGet 包 | 了解已配置的基础设施(FreeSql、MAX、NLog 等),避免重复添加 |
|
||||
| **主项目** | 检查 `Source/PipeReviewReport/PipeReviewReport.csproj` 已有 NuGet 包 | 了解主项目已引用的 UI 库 |
|
||||
| **Resources 项目** | 检查 `Source/PipeReviewReport.Resources/` | 了解已有的资源结构(图标、字体、样式) |
|
||||
| **Strings 项目** | 检查 `Source/PipeReviewReport.Strings/` | 了解多语言机制的现状 |
|
||||
| **现有模块** | 检查 `Source/Modules/` 下是否已有模块 | 了解模块化架构的已有实现 |
|
||||
| **App.xaml** | 检查 `Source/PipeReviewReport/App.xaml` | 了解已注册的资源字典 |
|
||||
| **Startup / DI 配置** | 检查 `Source/PipeReviewReport.Framework/` 下的依赖注入配置 | 了解已注册的服务 |
|
||||
| **现有 rules** | 检查 `.agents/rules/` 下的文件 | 了解已有规范,生成 plan 时知道哪些需要更新 |
|
||||
| **现有 skills** | 检查 `.cursor/skills/` 下的文件 | 了解已有技能 |
|
||||
|
||||
### 0.2 将检查结果融入 Plan
|
||||
|
||||
在后续 Step 2 和 Step 4 生成 Plan 时:
|
||||
|
||||
- **标注「已有」**:所有已存在的包、配置、代码在 Plan 中明确标注为「已有」,不要出现在「需新增」清单中。
|
||||
- **标注「需移除」**:如果用户选择了与已有配置冲突的选项(如选择 DevExpress 完整版时需移除 DevExpressMvvm 包),在 Plan 中列出。
|
||||
- **复用已有模块**:如果 `Source/Modules/` 下已有相关模块,Plan 中应优先建议扩展而非重复创建。
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — 第一轮:收集技术栈需求
|
||||
|
||||
使用 `AskQuestion` 工具询问用户以下问题。**必须先收集完所有回答再进入 Step 2**。
|
||||
|
||||
可以在一次 `AskQuestion` 调用中提交多个问题(推荐分组提交,避免一次问太多)。
|
||||
|
||||
### 问题 1:UI 样式库选择
|
||||
|
||||
- prompt: "这个项目需要使用哪种 UI 样式库?"
|
||||
- options:
|
||||
- `no-ui-lib`: "不需要额外的 UI 样式库(使用默认样式)"
|
||||
- `devexpress-full`: "DevExpress WPF(完整版)- 企业级 UI 控件库"
|
||||
- `handycontrol`: "MAX.HandyControl - 现代化 WPF 控件库(内部 fork,禁止官方 HandyControl)"
|
||||
- `material-design`: "MaterialDesignInXaml - Material Design 风格"
|
||||
- `panuon`: "Panuon.UI.Silver - 现代化 UI 库"
|
||||
- `other-ui`: "其他 UI 样式库"
|
||||
|
||||
### 问题 2:图标库配置
|
||||
|
||||
- prompt: "项目已内置阿里云 iconfont 图标系统。是否需要额外配置图标?"
|
||||
- options:
|
||||
- `default-icons`: "使用内置图标系统(推荐)"
|
||||
- `fontawesome-add`: "额外添加 FontAwesome 图标库"
|
||||
- `material-icons-add`: "额外添加 Material Icons 图标库"
|
||||
- `other-icons`: "其他图标库"
|
||||
|
||||
### 问题 3:图表库选择
|
||||
|
||||
- prompt: "需要使用图表库吗?(默认:ScottPlot5)"
|
||||
- options:
|
||||
- `default-charts`: "使用默认图表库(ScottPlot5)"
|
||||
- `devexpress-charts`: "DevExpress Charts(如果已选择 DevExpress WPF)"
|
||||
- `livecharts`: "LiveCharts - 简单易用的图表库"
|
||||
- `oxyplot`: "OxyPlot - 跨平台图表库"
|
||||
- `no-charts`: "不需要"
|
||||
- `other-charts`: "其他图表库"
|
||||
|
||||
### 问题 4:数据库选择
|
||||
|
||||
- prompt: "项目需要使用数据库吗?(默认:FreeSql + SQLite)"
|
||||
- options:
|
||||
- `default-db`: "使用默认数据库(FreeSql + SQLite)"
|
||||
- `sqlite-only`: "SQLite - 仅 SQLite,不使用 FreeSql"
|
||||
- `sql-server`: "SQL Server - 企业级数据库"
|
||||
- `postgresql`: "PostgreSQL - 开源关系数据库"
|
||||
- `mysql`: "MySQL - 流行关系数据库"
|
||||
- `redis-only`: "仅需要 Redis 缓存"
|
||||
- `other-db`: "其他数据库"
|
||||
|
||||
### 问题 5:ORM 选择(条件触发)
|
||||
|
||||
仅当问题 4 选择了数据库(除 `default-db` 和 `redis-only` 外)才询问:
|
||||
|
||||
- prompt: "需要使用 ORM 框架吗?(FreeSql 已作为默认)"
|
||||
- options:
|
||||
- `default-orm`: "使用默认的 FreeSql"
|
||||
- `ef-core`: "Entity Framework Core - 微软官方 ORM"
|
||||
- `dapper`: "Dapper - 轻量级 ORM"
|
||||
- `no-orm`: "不需要 ORM,使用原生 SQL"
|
||||
- `other-orm`: "其他 ORM"
|
||||
|
||||
### 问题 6:缓存方案
|
||||
|
||||
- prompt: "是否需要缓存方案?"
|
||||
- options:
|
||||
- `no-cache`: "不需要"
|
||||
- `memory-cache`: "内存缓存(Microsoft.Extensions.Caching.Memory)"
|
||||
- `redis`: "Redis 分布式缓存"
|
||||
- `sqlite-cache`: "SQLite 缓存"
|
||||
- `other-cache`: "其他缓存方案"
|
||||
|
||||
### 问题 7:日志框架
|
||||
|
||||
- prompt: "日志框架使用什么?(默认已有 NLog)"
|
||||
- options:
|
||||
- `default-nlog`: "使用默认的 NLog"
|
||||
- `serilog`: "切换到 Serilog"
|
||||
- `log4net`: "切换到 log4net"
|
||||
- `multiple-loggers`: "使用多个日志框架"
|
||||
|
||||
### 问题 8:网络/通信需求
|
||||
|
||||
- prompt: "项目需要网络通信功能吗?"
|
||||
- options:
|
||||
- `no-network`: "不需要"
|
||||
- `restsharp`: "HTTP 客户端(推荐 RestSharp)"
|
||||
- `websocket`: "WebSocket 实时通信"
|
||||
- `grpc-client`: "gRPC 客户端"
|
||||
- `signalr-client`: "SignalR 客户端"
|
||||
- `other-network`: "其他通信方案"
|
||||
|
||||
### 问题 9:其他依赖库
|
||||
|
||||
- prompt: "还需要其他特定的第三方库吗?(可多选)"
|
||||
- allow_multiple: true
|
||||
- options:
|
||||
- `no-other`: "没有其他需求"
|
||||
- `automapper`: "AutoMapper - 对象映射"
|
||||
- `newtonsoft-json`: "Newtonsoft.Json - JSON 序列化(已有)"
|
||||
- `system-text-json`: "System.Text.Json - JSON 序列化"
|
||||
- `quartz`: "Quartz.NET - 任务调度"
|
||||
- `fluentvalidation`: "FluentValidation - 数据校验"
|
||||
- `other-lib`: "其他库(手动输入)"
|
||||
|
||||
### 问题 10:模块化架构
|
||||
|
||||
- prompt: "是否使用模块化架构?"
|
||||
- options:
|
||||
- `yes-modules`: "是,使用模块化架构"
|
||||
- `no-modules`: "否,单一应用程序架构"
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — 生成初步计划(Plan v1)
|
||||
|
||||
基于 Step 1 的回答,**仅输出**(不执行)一份「技术栈初始化计划(v1)」。内容包含:
|
||||
|
||||
1. **技术栈总览**:本次选定的所有技术(UI / 图标 / 图表 / 数据库 / ORM / 缓存 / 日志 / 网络 / 模块化)
|
||||
2. **NuGet 包清单**:按项目分组(主项目 / Framework / Resources 等),注明包名、版本、用途
|
||||
3. **配置文件变更清单**:csproj 修改、App.xaml 资源字典、Directory.Build.props、NLog.config 等
|
||||
4. **代码文件变更清单**:Startup.cs 注册、MessageBoxServiceEx.cs 重写等
|
||||
5. **rules / skills 变更清单**:需要更新或新增的规则与技能
|
||||
6. **文档清单**:README.md、架构文档等
|
||||
|
||||
### 参考材料:常用 NuGet 包映射表
|
||||
|
||||
| 用户选择 | NuGet 包名 | 适用项目 | 说明 |
|
||||
|---------|----------|---------|------|
|
||||
| **DevExpress WPF(完整版)** | DevExpress.Wpf.Core | 主项目 | 核心 MVVM 框架(含 DevExpress.Mvvm) |
|
||||
| | DevExpress.Wpf.Themes.Win11Light | 主项目 | Win11 浅色主题 |
|
||||
| | DevExpress.Wpf.Grid | 主项目 | DataGrid/TreeList |
|
||||
| | DevExpress.Wpf.LayoutControl | 主项目 | 布局控件 |
|
||||
| | DevExpress.Wpf.Docking | 主项目 | 可停靠面板 |
|
||||
| | DevExpress.Wpf.Ribbon | 主项目 | Ribbon 工具栏 |
|
||||
| MAX.HandyControl | MAX.HandyControl | 主项目 | 内部 fork;**禁止**官方 `HandyControl`;程序集/命名空间仍为 HandyControl,需配置 App.xaml |
|
||||
| MaterialDesignInXaml | MaterialDesignThemes | 主项目 | Material Design 风格 |
|
||||
| Panuon.UI.Silver | Panuon.UI.Silver | 主项目 | |
|
||||
| FontAwesome | FontAwesome.WPF | 主项目 | 额外添加(项目已有 iconfont) |
|
||||
| Material Icons | MaterialDesignThemes.Mdix | 主项目 | 额外添加(项目已有 iconfont) |
|
||||
| **默认图表库** | MAX.Common.Plot5 | Framework | ScottPlot5(已包含) |
|
||||
| LiveCharts | LiveCharts.Wpf | 主项目 | |
|
||||
| OxyPlot | OxyPlot.Wpf | 主项目 | |
|
||||
| **默认数据库** | FreeSql (v3.5.309) | Framework | 已包含,核心库 |
|
||||
| | FreeSql.Extensions.JsonMap (v3.5.309) | Framework | 已包含,JSON 映射 |
|
||||
| | FreeSql.Provider.Sqlite (v3.5.309) | Framework | 已包含,SQLite 驱动 |
|
||||
| | FreeSql.Repository (v3.5.309) | Framework | 已包含,Repository 模式 |
|
||||
| SQL Server | Microsoft.Data.SqlClient | Framework | |
|
||||
| PostgreSQL | Npgsql | Framework | |
|
||||
| MySQL | MySql.Data | Framework | |
|
||||
| Redis | StackExchange.Redis | Framework | |
|
||||
| EF Core | Microsoft.EntityFrameworkCore.Sqlite | Framework | |
|
||||
| Dapper | Dapper | Framework | 轻量级 ORM |
|
||||
| RestSharp | RestSharp | Framework | |
|
||||
| AutoMapper | AutoMapper | Framework | 对象映射(项目默认已用 Mapster) |
|
||||
| Quartz.NET | Quartz | Framework | 任务调度 |
|
||||
| FluentValidation | FluentValidation | Framework | 数据校验 |
|
||||
|
||||
### 参考材料:DevExpress WPF(完整版)配置要点
|
||||
|
||||
如果用户选择 DevExpress WPF(完整版),生成 plan 时需包含以下要点:
|
||||
|
||||
1. **添加 DevExpress NuGet 包到主项目**(`Source/PipeReviewReport/PipeReviewReport.csproj`):
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<!-- 核心 MVVM 框架(包含 DevExpress.Mvvm) -->
|
||||
<PackageReference Include="DevExpress.Wpf.Core" Version="22.2.11" />
|
||||
|
||||
<!-- 主题(Win11 浅色主题,可根据需要选择其他主题) -->
|
||||
<PackageReference Include="DevExpress.Wpf.Themes.Win11Light" Version="22.2.11" />
|
||||
|
||||
<!-- 常用控件 -->
|
||||
<PackageReference Include="DevExpress.Wpf.Grid" Version="22.2.11" />
|
||||
<PackageReference Include="DevExpress.Wpf.LayoutControl" Version="22.2.11" />
|
||||
<PackageReference Include="DevExpress.Wpf.Docking" Version="22.2.11" />
|
||||
<PackageReference Include="DevExpress.Wpf.Ribbon" Version="22.2.11" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
**可选的 DevExpress 包**(在 plan 中根据 UI 原型决定是否启用):
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<!-- 图表相关 -->
|
||||
<PackageReference Include="DevExpress.Wpf.Charts" Version="22.2.11" />
|
||||
|
||||
<!-- RichEdit 文档编辑 -->
|
||||
<PackageReference Include="DevExpress.Wpf.RichEdit" Version="22.2.11" />
|
||||
<PackageReference Include="DevExpress.RichEdit.Export" Version="22.2.11" />
|
||||
|
||||
<!-- PDF 查看器 -->
|
||||
<PackageReference Include="DevExpress.Wpf.PdfViewer" Version="22.2.11" />
|
||||
|
||||
<!-- 打印功能 -->
|
||||
<PackageReference Include="DevExpress.Wpf.Printing" Version="22.2.11" />
|
||||
|
||||
<!-- 其他可选控件 -->
|
||||
<PackageReference Include="DevExpress.Wpf.NavBar" Version="22.2.11" />
|
||||
<PackageReference Include="DevExpress.Wpf.Gauges" Version="22.2.11" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
2. **移除 DevExpressMvvm 引用**(因为 DevExpress.Wpf.Core 已包含):
|
||||
|
||||
从 `Source/PipeReviewReport.Framework/PipeReviewReport.Framework.csproj` 中移除:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="DevExpressMvvm" Version="24.1.6" />
|
||||
```
|
||||
|
||||
3. **App.xaml 无需修改**:DevExpress 主题通过 NuGet 包自动加载,无需在 App.xaml 中手动添加资源字典。
|
||||
|
||||
4. **DevExpress 常用控件**:
|
||||
|
||||
| 控件 | NuGet 包 | 用途 |
|
||||
|------|---------|------|
|
||||
| DataGrid | DevExpress.Wpf.Grid | 数据表格,支持分组、过滤、排序、虚拟化 |
|
||||
| TreeList | DevExpress.Wpf.Grid | 树形列表(与 DataGrid 同一包) |
|
||||
| LayoutControl | DevExpress.Wpf.LayoutControl | 布局容器,自动排列子元素 |
|
||||
| DockLayoutManager | DevExpress.Wpf.Docking | 可停靠面板,类似 VS 的窗口布局 |
|
||||
| RibbonControl | DevExpress.Wpf.Ribbon | 功能区工具栏,类似 Office 的 Ribbon |
|
||||
| ChartControl | DevExpress.Wpf.Charts | 图表控件 |
|
||||
| RichEditControl | DevExpress.Wpf.RichEdit | 富文本编辑器 |
|
||||
|
||||
5. **DevExpress 主题选择**:
|
||||
|
||||
可用主题包(选择一个即可):
|
||||
- `DevExpress.Wpf.Themes.Win11Light` - Win11 浅色主题
|
||||
- `DevExpress.Wpf.Themes.Win11Dark` - Win11 深色主题
|
||||
- `DevExpress.Wpf.Themes.Office2019White` - Office 2019 白色主题
|
||||
- `DevExpress.Wpf.Themes.Office2019Colorful` - Office 2019 彩色主题
|
||||
- `DevExpress.Wpf.Themes.VS2019Blue` - Visual Studio 2019 蓝色主题
|
||||
- `DevExpress.Wpf.Themes.VS2019Dark` - Visual Studio 2019 深色主题
|
||||
|
||||
主题切换代码:
|
||||
|
||||
```csharp
|
||||
using DevExpress.Xpf.Core;
|
||||
|
||||
// 在应用启动时设置主题
|
||||
ApplicationThemeHelper.UpdateApplicationThemeName("Win11.Light");
|
||||
```
|
||||
|
||||
6. **plan 中应包含的 rules 更新**:
|
||||
- 创建 `.agents/rules/devexpress-wpf.md`
|
||||
- 说明常用控件的使用方法(DataGrid、TreeList、LayoutControl、Docking、Ribbon 等)
|
||||
- 说明主题选择方式
|
||||
- 更新 `.agents/rules/architecture.md`,在"核心技术组件"部分添加 DevExpress WPF 说明
|
||||
|
||||
### 参考材料:MAX.HandyControl 配置要点
|
||||
|
||||
如果用户选择 MAX.HandyControl(内部 fork),生成 plan 时需包含以下要点:
|
||||
|
||||
> **强制遵守:** 使用 NuGet 包 `MAX.HandyControl`,**禁止**引用官方包 `HandyControl`。程序集名与代码命名空间仍为 `HandyControl`,因此 pack URI / `using` 写法与官方兼容。本地源码:`{LIBS_ROOT}HandyControl\`(路径变量见 `max-libraries-reference.md`)。
|
||||
|
||||
1. **添加 MAX.HandyControl NuGet 包**(`Source/PipeReviewReport/PipeReviewReport.csproj`):
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MAX.HandyControl" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
2. **在 App.xaml 中添加 HandyControl 资源字典**(`Source/PipeReviewReport/App.xaml`):
|
||||
|
||||
```xml
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
3. **在需要使用 HandyControl 控件的 XAML 文件中添加命名空间**:
|
||||
|
||||
```xml
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
```
|
||||
|
||||
4. **HandyControl 常用控件示例**:
|
||||
|
||||
| 控件 | XAML 标签 | 说明 |
|
||||
|------|----------|------|
|
||||
| Button | `<hc:Button>` | 带动画效果的按钮 |
|
||||
| TextBox | `<hc:TextBox>` | 带水印效果的文本框;**必须** `MaxLength="{x:Static shared:InputLimits.Text}"`(路径片段用 `Path`) |
|
||||
| NumericUpDown | `<hc:NumericUpDown>` | 数值输入框 |
|
||||
| PasswordBox | `<hc:PasswordBox>` | 密码输入框;**必须**显式 `MaxLength` |
|
||||
| ComboBox | `<hc:ComboBox>` | 下拉选择框;不要用可编辑 ComboBox 收自由文本,改用 TextBox |
|
||||
| CheckBox | `<hc:CheckBox>` | 复选框 |
|
||||
| RadioButton | `<hc:RadioButton>` | 单选按钮 |
|
||||
| DatePicker | `<hc:DatePicker>` | 日期选择器 |
|
||||
| TimePicker | `<hc:TimePicker>` | 时间选择器 |
|
||||
| DateTimePicker | `<hc:DateTimePicker>` | 日期时间选择器 |
|
||||
| ToggleSwitch | `<hc:ToggleSwitch>` | 开关控件 |
|
||||
| ProgressBar | `<hc:ProgressBar>` | 进度条 |
|
||||
| Slider | `<hc:Slider>` | 滑块 |
|
||||
| ToggleButton | `<hc:ToggleButton>` | 切换按钮 |
|
||||
| ProgressButton | `<hc:ProgressButton>` | 带进度按钮 |
|
||||
| GroupBox | `<hc:GroupBox>` | 分组框 |
|
||||
| Card | `<hc:Card>` | 卡片容器 |
|
||||
| Divider | `<hc:Divider>` | 分隔线 |
|
||||
| ScrollViewer | `<hc:ScrollViewer>` | 滚动视图 |
|
||||
| MessageBox | `HandyControl.Controls.MessageBox` | 消息对话框 |
|
||||
|
||||
5. **HandyControl 消息框集成**(重写 `MessageBoxServiceEx`):
|
||||
|
||||
修改 `Source/PipeReviewReport.Framework/ViewModelServices/MessageBoxServiceEx.cs`,把内部的系统默认 MessageBox 替换为 HandyControl 的实现:
|
||||
|
||||
```csharp
|
||||
using HandyControl.Controls;
|
||||
|
||||
internal class MessageBoxServiceEx : IMessageBoxService
|
||||
{
|
||||
public void ShowInfo(string message)
|
||||
{
|
||||
MessageBox.Show(message, "信息", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
public bool ShowYesNoQuestion(string message)
|
||||
{
|
||||
var result = MessageBox.Show(message, "确认", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
return result == MessageBoxResult.Yes;
|
||||
}
|
||||
|
||||
// 其他方法实现...
|
||||
}
|
||||
```
|
||||
|
||||
**在 View 中注册**(如果引用了 UserControlStyle 或 DialogStyle.Base 则不需要再额外定义):
|
||||
|
||||
```xml
|
||||
<dxmvvm:Interaction.Behaviors>
|
||||
<f:MessageBoxServiceEx/>
|
||||
</dxmvvm:Interaction.Behaviors>
|
||||
```
|
||||
|
||||
**在 ViewModel 中使用**:
|
||||
|
||||
```csharp
|
||||
public IMessageBoxServiceEx MessageBoxService => GetService<IMessageBoxServiceEx>();
|
||||
```
|
||||
|
||||
6. **HandyControl 主题切换**:
|
||||
|
||||
HandyControl 支持多种主题,通过 App.xaml 中的资源字典控制:
|
||||
- `SkinDefault.xaml` + `Theme.xaml`:默认主题
|
||||
- `SkinViolet.xaml` + `Theme.xaml`:紫色主题
|
||||
- `SkinDark.xaml` + `Theme.xaml`:深色主题
|
||||
|
||||
代码切换主题:
|
||||
|
||||
```csharp
|
||||
using HandyControl.Tools;
|
||||
|
||||
// 切换主题
|
||||
HandyControl.Tools.ControlHelper.SetTheme(App.Current, "主题名称");
|
||||
```
|
||||
|
||||
7. **plan 中应包含的 rules 更新**:
|
||||
- 创建 `.agents/rules/handycontrol-usage.md`(写明包名为 `MAX.HandyControl`,禁止官方包)
|
||||
- 说明常用控件的使用方法
|
||||
- 说明主题切换方式
|
||||
- 提供常见场景的代码示例
|
||||
- 更新 `.agents/rules/architecture.md`,在"核心技术组件"部分添加 MAX.HandyControl 说明
|
||||
- 确认 `max-libraries-reference.md` 已包含 MAX.HandyControl 源码路径
|
||||
|
||||
### 参考材料:默认数据库 / 图表说明
|
||||
|
||||
- **FreeSql + SQLite**:项目默认已配置,支持 Code First、自动同步表结构、Repository 模式。详见 `.agents/rules/architecture.md`。
|
||||
- **ScottPlot5**:通过 `MAX.Common.Plot5` 封装,适合波形图、数据可视化、实时数据。已引用在 Framework 项目中。
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — 第二轮:索要业务材料与设计原型
|
||||
|
||||
向用户说明:**仅凭技术栈选择无法生成贴合业务的完整计划**,需要进一步资料才能产出可执行的最终 plan。
|
||||
|
||||
### 3.1 通过 AskQuestion 收集结构化信息
|
||||
|
||||
- prompt: "为了生成贴合业务的完整计划,请提供以下信息(可多选你需要提供的材料类型)"
|
||||
- allow_multiple: true
|
||||
- options:
|
||||
- `ui-mockups`: "我会提供 UI 设计原型图 / 效果图(Figma、Axure、PNG 等)"
|
||||
- `requirements-doc`: "我会提供业务需求文档 / 功能清单"
|
||||
- `data-model`: "我会提供数据模型 / 数据库设计"
|
||||
- `flow-diagrams`: "我会提供业务流程图 / 状态机图"
|
||||
- `hardware-interaction`: "涉及硬件读写操作(传感器、PLC、串口、GPIB、USB 设备等)"
|
||||
- `reference-products`: "我有参考产品 / 竞品链接"
|
||||
- `none-now`: "暂无额外材料,按通用方案生成"
|
||||
|
||||
### 3.2 通过自由对话收集详细材料
|
||||
|
||||
向用户**明确请求**以下材料(根据用户在 3.1 的选择,引导用户提供):
|
||||
|
||||
1. **UI 设计原型图**:
|
||||
- 直接粘贴图片到对话,或提供图片路径
|
||||
- 说明:主窗口布局、导航结构、关键页面、配色风格、控件偏好
|
||||
- 如果使用 DevExpress/HandyControl,原型图能帮助确定具体用哪些控件
|
||||
|
||||
2. **业务需求文档**:
|
||||
- 核心功能列表(用一句话描述每个功能)
|
||||
- 用户角色与权限(如果有)
|
||||
- 关键业务流程(从用户操作角度描述)
|
||||
|
||||
3. **数据模型**:
|
||||
- 主要实体及其关系
|
||||
- 已有的数据库表结构(如果有)
|
||||
- 数据规模估算(影响选型:SQLite vs SQL Server)
|
||||
|
||||
4. **参考产品 / 竞品**:
|
||||
- 链接或截图
|
||||
- 希望借鉴的部分(布局、交互、功能)
|
||||
|
||||
5. **特殊要求**:
|
||||
- 性能要求(实时性、并发、数据量)
|
||||
- 安全 / 合规要求(数据加密、审计、合规标准)
|
||||
- 部署方式(单机、ClickOnce、内网分发)
|
||||
- 多语言需求(项目已内置 Strings 多语言机制)
|
||||
|
||||
6. **硬件读写需求**(如果用户在 Step 3.1 选择了 `hardware-interaction`):
|
||||
- 涉及的硬件类型与型号(传感器、PLC、串口设备、GPIB 仪器、USB 设备等)
|
||||
- 通信协议(Modbus、TCP/IP、RS-232/485、GPIB、自定义协议等)
|
||||
- 数据采集频率与实时性要求(毫秒级、秒级、分钟级)
|
||||
- 硬件数量、并发连接要求
|
||||
- 是否有已有的硬件 SDK / DLL / 驱动
|
||||
- **关键决策:初期是否使用 Mock 虚拟设备开发**(详见下文)
|
||||
|
||||
### 硬件 Mock 策略(必须询问用户)
|
||||
|
||||
当用户提供了涉及硬件读写的原型图、需求书等材料后,**必须在生成 Plan 之前向用户确认**:
|
||||
|
||||
> "你提供的材料中涉及 [XX 硬件] 的读写操作。在项目初期构建阶段,建议先使用 **Mock 虚拟设备** 代替真实硬件进行开发和调试——这样可以脱离硬件环境独立开发、快速验证 UI 和业务流程。后续再替换为真实设备的实现。
|
||||
>
|
||||
> 这套方案通过 **接口 + 实现类** 的架构设计:所有业务代码只依赖接口,Mock 实现和真实硬件实现各自实现同一接口。后续替换时**只需替换实现类**,引用接口的地方完全不用动。"
|
||||
|
||||
然后使用 `AskQuestion` 让用户确认:
|
||||
|
||||
- prompt: "硬件读写策略:初期是否使用 Mock 虚拟设备?"
|
||||
- options:
|
||||
- `mock-first`: "是,先用 Mock 虚拟设备开发,后续替换为真实硬件(推荐)"
|
||||
- `real-hardware`: "直接使用真实硬件开发"
|
||||
|
||||
无论用户选择哪种策略,Plan 中都必须包含:
|
||||
|
||||
- **硬件接口定义**:为每种硬件类型定义清晰的 C# 接口(`IHardwareXxx`),包含所有读写操作方法
|
||||
- **接口与实现分离**:接口放在共享层(Framework 或 Shared 项目),实现类放在独立项目或模块中
|
||||
- **DI 注册**:通过 Autofac 注册接口与实现类的绑定,替换时只需改 DI 注册,无需改业务代码
|
||||
- **如果选择 mock-first**:Plan 中包含 Mock 实现类的创建任务(模拟数据返回、模拟设备状态、模拟异常场景)
|
||||
- **如果选择 real-hardware**:Plan 中包含真实硬件 SDK 集成、驱动安装、连接测试等任务
|
||||
|
||||
### 硬件相关 rules / skills 生成
|
||||
|
||||
Plan 中必须包含根据实际硬件需求生成对应的 **skill** 或 **rule** 的任务:
|
||||
|
||||
- 如果硬件有专用的通信协议(如 Modbus),在 Plan 的「rules 与 skills 更新」章节中增加创建对应 skill 的任务
|
||||
- 如果涉及通用串口通信,创建 `reference-serial-port` skill
|
||||
- 如果涉及 GPIB 仪器控制,创建 `reference-gpib` skill
|
||||
- 如果硬件有官方 SDK,创建该 SDK 的使用规范 rule
|
||||
|
||||
Plan 应明确列出需要创建哪些 skill/rule,并在「任务执行顺序」中安排对应的轮次。
|
||||
|
||||
**重要:等待用户提供实际材料后再进入 Step 4。** 如果用户选择 `none-now`,可以直接进入 Step 4 基于通用方案生成 plan,但要在 plan 中明确标注「待补充业务材料」的部分。
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — 生成完整计划(Plan v2 - 最终交付)
|
||||
|
||||
综合 Step 1 技术栈 + Step 3 业务材料,生成最终的、可执行的完整项目初始化计划。
|
||||
|
||||
### 4.1 Plan 必须包含的章节
|
||||
|
||||
最终的 plan 文件应包含以下结构(写入 `.cursor/plans/initialize-plan.md`):
|
||||
|
||||
```markdown
|
||||
# 项目初始化计划
|
||||
|
||||
## 1. 项目概述
|
||||
- 项目名称、目标、核心业务(基于 Step 3 业务材料总结)
|
||||
- 技术栈总览表
|
||||
|
||||
## 2. 环境与依赖(NuGet 包)
|
||||
- 按项目分组列出所有需要添加的包(包名、版本、用途)
|
||||
- 标注哪些是「已包含」、哪些是「需新增」、哪些是「需移除」
|
||||
|
||||
## 3. 配置文件变更
|
||||
- csproj 修改清单
|
||||
- App.xaml 资源字典(如适用)
|
||||
- Directory.Build.props
|
||||
- NLog.config / 其他配置
|
||||
|
||||
## 4. 项目结构与模块划分
|
||||
- 基于业务需求建议的模块划分(Modules)
|
||||
- 每个模块的职责
|
||||
- 主要 View / ViewModel 清单(基于 UI 原型图)
|
||||
|
||||
## 5. 基础设施层
|
||||
- 数据库初始化(FreeSql 配置、连接串、Code First 同步策略)
|
||||
- 日志配置
|
||||
- 缓存方案
|
||||
- DI 容器注册(Autofac + MAX.Core 约定)
|
||||
|
||||
## 6. 框架层
|
||||
- 需要新增/修改的容器服务(ISingletonDependency)
|
||||
- 需要新增/修改的 ViewModel 服务(ServiceBase)
|
||||
- 通用基础设施(消息框、对话框、加载指示器)
|
||||
|
||||
## 7. 硬件抽象层(如涉及硬件读写)
|
||||
|
||||
> **重要**:如果项目涉及硬件读写,必须先在 Step 3.2 向用户确认 Mock 策略后再填充本章节。
|
||||
|
||||
### 7.1 硬件接口定义
|
||||
|
||||
为每种硬件类型定义 C# 接口,所有业务代码通过接口访问硬件,不直接依赖具体实现类:
|
||||
|
||||
| 接口 | 对应硬件 | 核心方法 | 放置位置 |
|
||||
|------|---------|---------|---------|
|
||||
| `ISensorReader` | 温度传感器 | `ReadValueAsync()`, `CalibrateAsync()` | Framework/Interfaces/ |
|
||||
| `IPlcController` | PLC 控制器 | `ConnectAsync()`, `SendCommandAsync()` | Framework/Interfaces/ |
|
||||
| `ISerialPortDevice` | 串口设备 | `OpenAsync()`, `SendAsync()`, `ReceiveAsync()` | Framework/Interfaces/ |
|
||||
|
||||
**接口设计原则**:
|
||||
- 接口只暴露硬件能力,不暴露通信细节(波特率、串口号等配置参数通过构造函数或初始化方法传入)
|
||||
- 所有 I/O 操作使用 `async Task<T>`,支持取消(`CancellationToken`)
|
||||
- 包含 `ConnectAsync()` / `DisconnectAsync()` 生命周期方法
|
||||
- 包含健康检查方法(`IsConnected` 属性或 `PingAsync()`)
|
||||
- 接口放在 Framework 或 Shared 项目中,便于所有模块引用
|
||||
|
||||
### 7.2 Mock 实现类(初期开发用)
|
||||
|
||||
如果用户选择 `mock-first` 策略:
|
||||
|
||||
| Mock 类 | 实现接口 | 模拟策略 |
|
||||
|---------|---------|---------|
|
||||
| `MockSensorReader` | `ISensorReader` | 返回预设的模拟数据,可配置波动范围和异常概率 |
|
||||
| `MockPlcController` | `IPlcController` | 内存状态机模拟,可预设响应序列 |
|
||||
| `MockSerialPortDevice` | `ISerialPortDevice` | 内存队列模拟收发,支持注入测试数据 |
|
||||
|
||||
Mock 实现类放在 `Source/PipeReviewReport.Framework/Mocks/` 目录下。
|
||||
|
||||
### 7.3 真实硬件实现类(后续替换)
|
||||
|
||||
| 实现类 | 实现接口 | 依赖 |
|
||||
|--------|---------|------|
|
||||
| `RealSensorReader` | `ISensorReader` | 硬件 SDK/DLL |
|
||||
| `RealPlcController` | `IPlcController` | PLC SDK |
|
||||
| `RealSerialPortDevice` | `ISerialPortDevice` | `System.IO.Ports` |
|
||||
|
||||
真实实现类放在 `Source/PipeReviewReport.Framework/Hardware/` 或独立项目中。
|
||||
|
||||
### 7.4 DI 注册策略
|
||||
|
||||
通过 Autofac 的条件注册,替换时只需改一处:
|
||||
|
||||
```csharp
|
||||
// 开发阶段:注册 Mock 实现
|
||||
builder.RegisterType<MockSensorReader>().As<ISensorReader>().SingleInstance();
|
||||
|
||||
// 生产阶段:替换为真实实现(只需改这一行,其余代码不动)
|
||||
// builder.RegisterType<RealSensorReader>().As<ISensorReader>().SingleInstance();
|
||||
```
|
||||
|
||||
推荐使用配置文件或编译条件(`#if DEBUG`)控制注册,避免手动改代码。
|
||||
|
||||
### 7.5 替换保障
|
||||
|
||||
- **业务代码只引用接口**:所有 ViewModel、Service 中只声明 `ISensorReader`,不引用任何 `MockXxx` 或 `RealXxx`
|
||||
- **替换清单**:Plan 中明确列出替换时需修改的文件(通常只有 DI 注册文件 1 个)
|
||||
- **测试覆盖**:Mock 类和真实实现类共用同一套接口测试,确保行为一致
|
||||
|
||||
## 8. UI 层(基于设计原型)
|
||||
- 主窗口布局方案
|
||||
- 导航结构
|
||||
- 关键页面拆分(对照原型图逐页说明)
|
||||
- 主题/样式策略
|
||||
|
||||
## 9. 数据模型与实体
|
||||
- 主要实体类设计
|
||||
- 实体关系
|
||||
- FreeSql 映射约定
|
||||
|
||||
## 10. 业务流程实现路径
|
||||
- 按业务流程列出实现步骤
|
||||
- 每个步骤涉及的 View / ViewModel / Service
|
||||
|
||||
## 11. rules 与 skills 更新
|
||||
- 需要更新的现有 rules(architecture.md、wpf-core.md、wpf-xaml.md 等)
|
||||
- 需要新增的 rules(如 devexpress-wpf.md、handycontrol-usage.md、硬件相关 skill/rule)
|
||||
- 需要新增的 skills
|
||||
|
||||
## 12. 文档
|
||||
- README.md 更新内容
|
||||
- 架构文档
|
||||
- 开发指南
|
||||
|
||||
## 13. 任务执行顺序(分模块、多轮执行)
|
||||
|
||||
**核心原则:Plan 是给多轮对话逐步执行的,不允许一次性写完所有代码。** 任务拆分必须满足:
|
||||
|
||||
- **按模块拆分**:每个功能模块(或页面)是一个独立的执行单元,一轮对话只负责一个模块。
|
||||
- **按层级拆分**:基础设施层 → 框架层 → 业务模块层,先完成底层再建上层。
|
||||
- **每轮有明确的验收标准**:每轮结束必须能编译通过并对应当前阶段的功能。
|
||||
|
||||
### 推荐的分轮执行方案
|
||||
|
||||
| 轮次 | 范围 | 说明 |
|
||||
|------|------|------|
|
||||
| **Round 1** | 基础设施配置 | NuGet 包安装、slnx 更新、App.xaml 资源字典、DI 注册基建设置 |
|
||||
| **Round 2** | 框架层完善 | Model/Entity 实体类、FreeSql CodeFirst 配置、通用服务(消息框等) |
|
||||
| **Round 3-N** | 功能模块逐一实现 | 每个模块包含:Views + ViewModels + Services,一模块一轮 |
|
||||
| **Round N+1** | 主窗口整合 | 组装各模块到主窗口(Shell),配置导航、菜单结构 |
|
||||
| **Round 最后** | 验收与修复 | 完整编译 + 启动验证 + 原型图对比修复 |
|
||||
|
||||
### 任务标注格式
|
||||
|
||||
每个任务标注:
|
||||
- 优先级(P0 必须 / P1 重要 / P2 可选)
|
||||
- 所属轮次(Round X)
|
||||
- 依赖关系(任务 B 依赖任务 A)
|
||||
- 预计工作量(小 / 中 / 大)
|
||||
- 验证标准(每个任务如何确认完成)
|
||||
|
||||
### 示例
|
||||
|
||||
```markdown
|
||||
### Round 1: 基础设施配置
|
||||
|
||||
- [P0] 安装 NuGet 包 → 验证:dotnet restore 无报错
|
||||
- [P0] 更新 slnx 添加新模块项目 → 验证:dotnet build 无报错
|
||||
- [P1] 配置 App.xaml 资源字典 → 验证:启动后主题生效
|
||||
|
||||
### Round 3: 报警日志模块
|
||||
|
||||
- [P0] 创建 AlarmLogMainView.xaml + AlarmLogMainViewModel → 验证:页面可导航到
|
||||
- [P0] 实现报警数据查询 Service → 验证:Service 返回测试数据
|
||||
- [P1] 集成图表库显示报警趋势 → 验证:图表正确渲染
|
||||
```
|
||||
|
||||
## 14. 验收标准
|
||||
- 编译通过:dotnet build 无错误
|
||||
- 启动正常:应用可正常启动并显示主窗口
|
||||
- 基础流程跑通:(根据业务材料定义的最小可用流程)
|
||||
|
||||
## 15. 待补充 / 待确认事项
|
||||
- 列出所有未确定的决策点(用户在 Step 3 未提供的部分)
|
||||
- 列出需要用户进一步澄清的问题
|
||||
|
||||
## 16. 执行阶段代码质量准则
|
||||
|
||||
以下准则应作为 Plan 的一部分写入,指导后续执行对话中 AI 生成代码的质量。这些不是 Plan 生成阶段需要执行的,而是 Plan 中为执行阶段预设的约束。
|
||||
|
||||
### 16.1 csproj 与 slnx 管理
|
||||
|
||||
- 每新建一个 `.csproj` 项目,必须同时将其添加到解决方案文件(`.slnx`)中。
|
||||
- Plan 中应明确列出每个新建项目对应的 slnx 节位置。
|
||||
|
||||
### 16.2 XAML 命名空间规范
|
||||
|
||||
AI 生成 View.xaml 时常见的错误是使用错误的命名空间前缀(如 `local:XXXViewModel`)。Plan 中应规定正确的 XAML 命名空间用法:
|
||||
|
||||
- **不要在 XAML 中使用 `d:DataContext` 或错误的 `local:` 前缀指定 ViewModel 类型。**
|
||||
- 正确做法是使用模块专用的命名空间前缀 + `core:DISource`:
|
||||
|
||||
```xml
|
||||
<!-- 正确示例:使用 vms 命名空间 + DI Source -->
|
||||
<UserControl xmlns:vms="clr-namespace:CFBG_HostSoftware.AlarmLog.ViewModels"
|
||||
DataContext="{core:DISource Type=vms:AlarmLogMainViewModel}">
|
||||
```
|
||||
|
||||
- Plan 中应明确每个模块 View 对应的 ViewModel 命名空间和类型,作为执行阶段的参照。
|
||||
|
||||
### 16.3 自定义 Window 的默认行为
|
||||
|
||||
- 优先使用库自带或 WPF 自带的 `Window` 作为窗口基类。
|
||||
- 如果确实需要自定义 `Window` 样式,必须保留标准 Windows 窗口行为:标题栏拖拽移动、双击最大化/还原、系统菜单(右键标题栏)、窗口边框调整大小等。
|
||||
- 推荐:直接使用 WPF 原生 `Window` + 样式模板,而非完全重写窗口行为。
|
||||
|
||||
### 16.4 图表控件(ScottPlot / MAX.Common.Plot5)
|
||||
|
||||
- `MAX.Common.Plot5` 暴露的不是 ScottPlot 原生的 `WpfPlot`,而是封装的 `PlotControl`。
|
||||
- 如果 Plan 中涉及使用图表,应在 Framework 项目中新建一个子类继承 `PlotControl`:
|
||||
|
||||
```csharp
|
||||
// Source/PipeReviewReport.Framework/Controls/PlotControlEx.cs
|
||||
using MAX.Common.Plot5;
|
||||
|
||||
namespace PipeReviewReport.Framework.Controls;
|
||||
|
||||
public class PlotControlEx : PlotControl
|
||||
{
|
||||
// 可扩展自定义绘图逻辑
|
||||
}
|
||||
```
|
||||
|
||||
- 所有业务模块中的图表使用 `PlotControlEx` 而非直接使用 `PlotControl` 或 `WpfPlot`。
|
||||
```
|
||||
|
||||
### 4.2 生成 Plan 的注意事项
|
||||
|
||||
1. **基于实际材料**:UI 部分、模块划分、数据模型必须基于 Step 3 收到的材料,不要凭空编造业务细节。
|
||||
2. **基于现状检查**:利用 Step 0 的检查结果,标注已有项、避免重复添加、优先复用已有模块和配置。
|
||||
3. **标注不确定性**:对于材料不足的部分,在「待补充事项」中明确列出,不要假装知道。
|
||||
4. **保持可执行性**:每个任务都要具体到「改哪个文件 / 加什么代码」,让用户(或后续 AI 对话)可以直接照做。
|
||||
5. **遵循项目约定**:所有方案必须符合 `.agents/rules/` 下的现有规范(MVVM 模式、DI 约定、命名空间约定等)。
|
||||
6. **分模块可迭代**:任务拆分必须支持多轮对话逐步执行(详见「16. 执行阶段代码质量准则」),不允许设计为一次性的全量代码生成。
|
||||
7. **写文件**:最终 plan 写入 `.cursor/plans/initialize-plan.md`(如果目录不存在,在 plan 中说明需要先创建)。
|
||||
8. **输出摘要**:在对话中输出 plan 的摘要 + 文件路径,提醒用户确认后再执行。
|
||||
|
||||
---
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] Step 0:已完成项目现状检查(slnx、csproj、现有模块、App.xaml、rules、skills 等)
|
||||
- [ ] Step 0:检查结果已整理,作为 Plan 中「已有」标注的依据
|
||||
- [ ] Step 1:已通过 AskQuestion 收集完所有技术栈问题(10 个问题)
|
||||
- [ ] Step 2:已生成并展示 Plan v1(技术栈 + 现状检查结果)
|
||||
- [ ] Step 3:已通过 AskQuestion 询问材料类型,并请求用户实际提供
|
||||
- [ ] Step 3:如涉及硬件读写(`hardware-interaction`),已向用户确认 Mock 策略(mock-first / real-hardware)
|
||||
- [ ] Step 3:已等待用户材料(或用户明确选择「暂无」)
|
||||
- [ ] Step 4:已综合所有材料生成完整 plan
|
||||
- [ ] Step 4:plan 包含「分模块多轮执行」方案,任务按模块拆分、可逐轮执行
|
||||
- [ ] Step 4:plan 包含「执行阶段代码质量准则」(XAML 命名空间、Window 行为、PlotControl 使用、slnx 更新)
|
||||
- [ ] Step 4:如涉及硬件,plan 包含硬件抽象层(接口定义、Mock/真实实现、DI 注册策略、替换保障)及硬件相关 skill/rule 创建任务
|
||||
- [ ] Step 4:plan 已写入 `.cursor/plans/initialize-plan.md`
|
||||
- [ ] Step 4:已在对话中输出摘要并提醒用户确认
|
||||
- [ ] 全程未执行任何实际代码 / 配置 / 文件变更
|
||||
|
||||
## 重要约束
|
||||
|
||||
1. **本 skill 是 Plan 生成器,不是执行器**:在整个过程中,禁止修改任何源码、配置、csproj、rules、skills。
|
||||
2. **三步流程缺一不可**:Step 0 现状检查 → Step 1-2 技术栈 → Step 3-4 业务材料,不能跳过任何步骤。
|
||||
3. **Plan 必须支持分模块多轮执行**:不允许生成"一次性全量代码写入"式的 plan。任务必须按模块/页面拆分,每轮可独立验证。
|
||||
4. **不要替用户做业务决策**:如果材料不足,明确列出「待补充事项」,不要编造业务流程或数据模型。
|
||||
5. **保持中立**:推荐技术方案时给出理由和备选,不要绝对化。
|
||||
6. **最终交付物只有一份 plan 文件**:除了 `.cursor/plans/initialize-plan.md` 外,不应创建任何其他文件。
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: max-common-wpf
|
||||
description: 在 Windows WPF 项目中接入和使用 MAX.Common.WPF 的控件、转换器、附加行为、枚举绑定、视图生命周期、防抖和虚拟化功能。适用于明确使用 MAX.Common.WPF 或已引用该库的界面开发;不适用于 WinUI、Avalonia 或 MAUI。
|
||||
---
|
||||
|
||||
# MAX.Common.WPF 功能使用
|
||||
|
||||
根据消费项目需求使用现有 WPF 能力。此 Skill 可独立复制使用,不要求安装其他 Skill,也不包含 NuGet 包或原始库源码。
|
||||
|
||||
## 接入与参考版本
|
||||
|
||||
- 先检查 `.csproj`、集中版本配置、App.xaml 资源字典和既有样式,不改变项目的应用框架或依赖管理方式。
|
||||
- 基于 2026-10-27 源码快照:`MAX.Common.WPF` 1.4.23,依赖 `MAX.Common` 1.4.17;目标为 `net472;net48;net481;net6.0-windows;net7.0-windows;net8.0-windows;net9.0-windows;net10.0-windows`。这些是参考版本,不代表最新发布版本。
|
||||
- 优先使用项目现有兼容版本。新接入时选与项目 TFM 匹配的包资产,现代 .NET 项目设置 `<UseWPF>true</UseWPF>` 并使用 Windows TFM;不要将普通 netstandard 类库改成 WPF。
|
||||
- 普通项目通过 `PackageReference Include="MAX.Common.WPF" Version="1.4.23"` 接入,集中包管理按现有模式拆分版本。MAX.Common 由包依赖传入,通常无需重复添加;MAX.Core、通知系统与 IoC 不属于本 Skill 必需依赖。
|
||||
|
||||
## 功能定位
|
||||
|
||||
按任务读取 [功能索引](references/features.md)。具体命名空间、属性和方法在 [常用 API 声明](references/api.md) 中按类型搜索。声明摘录没有完整方法体;行为不明确时查看当前安装包 XML 文档、反射或可用源码,再以编译和运行验证。消费项目没有原仓库也应能使用本指南。
|
||||
|
||||
## XAML 资源入口
|
||||
|
||||
程序集声明了 `xmlns:wpf="MAX.WPF"`。控件样式和共享转换器位于 `Resources/Generic.xaml`,不要猜成 `Themes/Generic.xaml`。需要这些资源时,在现有 Application.Resources 的 MergedDictionaries 中合并一次:
|
||||
|
||||
```xml
|
||||
<ResourceDictionary Source="/MAX.Common.WPF;component/Resources/Generic.xaml"/>
|
||||
```
|
||||
|
||||
页面最小示例(绑定属性由页面的 DataContext 提供):
|
||||
|
||||
```xml
|
||||
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:wpf="MAX.WPF">
|
||||
<StackPanel.Resources>
|
||||
<wpf:Boolean2VisibilityConverter x:Key="ShowWhenTrue"/>
|
||||
</StackPanel.Resources>
|
||||
<wpf:NumberBox Minimum="0" Maximum="100" Increment="1"
|
||||
Value="{Binding Amount, Mode=TwoWay}"/>
|
||||
<TextBlock Text="处理中"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource ShowWhenTrue}}"/>
|
||||
</StackPanel>
|
||||
```
|
||||
|
||||
不要假设所有转换器都已登记为全局资源;可以显式实例化所需转换器。`Boolean2VisibilityConverter` 的 false 和非 bool 输入返回 Collapsed。
|
||||
|
||||
## 常见集成约束
|
||||
|
||||
- 枚举选择优先使用 `EnumBindingMarkupExtension` 配合 `EnumBindingExtension.SelectedEnum`;`EnumBindingComboBox` 已标记 Obsolete。标记扩展返回 `EnumBindingItem`(Display、Value),不是裸枚举列表;显示与选择路径需要对应包装对象。
|
||||
- `ViewModelBase` 继承 `MAX.Common.Models.BindModelBase`。只有需要视图事件与 Loaded / LoadedOnce / Unloaded 钩子时才采用;不要替换现有框架的基类。通过 `AssignView(view)` 关联一次稳定视图后才能使用其 Dispatcher。
|
||||
- `ViewModelExtension.AutoAssignViewModel` 仅监听启用后的 DataContextChanged,不会立即处理已经设置的 DataContext;需要先启用再设置 DataContext,或显式 AssignView。基类没有提供完整的换视图解绑流程,不要把同一实例反复关联不同视图。
|
||||
- `DebounceDispatcher.Debounce(TimeSpan, Action)` 基于 DispatcherTimer,应从 UI 线程调用并复用实例;不能每次输入都创建新实例。
|
||||
- `AsyncDebouncer.Invoke(Func<Task>)` 通过 Task.Run 执行回调,UI 更新要回到 Dispatcher。新调用取消等待,不会取消已经开始的业务任务;回调内处理业务异常,搜索结果另行防止旧请求覆盖新请求。它没有可 await 的 InvokeAsync 或公开 Cancel/Dispose API。
|
||||
- `DispatcherHelper.RunInDispatcher` 在非目标线程同步 Invoke;省略 Dispatcher 的重载依赖 `Application.Current`。不要在 UI 线程同步等待同时需要 Dispatcher 的后台任务。
|
||||
- 虚拟化依赖有限可视区域、滚动宿主和 ItemsPanel 配置。`VirtualizingItemsControl` 默认使用 VirtualizingStackPanel,需要换行时显式设置 VirtualizingWrapPanel。不能仅添加面板便承诺大数据性能。
|
||||
- 通用命令在 `MAX.Common.Commands`,不是 WPF.Commands。MethodCommand<T> / AsyncMethodCommand<T> 的 T 需引用类型或可空值类型,状态变化手动 RaiseCanExecuteChanged;异步命令委托需自行处理业务异常。
|
||||
|
||||
## 枚举绑定示例
|
||||
|
||||
已有 `xmlns:local` 指向业务枚举命名空间、`SelectedStatus` 是 `Status` 属性时:
|
||||
|
||||
```xml
|
||||
<ComboBox ItemsSource="{wpf:EnumBindingMarkup EnumType={x:Type local:Status}}"
|
||||
DisplayMemberPath="Display"
|
||||
SelectedValuePath="Value"
|
||||
wpf:EnumBindingExtension.SelectedEnum="{Binding SelectedStatus, Mode=TwoWay}"/>
|
||||
```
|
||||
|
||||
允许空选项时使用 AllowNull 并让业务属性可空;自定义显示通过扩展的 Converter 提供,勿假设自动读取所有枚举描述标记。
|
||||
|
||||
## 验证与交付
|
||||
|
||||
构建实际消费项目的目标框架。运行窗口检查资源解析、DataContext、双向绑定、命令、UI 线程和卸载后的行为。触及虚拟化时用实际数据与滚动验证;触及防抖时检查连续输入和结果乱序。仅编译不能证明 XAML 资源与运行时绑定正确。交付时列出改动文件、包版本和实际验证情况,可按需继续代码审查。
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "MAX.Common.WPF"
|
||||
short_description: "复用 MAX.Common.WPF 的控件、绑定、行为、防抖与虚拟化能力"
|
||||
@@ -0,0 +1,345 @@
|
||||
# 常用 API 声明参考
|
||||
|
||||
来源:MAX.Common.WPF,2026-10-27 工作区源码快照。
|
||||
|
||||
按下方主题或文件名搜索。仅摘录选定文件的 public / protected 声明;省略方法体、初始化表达式与 XML 注释。这是定位参考,不是可直接编译的 C#,不保证包含嵌套类型的完整可见性上下文。行为说明见 SKILL.md;目标包版本不同时以实际包为准。
|
||||
|
||||
目录:控件与布局、枚举、视图与绑定、线程与防抖、虚拟化与转换器
|
||||
|
||||
## 控件与布局
|
||||
|
||||
### Controls/NumberBox.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class NumberBox : TextBox
|
||||
public NumberBox()
|
||||
protected Button UpButton
|
||||
protected Button DownButton
|
||||
public override void OnApplyTemplate()
|
||||
protected virtual void DataObjectPasting(object sender, DataObjectPastingEventArgs e)
|
||||
protected virtual void OnValueChanged(PayloadRoutedEventArgs<double> e)
|
||||
public event EventHandler<PayloadRoutedEventArgs<double>> ValueChanged
|
||||
public double Value
|
||||
public double Maximum
|
||||
public double Minimum
|
||||
public double Increment
|
||||
public int? DecimalPlaces
|
||||
public string ValueFormat
|
||||
public bool ShowUpDownButton
|
||||
```
|
||||
|
||||
### Controls/TitleContent.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class TitleContent : ContentControl
|
||||
public object Title
|
||||
public double TitleWidth
|
||||
public HorizontalAlignment TitleHorizontalAlignment
|
||||
public VerticalAlignment TitleVerticalAlignment
|
||||
public TitlePlacementType Placement
|
||||
public Thickness TopTitlePadding
|
||||
public Thickness LeftTitlePadding
|
||||
public TitleContent()
|
||||
```
|
||||
|
||||
### Controls/BoolContent.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class BoolContent : Control
|
||||
public object TrueContent
|
||||
public object FalseContent
|
||||
public bool Bool
|
||||
public CornerRadius CornerRadius
|
||||
```
|
||||
|
||||
### Controls/SpacingStackPanel.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class SpacingStackPanel : Panel
|
||||
public Orientation Orientation
|
||||
public bool ChildWrapping
|
||||
public double HorizontalSpacing
|
||||
public double VerticalSpacing
|
||||
public double ItemWidth
|
||||
public double ItemHeight
|
||||
public HorizontalAlignment? ItemHorizontalAlignment
|
||||
public VerticalAlignment? ItemVerticalAlignment
|
||||
```
|
||||
|
||||
### Controls/SpacingUniformGrid.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class SpacingUniformGrid : Panel
|
||||
public int Rows
|
||||
public int Columns
|
||||
public double RowSpacing
|
||||
public double ColumnSpacing
|
||||
```
|
||||
|
||||
### Controls/AutoColumnsUniformGrid.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class AutoColumnsUniformGrid : UniformGrid
|
||||
public double CellMinWidth
|
||||
public int MinColumns
|
||||
public int MaxColumns
|
||||
```
|
||||
|
||||
### Controls/ButtonPopup.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class ButtonPopup : CustomPopup
|
||||
public ButtonBase SourceButton
|
||||
public ICommand OnClickOpenCommand
|
||||
public ButtonPopup() : base()
|
||||
```
|
||||
|
||||
### Controls/NotifyIcon.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Controls`
|
||||
|
||||
```csharp
|
||||
public class NotifyIcon : FrameworkElement, IDisposable
|
||||
public static string WindowClassNamePrefix
|
||||
public NotifyIcon()
|
||||
public void Initialize()
|
||||
public static void ShowBalloonTip(string title, string content, NotifyIconInfoType infoType, string token)
|
||||
public void ShowBalloonTip(string title, string content, NotifyIconInfoType infoType)
|
||||
public void Dispose()
|
||||
public void CloseContextControl()
|
||||
public string Token
|
||||
public string Text
|
||||
public ImageSource Icon
|
||||
public object ContextContent
|
||||
public TimeSpan BlinkInterval
|
||||
public bool IsBlink
|
||||
public event RoutedEventHandler Click
|
||||
public event RoutedEventHandler MouseDoubleClick
|
||||
```
|
||||
|
||||
## 枚举、视图与绑定
|
||||
|
||||
### MarkupExtensions/EnumBindingMarkupExtension.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.MarkupExtensions`
|
||||
|
||||
```csharp
|
||||
public class EnumBindingMarkupExtension : MarkupExtension
|
||||
public IValueConverter Converter
|
||||
public object ConverterParameter
|
||||
public Type EnumType
|
||||
public bool AllowNull
|
||||
public object NullDisplay
|
||||
public bool DisplayNullLast
|
||||
public EnumBindingMarkupExtension()
|
||||
public EnumBindingMarkupExtension(Type enumType)
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
public class EnumBindingItem
|
||||
public object Display
|
||||
public object Value
|
||||
public override string ToString()
|
||||
public override bool Equals(object obj)
|
||||
public override int GetHashCode()
|
||||
```
|
||||
|
||||
### Extensions/EnumBindingExtension.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Extensions`
|
||||
|
||||
```csharp
|
||||
public static class EnumBindingExtension
|
||||
public static void SetSelectedEnum(DependencyObject element, object value)
|
||||
public static object GetSelectedEnum(DependencyObject element)
|
||||
```
|
||||
|
||||
### Extensions/ViewModelExtension.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Extensions`
|
||||
|
||||
```csharp
|
||||
public class ViewModelExtension
|
||||
public static bool GetAutoAssignViewModel(DependencyObject obj)
|
||||
public static void SetAutoAssignViewModel(DependencyObject obj, bool value)
|
||||
```
|
||||
|
||||
### Extensions/BindingProxies.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Extensions`
|
||||
|
||||
```csharp
|
||||
public abstract class BindingProxyBase<T> : Freezable
|
||||
public T Data
|
||||
public class BindingProxy : BindingProxyBase<object>
|
||||
public class BooleanProxy : BindingProxyBase<bool>
|
||||
```
|
||||
|
||||
### Extensions/TextBoxExtension.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Extensions`
|
||||
|
||||
```csharp
|
||||
public static class TextBoxExtension
|
||||
public static bool GetHandleEnterToNextFocus(DependencyObject obj)
|
||||
public static void SetHandleEnterToNextFocus(DependencyObject obj, bool value)
|
||||
public static bool GetHandleEscapeToUndoText(DependencyObject obj)
|
||||
public static void SetHandleEscapeToUndoText(DependencyObject obj, bool value)
|
||||
```
|
||||
|
||||
### Extensions/ButtonExtension.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Extensions`
|
||||
|
||||
```csharp
|
||||
public static class ButtonExtension
|
||||
public static bool GetIsChecked(DependencyObject obj)
|
||||
public static void SetIsChecked(DependencyObject obj, bool value)
|
||||
public static bool GetSetPreviewClick(DependencyObject obj)
|
||||
public static void SetSetPreviewClick(DependencyObject obj, bool value)
|
||||
```
|
||||
|
||||
## 线程与防抖
|
||||
|
||||
### Helpers/DispatcherHelper.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Helpers`
|
||||
|
||||
```csharp
|
||||
public static class DispatcherHelper
|
||||
public static void RunInDispatcher(Dispatcher dispatcher, Action action)
|
||||
public static Dispatcher AppDispatcher
|
||||
public static void RunInDispatcher(Action action)
|
||||
```
|
||||
|
||||
### Utilities/DebounceDispatcher.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Utilities`
|
||||
|
||||
```csharp
|
||||
public class DebounceDispatcher
|
||||
public void Debounce(TimeSpan delay, Action action)
|
||||
```
|
||||
|
||||
### Utilities/AsyncDebouncer.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Utilities`
|
||||
|
||||
```csharp
|
||||
public class AsyncDebouncer(int delayMilliseconds = 300)
|
||||
public void Invoke(Func<Task> action)
|
||||
```
|
||||
|
||||
### Utilities/Timers/TimerControllerBase.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Utilities.Timers`
|
||||
|
||||
```csharp
|
||||
public abstract class TimerControllerBase : IDisposable
|
||||
public abstract void Dispose()
|
||||
```
|
||||
|
||||
### Utilities/Timers/DispatcherTimerController.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Utilities.Timers`
|
||||
|
||||
```csharp
|
||||
public class DispatcherTimerController : TimerControllerBase
|
||||
public event TypedEventHandler<DispatcherTimerController, Exception> ExceptionOccurred
|
||||
public DispatcherTimerController(DispatcherPriority dispatcherPriority, TimeSpan interval, Func<Task> actionAsync, Dispatcher dispatcher = null) : this(dispatcherPriority, interval, dispatcher)
|
||||
public DispatcherTimerController(DispatcherPriority dispatcherPriority, TimeSpan interval, Action action, Dispatcher dispatcher = null) : this(dispatcherPriority, interval, dispatcher)
|
||||
public virtual void OnException(Exception exception)
|
||||
public virtual void Start()
|
||||
public virtual void Stop()
|
||||
public override void Dispose()
|
||||
```
|
||||
|
||||
## 虚拟化与转换器
|
||||
|
||||
### Virtualization/VirtualizingItemsControl.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Virtualization`
|
||||
|
||||
```csharp
|
||||
public class VirtualizingItemsControl : ItemsControl
|
||||
public VirtualizingItemsControl()
|
||||
```
|
||||
|
||||
### Virtualization/VirtualizingWrapPanel.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Virtualization`
|
||||
|
||||
```csharp
|
||||
public class VirtualizingWrapPanel : VirtualizingPanelBase
|
||||
public Orientation Orientation
|
||||
public Size ItemSize
|
||||
public bool AllowDifferentSizedItems
|
||||
public IItemSizeProvider? ItemSizeProvider
|
||||
public ItemAlignment ItemAlignment
|
||||
public SpacingMode SpacingMode
|
||||
public bool StretchItems
|
||||
public bool IsGridLayoutEnabled
|
||||
public void ClearItemSizeCache()
|
||||
```
|
||||
|
||||
### Virtualization/IItemSizeProvider.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Virtualization`
|
||||
|
||||
```csharp
|
||||
public interface IItemSizeProvider
|
||||
```
|
||||
|
||||
### Converters/Boolean2VisibilityConverter.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Converters`
|
||||
|
||||
```csharp
|
||||
public class Boolean2VisibilityConverter : IValueConverter
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
```
|
||||
|
||||
### Converters/EnumDisplayConverter.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Converters`
|
||||
|
||||
```csharp
|
||||
public class EnumDisplayConverter : IValueConverter
|
||||
public static EnumDisplayManagerBase EnumDisplayManager
|
||||
public EnumDisplayManagerBase SelfEnumDisplayManager
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
```
|
||||
|
||||
### Converters/FormatConverter.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Converters`
|
||||
|
||||
```csharp
|
||||
public class FormatConverter : IMultiValueConverter
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
```
|
||||
|
||||
### Converters/StringJoinConverter.cs
|
||||
|
||||
命名空间:`MAX.Common.WPF.Converters`
|
||||
|
||||
```csharp
|
||||
public class StringJoinConverter : IValueConverter
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# WPF 功能索引
|
||||
|
||||
参考版本:MAX.Common.WPF 1.4.23。以下均以 `MAX.Common.WPF` 为根命名空间。只选任务需要的能力,常用成员见同目录 api.md。
|
||||
|
||||
| 需求 | 类型 / 命名空间 | 使用要点 |
|
||||
| --- | --- | --- |
|
||||
| 数值输入 | Controls.NumberBox | Value 为 double;Minimum、Maximum、Increment、DecimalPlaces、ValueFormat、ShowUpDownButton;模板资源在 Resources/Generic.xaml |
|
||||
| 标题与条件内容 | Controls.TitleContent、BoolContent | TitleContent 有 Placement 等布局属性;BoolContent 用 Bool、TrueContent、FalseContent |
|
||||
| 间距布局 | Controls.SpacingStackPanel、SpacingUniformGrid、AutoColumnsUniformGrid | 区分面板间距与子元素 Margin,按声明配置 |
|
||||
| 图标与链接 | Controls.FontIcon、HyperlinkEx | FontIcon 继承 TextBlock,具体字体和字形必须与项目资源一致 |
|
||||
| 按钮与弹出层 | Controls.MenuButton、ButtonPopup、CustomPopup、ClippingBorder | 按实际属性处理关闭、焦点和裁切,不猜测通用 Popup 行为 |
|
||||
| 托盘图标 | Controls.NotifyIcon;Enums.NotifyIconInfoType | 涉及 Win32 资源与应用生命周期,检查实际清理 API |
|
||||
| DataGrid | Controls.DataGridExtended;Extensions.DataGridExtension;Helpers.DataGridHelper | 先区分扩展控件、附加属性和普通 helper |
|
||||
| 标准控件附加行为 | Extensions.TextBoxExtension、ButtonExtension、ListBoxExtension、TreeViewExtension、SelectorExtension、ScrollViewerExtension、ProgressBarExtension、ToolTipExtension | 搜索对应 Get/Set 方法确认附加属性名称、默认值与事件机制 |
|
||||
| 通用控件行为 | Extensions.ControlExtension(Context / DragDrop / Focus / Keyboard / Layout / Loaded / Mouse 分文件)、WindowExtension、GridLayoutHelper | 使用实际 API 名称,不把文件名当作附加属性名 |
|
||||
| 上下文菜单和绑定代理 | Extensions.ContextMenuExtension、ContextMenuListener、BindingProxy / BooleanProxy、CommandProxy;Models.ContextMenuModel;ViewModels.ContextMenuViewModel | Popup/ContextMenu 不在普通可视树,核对 DataContext 来源 |
|
||||
| 枚举选择与显示 | MarkupExtensions.EnumBindingMarkupExtension;Extensions.EnumBindingExtension;Converters.EnumDisplayConverter;Utilities.EnumDisplayManagerWPF | ItemsSource 返回包装对象;旧 EnumBindingComboBox 已弃用 |
|
||||
| 布尔、空值与可见性 | Converters.Boolean2VisibilityConverter、Boolean2VisibilityReConverter、Object2VisibilityConverter、String2VisibilityConverter、IListToVisibilityConverter、BooleanArr2VisibilityConverter 等 | 各 Converter 对 null、空串、集合的定义不同,核对实现;多值转换器需 MultiBinding |
|
||||
| 格式、颜色、尺寸转换 | Converters.FileSizeDisplayConverter、DateTimeFormatConverter、FormatConverter、StringJoinConverter、ColorToBrushConverter、ContrastBrushConverter、ThicknessSplitConverter、CornerRadiusSplitConverter 等 | 按 IValueConverter / IMultiValueConverter 及 ConvertBack 支持情况选择绑定模式 |
|
||||
| 视图生命周期 | ViewModels.ViewModelBase;Extensions.ViewModelExtension | LoadedOnce / Loaded / Unloaded;关联视图并非 IoC 自动创建 ViewModel |
|
||||
| UI 调度与防抖 | Helpers.DispatcherHelper;Utilities.DebounceDispatcher、AsyncDebouncer | UI Dispatcher 与后台 Task.Run 的回调线程不同 |
|
||||
| 定时控制 | Utilities.Timers.DispatcherTimerController、SystemTimerController、ThreadingTimerController | 依据 UI 访问要求选类型,并核对停止与释放方法 |
|
||||
| 虚拟化列表和换行 | Virtualization.VirtualizingItemsControl、VirtualizingWrapPanel、VirtualizingWrapPanelWithItemExpansion、IItemSizeProvider | 保持有限视口与滚动配置;不同尺寸和展开项需单独验证 |
|
||||
| 窗口、快捷键与系统 | Helpers.WindowHelper、WindowHotKeyActionsHelper、KeyboardHelper、SystemHelper、SecurityHelper、FileActionHelper;Utilities.WindowActivator、JobObject | Windows 特定能力,按 HWND/资源生命周期使用 |
|
||||
| 视觉树、颜色、图标 | Helpers.ViewHelper、ViewDebugHelper、ColorHelper、IconHelper、Vector2Helper | 与普通 MAX.Common 数据类型和工具区分 |
|
||||
| 属性监听与筛选 | Utilities.PropertyChangeNotifier、AttachedCommandRegister;Models.CollectionSearchFilter、ControlTokenManager、AppDiagnosticsModel | 阅读订阅、解除和集合更新约定 |
|
||||
|
||||
## 资源和版本核对
|
||||
|
||||
同一 `MAX.WPF` XML 命名空间映射了 Controls、Converters、Extensions、MarkupExtensions、Virtualization 等 CLR 命名空间;C# using 仍需写真实 CLR 命名空间。资源字典中显式定义了 NumberBox、TitleContent、BoolContent 样式以及多个转换器,不能推断其他控件具有同样的默认样式加载机制。
|
||||
|
||||
需要索引未列的成员时,搜索已安装包 XML 文档或反射公共成员;不要根据相似 UI 库推测 API。
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: max-common
|
||||
description: 在 .NET 项目中接入和使用 MAX.Common,复用其命令、属性通知、集合视图、数值与文件工具、枚举和事件功能。适用于明确使用 MAX.Common 或项目已引用该库的开发任务;WPF 控件和附加行为由 max-common-wpf 处理。
|
||||
---
|
||||
|
||||
# MAX.Common 功能使用
|
||||
|
||||
帮助消费项目复用 MAX.Common 的真实能力,避免重复实现已有工具。此 Skill 是使用指南,不包含库二进制,也不要求访问原开发仓库。
|
||||
|
||||
## 接入与版本
|
||||
|
||||
- 先检查消费项目的 `.csproj`、`Directory.Packages.props`、NuGet 配置和已有 using,保留其依赖管理方式。
|
||||
- 本参考基于 2026-10-27 的源码快照:包 `MAX.Common` 1.4.17,目标为 `netstandard2.0;netstandard2.1`。这是快照版本,不代表最新版本。优先沿用项目已安装的兼容版本;需要新增时使用可用源中的确定版本。
|
||||
- 普通项目通过 `PackageReference Include="MAX.Common" Version="1.4.17"` 接入;集中包管理项目将版本放到 `PackageVersion`。源码工程采用 C# 12,不代表所有调用项目都必须使用 C# 12。
|
||||
- `PolySharp` 是库构建依赖(PrivateAssets=all),不要仅为调用库强制添加它。没有必要为使用本库引入 MAX.Core、Prism 或其他应用框架。
|
||||
|
||||
## 按需求选择功能
|
||||
|
||||
先阅读 [功能索引](references/features.md) 中与任务对应的部分。需要确定命名空间、构造参数和方法名称时,搜索 [常用 API 声明](references/api.md) 的文件名或类型名;这是选定功能的声明摘录,不是全部 API,也不包含完整实现语义。
|
||||
|
||||
引用的包版本与快照不同时,以本地已安装包 XML 文档、反射结果、源码或编译器为准。没有源码时不假设原仓库存在,更不编造未收录的方法。
|
||||
|
||||
## 已确认的使用约束
|
||||
|
||||
- `MethodCommand<T>` 和 `AsyncMethodCommand<T>` 只接受引用类型或可空值类型,例如 `int?`;`int` 会在构造时抛出异常。传入的实际 CommandParameter 仍必须能转换为 T。
|
||||
- 命令状态变化后调用 `RaiseCanExecuteChanged()`;库不会自动观察业务属性。同步命令的 `Execute` 不检查 `CanExecute`,程序直接执行时自行判断。
|
||||
- `AsyncMethodCommand` 使用 `async void Execute`,执行期间拒绝重复执行,通过 finally 恢复状态,但不吞掉业务异常。需要呈现错误时在异步委托中捕获并处理;它不是可 await 的 `ExecuteAsync` API,也不是跨线程互斥锁。
|
||||
- `BindModelBase` 使用 `SetProperty(ref field, value, ...)`;`BindableBaseEx` 同时提供属性包 `GetValue/SetValue` 和字段方式。两者构造函数都会调用虚方法 `OnInitialized()`,不要在该回调中依赖派生构造函数才完成的初始化。
|
||||
- `BindModelBase` 先执行 onChanged 再通知属性变化,`BindableBaseEx` 的字段设置先通知再调用 changedCallback。涉及通知顺序时不要将二者视为完全等价。
|
||||
- `AsyncEvent<TEventArgs>` 通过可空字段 `+=` 首次创建,构造函数私有;`InvokeAsync` 按注册顺序逐个 await,异常会中止后续回调。首次并发订阅存在竞争,不能当作线程安全事件总线。
|
||||
- `RunLimiter` 限制实例生命周期内的尝试次数,不是时间窗口节流,也不限制同时运行数;动作抛异常也已消耗计数。
|
||||
- `PathValidator.IsFolderPathValidAndSafe(path, allowRelativePath)` 检查路径格式,不保证存在、可写或位于允许的根目录;路径范围约束需要调用方另外实现。
|
||||
- `SerializationHelper.TryGetValue` 面向 `SerializationInfo`,失败返回默认值,不是 JSON 序列化工具。`MD5Helper`、Base64 和 URI 编码工具的名称不代表它们提供密码加密。
|
||||
|
||||
## 最小用法
|
||||
|
||||
```csharp
|
||||
using MAX.Common.Commands;
|
||||
using MAX.Common.Models;
|
||||
|
||||
public sealed class EditorModel : BindModelBase
|
||||
{
|
||||
private string name = string.Empty;
|
||||
public MethodCommand ClearCommand { get; }
|
||||
|
||||
public EditorModel()
|
||||
{
|
||||
ClearCommand = new MethodCommand(() => Name = string.Empty,
|
||||
() => Name.Length > 0);
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => name;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref name, value))
|
||||
ClearCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 验证与交付
|
||||
|
||||
按消费项目实际 TFM 构建。对使用到的行为验证关键边界,例如命令空参数与异常恢复、集合空源与索引、文件路径无效输入。只调用本库时无需构建整个原始解决方案。交付时说明引用版本、变更文件与验证结果;未执行的验证明确标注,可按需继续代码审查。
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "MAX.Common"
|
||||
short_description: "复用 MAX.Common 的命令、绑定模型、集合与通用工具能力"
|
||||
@@ -0,0 +1,381 @@
|
||||
# 常用 API 声明参考
|
||||
|
||||
来源:MAX.Common,2026-10-27 工作区源码快照。
|
||||
|
||||
按下方主题或文件名搜索。仅摘录选定文件的 public / protected 声明;省略方法体、初始化表达式与 XML 注释。这是定位参考,不是可直接编译的 C#,不保证包含嵌套类型的完整可见性上下文。行为说明见 SKILL.md;目标包版本不同时以实际包为准。
|
||||
|
||||
目录:命令与绑定模型、集合与数据、文件、数值与校验、事件与运行控制
|
||||
|
||||
## 命令与绑定模型
|
||||
|
||||
### Commands/MethodCommand.cs
|
||||
|
||||
命名空间:`MAX.Common.Commands`
|
||||
|
||||
```csharp
|
||||
public class MethodCommand( Action execute, Func<bool>? canExecute = null ) : ICommand
|
||||
public event EventHandler? CanExecuteChanged
|
||||
public bool CanExecute(object parameter)
|
||||
public void Execute(object parameter)
|
||||
public void RaiseCanExecuteChanged()
|
||||
public class MethodCommand<T> : ICommand
|
||||
public MethodCommand( Action<T> execute, Func<T, bool>? canExecute = null )
|
||||
public event EventHandler? CanExecuteChanged
|
||||
public bool CanExecute(object parameter)
|
||||
public void Execute(object parameter)
|
||||
public void RaiseCanExecuteChanged()
|
||||
```
|
||||
|
||||
### Commands/AsyncMethodCommand.cs
|
||||
|
||||
命名空间:`MAX.Common.Commands`
|
||||
|
||||
```csharp
|
||||
public class AsyncMethodCommand(Func<Task> execute, Func<bool>? canExecute = null) : ICommand
|
||||
public event EventHandler? CanExecuteChanged
|
||||
public bool CanExecute(object parameter)
|
||||
public async void Execute(object parameter)
|
||||
public void RaiseCanExecuteChanged()
|
||||
public class AsyncMethodCommand<T> : ICommand
|
||||
public event EventHandler? CanExecuteChanged
|
||||
public AsyncMethodCommand(Func<T, Task> execute, Func<T, bool>? canExecute = null)
|
||||
public bool CanExecute(object parameter)
|
||||
public async void Execute(object parameter)
|
||||
public void RaiseCanExecuteChanged()
|
||||
```
|
||||
|
||||
### Models/BindModelBase.cs
|
||||
|
||||
命名空间:`MAX.Common.Models`
|
||||
|
||||
```csharp
|
||||
public abstract class BindModelBase : INotifyPropertyChanged
|
||||
public event PropertyChangedEventHandler? PropertyChanged
|
||||
public BindModelBase()
|
||||
public virtual void OnInitialized()
|
||||
public virtual bool ShouldCheckValueMatching<T>(string? propertyName, T from, T to)
|
||||
protected virtual bool SetProperty<T>(ref T storage, T value, Action? onChanged = null, [CallerMemberName] string? propertyName = null)
|
||||
protected virtual bool SetPropertyNoCheck<T>(ref T storage, T value, Action? onChanged = null, [CallerMemberName] string? propertyName = null)
|
||||
protected virtual void OnPropertyChanging<T>(string? propertyName, T from, T to, CancelEventArgs cancelEventArgs)
|
||||
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
|
||||
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
```
|
||||
|
||||
### Models/BindableBaseEx.cs
|
||||
|
||||
命名空间:`MAX.Common.Models`
|
||||
|
||||
```csharp
|
||||
public abstract class BindableBaseEx : INotifyPropertyChanged
|
||||
public event PropertyChangedEventHandler? PropertyChanged
|
||||
public BindableBaseEx()
|
||||
public virtual void OnInitialized()
|
||||
public static string GetPropertyName<T>(Expression<Func<T>> expression)
|
||||
protected T? GetProperty<T>(Expression<Func<T>> expression)
|
||||
protected bool SetProperty<T>(Expression<Func<T>> expression, T value)
|
||||
protected bool SetProperty<T>(Expression<Func<T>> expression, T value, Action? changedCallback)
|
||||
protected bool SetProperty<T>(Expression<Func<T>> expression, T value, Action<T?>? changedCallback)
|
||||
protected bool SetProperty<T>(ref T storage, T value, Expression<Func<T>> expression)
|
||||
protected bool SetProperty<T>(ref T storage, T value, Expression<Func<T>> expression, Action? changedCallback)
|
||||
protected void RaisePropertyChanged<T>(Expression<Func<T>> expression)
|
||||
protected T? GetValue<T>([CallerMemberName] string? propertyName = null)
|
||||
protected bool SetValue<T>(T value, [CallerMemberName] string? propertyName = null)
|
||||
protected bool SetValue<T>(T value, Action? changedCallback, [CallerMemberName] string? propertyName = null)
|
||||
protected bool SetValue<T>(T value, Action<T?>? changedCallback, [CallerMemberName] string? propertyName = null)
|
||||
protected bool SetValue<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
|
||||
protected bool SetValue<T>(ref T storage, T value, Action? changedCallback, [CallerMemberName] string? propertyName = null)
|
||||
protected bool SetProperty<T>(ref T storage, T value, string propertyName)
|
||||
protected virtual bool SetProperty<T>(ref T storage, T value, string propertyName, Action? changedCallback)
|
||||
protected virtual bool SetPropertyCore<T>(string propertyName, T value, out T? oldValue)
|
||||
protected virtual void RaisePropertyChanged(string propertyName)
|
||||
protected void RaisePropertyChanged()
|
||||
protected void RaisePropertiesChanged(params string[]? propertyNames)
|
||||
protected virtual void VerifyAccess()
|
||||
protected virtual bool CompareValues<T>(T storage, T value, string propertyName)
|
||||
```
|
||||
|
||||
### Models/BindObject.cs
|
||||
|
||||
命名空间:`MAX.Common.Models`
|
||||
|
||||
```csharp
|
||||
public sealed class BindObject<T> : BindModelBase
|
||||
public event TypedEventHandler<BindObject<T>, PropertyValueUpdatedEventArgs<T>>? ValueChanged
|
||||
public T? Value
|
||||
public BindObject(T? defaultValue = default)
|
||||
public void Set(T value)
|
||||
public static implicit operator BindObject<T>(T value)
|
||||
public static implicit operator T?(BindObject<T> value)
|
||||
```
|
||||
|
||||
## 集合与数据
|
||||
|
||||
### Data/Collections/BidirectionalDictionary.cs
|
||||
|
||||
命名空间:`MAX.Common.Data.Collections`
|
||||
|
||||
```csharp
|
||||
public class BidirectionalDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, IReadOnlyCollection<KeyValuePair<TKey, TValue>>
|
||||
public TValue this[TKey key]
|
||||
public int Count
|
||||
public ICollection<TKey> Keys
|
||||
public ICollection<TValue> Values
|
||||
public bool IsReadOnly
|
||||
public bool IsFixedSize
|
||||
public bool IsSynchronized
|
||||
public object SyncRoot
|
||||
public BidirectionalDictionary()
|
||||
public BidirectionalDictionary(int capacity)
|
||||
public void Add(TKey key, TValue value)
|
||||
public bool Remove(TKey key)
|
||||
public bool RemoveByValue(TValue value)
|
||||
public void Clear()
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
public bool TryGetKey(TValue value, out TKey key)
|
||||
public TKey GetKey(TValue value)
|
||||
public TValue GetValue(TKey key)
|
||||
public bool ContainsKey(TKey key)
|
||||
public bool ContainsValue(TValue value)
|
||||
public void Add(KeyValuePair<TKey, TValue> item)
|
||||
public bool Contains(KeyValuePair<TKey, TValue> item)
|
||||
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
|
||||
public bool Remove(KeyValuePair<TKey, TValue> item)
|
||||
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
|
||||
```
|
||||
|
||||
### Data/Lists/CyclicReadOnlyList.cs
|
||||
|
||||
命名空间:`MAX.Common.Data.Lists`
|
||||
|
||||
```csharp
|
||||
public class CyclicReadOnlyList<T> : IReadOnlyList<T>
|
||||
public CyclicReadOnlyList(IReadOnlyList<T> source, int startIndex, int loopCount)
|
||||
public int Count
|
||||
public T this[int index]
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
```
|
||||
|
||||
### Data/Lists/SelectorReadonlyList.cs
|
||||
|
||||
命名空间:`MAX.Common.Data.Lists`
|
||||
|
||||
```csharp
|
||||
public class SelectorReadonlyList<TSource, TResult>(IReadOnlyList<TSource> source, Func<TSource, TResult> selector) : IReadOnlyList<TResult>
|
||||
public TResult this[int index]
|
||||
public int Count
|
||||
public IEnumerator<TResult> GetEnumerator()
|
||||
public static class SelectorReadonlyListExtensions
|
||||
public static IReadOnlyList<TResult> SelectAsReadonlyList<TSource, TResult>(this IReadOnlyList<TSource> source, Func<TSource, TResult> selector)
|
||||
```
|
||||
|
||||
### Data/Lists/ReversedReadonlyList.cs
|
||||
|
||||
命名空间:`MAX.Common.Data.Lists`
|
||||
|
||||
```csharp
|
||||
public class ReversedReadonlyList<T>(IReadOnlyList<T> source) : IReadOnlyList<T>
|
||||
public IReadOnlyList<T> Source
|
||||
public virtual T this[int index]
|
||||
public int Count
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
```
|
||||
|
||||
### Data/PrecisionBuilder.cs
|
||||
|
||||
命名空间:`MAX.Common.Data`
|
||||
|
||||
```csharp
|
||||
public class PrecisionBuilder
|
||||
public int PrecisionLength
|
||||
public bool UseScientificNotation
|
||||
public int ScientificNatationTriggerLength
|
||||
public bool RemoveTrailingZeros
|
||||
public PrecisionBuilder( int precisionLength, bool useScientificNotation, int scientificNatationTriggerLength, bool removeTrailingZeros )
|
||||
public PrecisionBuilder( int precisionLength, bool useScientificNotation, int scientificNatationTriggerLength, bool removeTrailingZeros, string scientificNotationString, string autoString )
|
||||
public string GetResultString(double d)
|
||||
```
|
||||
|
||||
### Models/EnumItem.cs
|
||||
|
||||
命名空间:`MAX.Common.Models`
|
||||
|
||||
```csharp
|
||||
public readonly record struct EnumItem( string Name, object Value, string? Description, string? ToolTip )
|
||||
public static IEnumerable<EnumItem> GetEnumItems<T>() where T : Enum
|
||||
public static IEnumerable<EnumItem> GetEnumItems(Type type)
|
||||
public static EnumItem GetEnumItem(Enum @enum)
|
||||
```
|
||||
|
||||
## 文件、数值与校验
|
||||
|
||||
### Helpers/FileHelper.Size.cs
|
||||
|
||||
命名空间:`MAX.Common.Helpers`
|
||||
|
||||
```csharp
|
||||
public static partial class FileHelper
|
||||
public static string FileSizeToKB(this long size, string format = "#,##0", string prefix = "", string suffix = " KB")
|
||||
public static string FileSizeToMB(this long size, string format = "#,##0", string prefix = "", string suffix = " MB")
|
||||
public static string FileSizeToGB(this long size, string format = "#,##0", string prefix = "", string suffix = " GB")
|
||||
public static string FileSizeToTB(this long size, string format = "#,##0", string prefix = "", string suffix = " TB")
|
||||
public static string FileSizeToPB(this long size, string format = "#,##0", string prefix = "", string suffix = " PB")
|
||||
public static string FileSizeToString(this long size, string format = "#,##0.##", string prefix = "")
|
||||
```
|
||||
|
||||
### Helpers/FileHelper.cs
|
||||
|
||||
命名空间:`MAX.Common.Helpers`
|
||||
|
||||
```csharp
|
||||
public static partial class FileHelper
|
||||
public static string GetUniqueFileName(string fullPath)
|
||||
public static string FixInvalidFullFilePath(string fullPath, string replacement = " ")
|
||||
public static bool IsFileNameInvalid(this string name)
|
||||
public static string FixInvalidFileName(string fileName, string replacement = " ")
|
||||
public static bool FileNameExists(this string fileName, IEnumerable<string> fileNames)
|
||||
public static Exception? ShowInExplorer(this string? filePath)
|
||||
public static Exception? OpenPathInSystemDefault(this string? filePath)
|
||||
public static async Task<string> CalculateFileMD5Async(string filePath, int bufferSize = 4096)
|
||||
public static string CalculateFileMD5(string filePath, int bufferSize = 4096)
|
||||
public static string? CalculateFileMD5(byte[]? bytes)
|
||||
public static async Task<string?> CalculateFileMD5Async(byte[]? bytes)
|
||||
public static List<string> ReadAllLines(string filePath, Encoding? encoding = null)
|
||||
public static FileInfo? SafeGetFileInfo(this string? filePath, Action<Exception>? handleException = null)
|
||||
public static bool AreFilePathsEqual(this string? path1, string? path2)
|
||||
public static bool? AreFilePathsEqualSafe(this string? path1, string? path2)
|
||||
public static string NormalizePath(this string? path)
|
||||
public static string NormalizePathSafe(this string? path)
|
||||
```
|
||||
|
||||
### Helpers/DirectoryHelper.cs
|
||||
|
||||
命名空间:`MAX.Common.Helpers`
|
||||
|
||||
```csharp
|
||||
public static class DirectoryHelper
|
||||
public static bool IsRootDirectory(string path)
|
||||
public static bool IsWindowsRootDirectory(string path)
|
||||
public static bool IsUnixRootDirectory(string path)
|
||||
public static DirectoryInfo CreateDirectory(string? dir, bool checkFileParent = false)
|
||||
public static Exception? TryCreateDirectory(string? dir, bool checkFileParent = false)
|
||||
public static bool HasWriteAccessToFolder(string folderPath)
|
||||
public static void CopyDirectory( string sourceDir, string destinationDir, bool recursive = true, Predicate<FileInfo>? handleFile = null )
|
||||
public static string GetUniqueFolderName(string folderPath, bool reuseEmptyDirectory = false)
|
||||
public static bool IsDirectoryEmpty(this string folderPath)
|
||||
public static bool IsDirectoryEmptyOrNotExist(this string folderPath)
|
||||
public static string AppendDirectorySeparatorChar(string path)
|
||||
public static string GetRelativePath(string relativeTo, string path)
|
||||
```
|
||||
|
||||
### Helpers/NumberHelper.Clamp.cs
|
||||
|
||||
命名空间:`MAX.Common.Helpers`
|
||||
|
||||
```csharp
|
||||
public static partial class NumberHelper
|
||||
public static T ClampGeneric<T>(this T value, T min, T max) where T : IComparable<T>
|
||||
public static float Clamp(this float input, float min, float max)
|
||||
public static double Clamp(this double input, double min, double max)
|
||||
public static decimal Clamp(this decimal input, decimal min, decimal max)
|
||||
public static int Clamp(this int input, int min, int max)
|
||||
public static long Clamp(this long input, long min, long max)
|
||||
public static short Clamp(this short input, short min, short max)
|
||||
public static sbyte Clamp(this sbyte input, sbyte min, sbyte max)
|
||||
public static uint Clamp(this uint input, uint min, uint max)
|
||||
public static ulong Clamp(this ulong input, ulong min, ulong max)
|
||||
public static ushort Clamp(this ushort input, ushort min, ushort max)
|
||||
public static byte Clamp(this byte input, byte min, byte max)
|
||||
```
|
||||
|
||||
### Utilities/PathValidator.cs
|
||||
|
||||
命名空间:`MAX.Common.Utilities`
|
||||
|
||||
```csharp
|
||||
public static partial class PathValidator
|
||||
public static bool IsFolderPathValidAndSafe([NotNullWhen(true)] string? path, bool allowRelativePath)
|
||||
```
|
||||
|
||||
### Utilities/FolderNameValidator.cs
|
||||
|
||||
命名空间:`MAX.Common.Utilities`
|
||||
|
||||
```csharp
|
||||
public static class FolderNameValidator
|
||||
public enum ValidationResult
|
||||
public static IReadOnlyCollection<string> ReservedNamesInWindows
|
||||
public static bool IsValidFolderName([NotNullWhen(true)] string? folderName)
|
||||
public static ValidationResult Validate(string? folderName)
|
||||
public static string GetErrorMessage(ValidationResult result)
|
||||
```
|
||||
|
||||
## 事件与运行控制
|
||||
|
||||
### Events/TypedEvent.cs
|
||||
|
||||
命名空间:`MAX.Common.Events`
|
||||
|
||||
```csharp
|
||||
public class TypedEvent<TSender, TArgs>
|
||||
public event TypedEventHandler<TSender, TArgs>? Event
|
||||
public void Raise(TSender sender, TArgs args)
|
||||
public class TypedEvent<TArgs>
|
||||
public event Action<TArgs>? Event
|
||||
public void Raise(TArgs args)
|
||||
public class TypedEvent
|
||||
public event Action? Event
|
||||
public void Raise()
|
||||
```
|
||||
|
||||
### Events/AsyncEvent.cs
|
||||
|
||||
命名空间:`MAX.Common.Events`
|
||||
|
||||
```csharp
|
||||
public class AsyncEvent<TEventArgs> where TEventArgs : EventArgs
|
||||
public static AsyncEvent<TEventArgs> operator +(AsyncEvent<TEventArgs>? e, Func<object, TEventArgs, Task>? callback)
|
||||
public static AsyncEvent<TEventArgs>? operator -(AsyncEvent<TEventArgs>? e, Func<object, TEventArgs, Task> callback)
|
||||
public async Task InvokeAsync(object sender, TEventArgs eventArgs)
|
||||
```
|
||||
|
||||
### Utilities/RunLimiter.cs
|
||||
|
||||
命名空间:`MAX.Common.Utilities`
|
||||
|
||||
```csharp
|
||||
public class RunLimiter(Action action, int maxLimit = 1)
|
||||
public bool Run()
|
||||
public bool CanRun()
|
||||
```
|
||||
|
||||
### Utilities/DisposeAction.cs
|
||||
|
||||
命名空间:`MAX.Common.Utilities`
|
||||
|
||||
```csharp
|
||||
public class DisposeAction(Action action) : IDisposable
|
||||
public Action Action
|
||||
public void Dispose()
|
||||
```
|
||||
|
||||
### Utilities/CompensatingTimer.cs
|
||||
|
||||
命名空间:`MAX.Common.Utilities`
|
||||
|
||||
```csharp
|
||||
public class CompensatingTimer : IDisposable
|
||||
public event Action<Exception>? OnTickException
|
||||
public CompensatingTimer(TimeSpan interval, Action tickCallback)
|
||||
public void Start()
|
||||
public void Stop()
|
||||
public void Dispose()
|
||||
```
|
||||
|
||||
### Helpers/SerializationHelper.cs
|
||||
|
||||
命名空间:`MAX.Common.Helpers`
|
||||
|
||||
```csharp
|
||||
public static class SerializationHelper
|
||||
public static T? TryGetValue<T>(this SerializationInfo info, string name, T? defaultWhenException = default)
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# 功能索引
|
||||
|
||||
参考版本:MAX.Common 1.4.17。以下命名空间均以 `MAX.Common` 为根。按任务读取对应 API 声明,避免把相似名称当作相同语义。
|
||||
|
||||
| 需求 | 命名空间 / 入口 | 选择提示 |
|
||||
| --- | --- | --- |
|
||||
| 同步、异步 ICommand | Commands:MethodCommand、AsyncMethodCommand,均有泛型版本 | 不依赖 WPF 控件;泛型参数有可空约束 |
|
||||
| 属性变化通知 | Models:BindModelBase、BindableBaseEx、BindObject | 依调用方需要选字段模型或属性包,不强行更换现有 MVVM 框架 |
|
||||
| 枚举描述与展示元数据 | Models.EnumItem、Attributes.EnumDisplayAttribute、TooltipAttribute;Helpers.EnumHelper;Utilities.EnumDisplayManagerBase | 先看元数据读取方式;WPF 显示由 WPF 包处理 |
|
||||
| 双向映射、列表字典 | Data.Collections:BidirectionalDictionary、ListDictionary | 双向字典提供 TryGetKey / TryGetValue;重复值等规则核对当前实现 |
|
||||
| 循环、反转、投影与区间读取 | Data.Lists:CyclicReadOnlyList、ReversedReadonlyList、SelectorReadonlyList、RangeDataReadList、ExtendReadonlyList、ObjectReadonlyList | 只读接口不等于独立不可变副本,检查是否持有源集合与索引边界 |
|
||||
| 二维数据与表格 | Data.Arrays.ReadonlyMatrix、Data.Tables.ListTable;Interfaces.IReadonlyMatrix、IDataTable;Helpers.CollectionHelper 的 Matrix 部分 | 确认行列顺序和维度,避免与 UI DataGrid 混淆 |
|
||||
| 数值与精度 | Helpers.NumberHelper;Data.PrecisionBuilder;Models.Range、Helpers.RangeHelper | 检查上下限、转换和舍入行为,勿猜测精度单位 |
|
||||
| 二维向量 | Data:Vector2、Vector2F、Vector2Int、Vector2Mutable;Interfaces.IVector2 | 按数值类型与可变性选择;WPF Point 转换在 WPF 包 |
|
||||
| 集合操作 | Helpers.CollectionHelper 的 Array / Collection / Enumerable / List 部分、DictionaryHelper;Models.ObservableHashSet | 按具体签名区分原地修改和返回新集合 |
|
||||
| 文件、目录、大小显示 | Helpers.FileHelper、DirectoryHelper;Comparers.FilePathStringComparer | `long.FileSizeToString()` 自动显示大小;删除、复制等操作先确认覆盖和异常语义 |
|
||||
| 路径、文件夹名校验 | Utilities.PathValidator、FolderNameValidator | 文件夹名单段校验和完整路径校验分开使用;不提供目录访问授权 |
|
||||
| 字符串、日期、随机数、GUID | Helpers.StringHelper、DateTimeHelper、RandomHelper、GuidHelper | 沿用现有文化、格式和业务随机性要求 |
|
||||
| 类型、反射、属性与赋值 | Helpers.TypeHelper、ReflectionHelper、AttributeHelper、StructHelper;Utilities.AssignUtility;Attributes.AssignableAttribute;Interfaces.IAssignable | 先查反射范围及标记规则,不把赋值当作任意对象深克隆 |
|
||||
| 强类型与异步事件 | Events.TypedEvent、AsyncEvent;Events.Args;根命名空间 TypedEventHandler | 区分同步通知和串行 Task 回调,处理订阅生命周期 |
|
||||
| 次数限制、清理与定时 | Utilities.RunLimiter、DisposeAction、CompensatingTimer | RunLimiter 是总次数;DisposeAction 可用于 using 作用域清理;定时器按实际 API 管理生命周期 |
|
||||
| 编码与摘要 | Cryptography.Base64Helper、UriHelper、MD5Helper | 用于已有编码和摘要需求;不将编码当作加密 |
|
||||
| 进程与运行环境 | Helpers.ProcessHelper、ArchitectureChecker、GCHelper | netstandard 目标不保证具体系统 API 跨平台可用 |
|
||||
| 版本与颜色数据 | Models.VersionStruct、SimpleColor | WPF Color / Brush 转换需 WPF 包 |
|
||||
|
||||
其他支撑类型可在安装包 XML 文档或源码按上述命名空间搜索。这里是能力路由,不承诺未列明的签名与行为。
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: reference-enum-display
|
||||
description: EnumDisplayService 内部实现和自定义枚举显示逻辑完整指南。当用户需要扩展枚举显示、添加自定义枚举翻译、在 EnumDisplayService 中注册枚举显示处理器、或了解 EnumDisplayManager 机制时自动加载。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# EnumDisplayService 完整参考
|
||||
|
||||
## 服务初始化
|
||||
|
||||
`EnumDisplayService` 在应用启动时自动初始化(通过 `IAppInitialize`),并将 `EnumDisplayManager` 注入到 `EnumDisplayConverter` 中。
|
||||
|
||||
```csharp
|
||||
// Framework/Services/EnumDisplayService.cs
|
||||
internal class EnumDisplayService : IEnumDisplayService, IAppInitialize
|
||||
{
|
||||
public static EnumDisplayManagerBase EnumDisplayManager { get; } = new _EnumDisplayManager();
|
||||
|
||||
void IAppInitialize.AppInitialize(IStatusReport statusReport)
|
||||
{
|
||||
// 将 EnumDisplayManager 注入到通用转换器中
|
||||
EnumDisplayConverter.EnumDisplayManager = EnumDisplayManager;
|
||||
}
|
||||
|
||||
private class _EnumDisplayManager() : EnumDisplayManagerWPF(typeof(AppStrings))
|
||||
{
|
||||
protected override Dictionary<Type, IEnumDisplayHandler> GetDefaultHandlers()
|
||||
{
|
||||
Dictionary<Type, IEnumDisplayHandler> dict = base.GetDefaultHandlers();
|
||||
|
||||
// 为特定枚举类型添加自定义显示逻辑
|
||||
dict[typeof(MyStatus)] = L<MyStatus>(e => e switch
|
||||
{
|
||||
MyStatus.Active => "活动",
|
||||
MyStatus.Inactive => "非活动",
|
||||
_ => e.ToString()
|
||||
});
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展枚举显示服务
|
||||
|
||||
有两种方式为自定义枚举添加显示逻辑。
|
||||
|
||||
### 方式 1:在 EnumDisplayService 中扩展(推荐)
|
||||
|
||||
在 `_EnumDisplayManager` 类中添加自定义处理器:
|
||||
|
||||
```csharp
|
||||
private class _EnumDisplayManager() : EnumDisplayManagerWPF(typeof(AppStrings))
|
||||
{
|
||||
protected override Dictionary<Type, IEnumDisplayHandler> GetDefaultHandlers()
|
||||
{
|
||||
Dictionary<Type, IEnumDisplayHandler> dict = base.GetDefaultHandlers();
|
||||
|
||||
// 添加枚举显示处理器
|
||||
dict[typeof(DeviceState)] = L<DeviceState>(e => e switch
|
||||
{
|
||||
DeviceState.Online => "在线",
|
||||
DeviceState.Offline => "离线",
|
||||
DeviceState.Connecting => "连接中",
|
||||
_ => e.ToString()
|
||||
});
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方式 2:使用本地化字符串资源
|
||||
|
||||
如果枚举显示文本已定义在 `AppStrings` 中,`EnumDisplayManagerWPF` 会自动处理(按 `{EnumType}_{Value}` 命名约定映射)。
|
||||
|
||||
```csharp
|
||||
// AppStrings.cs
|
||||
public class AppStrings
|
||||
{
|
||||
public static string MyStatus_Active => "活动";
|
||||
public static string MyStatus_Inactive => "非活动";
|
||||
}
|
||||
|
||||
// 枚举值 MyStatus.Active 会自动映射到 AppStrings.MyStatus_Active
|
||||
```
|
||||
|
||||
## C# 中使用
|
||||
|
||||
```csharp
|
||||
// 获取枚举的显示文本
|
||||
string displayText = MyStatus.Active.GetEnumDisplay(); // "活动"
|
||||
|
||||
// 在 ViewModel 中使用
|
||||
public string StatusText => Status.GetEnumDisplay();
|
||||
```
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: reference-fast-wpf-grid
|
||||
description: >-
|
||||
MAX.FastWpfGrid 高性能表格完整使用指南(FastGridModelBase 模式、XAML 绑定、Excel 导出)。
|
||||
当用户处理百万行级别大数据表格、使用 MAX.FastWpfGrid、FastWpfGrid、FastGridControl、
|
||||
FastGridModelBase 或需要表格导出时自动加载。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# MAX.FastWpfGrid 完整参考
|
||||
|
||||
## 概述
|
||||
|
||||
MAX.FastWpfGrid 是基于 **WriteableBitmap** 渲染的高性能 WPF 表格控件,不依赖 WPF 视觉树,所有内容绘制到位图上,实现百万行数据的流畅滚动。
|
||||
|
||||
- **NuGet 包**:`MAX.FastWpfGrid`(**禁止**旧包名 `FastWpfGrid`)
|
||||
- **命名空间 / 程序集**:`MAX.FastWpfGrid`(旧命名空间 `FastWpfGrid` 已废弃)
|
||||
- **本地源码**:`{LIBS_ROOT}MAX.FastWpfGrid\MAX.FastWpfGrid\`(路径变量见 `max-libraries-reference.md`)
|
||||
- **核心类型**:
|
||||
- `FastGridControl`:WPF 控件,通过 `Model` 属性绑定数据模型
|
||||
- `FastGridModelBase`:数据模型基类,继承后覆写 `GetRowHeaderText`、`GetColumnHeaderText`、`GetCellText` 方法提供数据
|
||||
- `FastGridCellImpl`:富文本单元格(图文混排、背景色),在 `GetCell()` 中使用
|
||||
- **建模模式**:通常封装一个 `TableModelBase` 抽象基类(继承 `FastGridModelBase`),统一处理行列头渲染和右键菜单分发
|
||||
|
||||
## 模型模式
|
||||
|
||||
继承 `FastGridModelBase`,覆写三个核心方法:
|
||||
|
||||
```csharp
|
||||
using MAX.FastWpfGrid;
|
||||
|
||||
public class XxxTableModel : FastGridModelBase
|
||||
{
|
||||
private readonly IReadOnlyList<XxxData> _data;
|
||||
|
||||
public XxxTableModel(IReadOnlyList<XxxData> data)
|
||||
: base(columnCount, data.Count)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public override string GetRowHeaderText(int row)
|
||||
=> (row + 1).ToString();
|
||||
|
||||
public override string GetColumnHeaderText(int column)
|
||||
=> column switch
|
||||
{
|
||||
0 => "时间戳",
|
||||
1 => "通道A",
|
||||
2 => "通道B",
|
||||
_ => $"列{column}",
|
||||
};
|
||||
|
||||
public override string GetCellText(int row, int column)
|
||||
=> column switch
|
||||
{
|
||||
0 => _data[row].Timestamp.ToString(),
|
||||
1 => _data[row].ChannelA.ToString("0.####"),
|
||||
2 => _data[row].ChannelB.ToString("0.####"),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
需要颜色、图文混排的单元格,覆写 `GetCell()` 返回 `FastGridCellImpl` 实例。
|
||||
|
||||
## XAML 绑定
|
||||
|
||||
```xml
|
||||
xmlns:fwg="clr-namespace:MAX.FastWpfGrid;assembly=MAX.FastWpfGrid"
|
||||
|
||||
<fwg:FastGridControl
|
||||
Model="{Binding TableModel}"
|
||||
IsReadOnly="True"
|
||||
HeaderHeight="18"
|
||||
BackgroundColor="#fafafa"
|
||||
CellFontSize="12"/>
|
||||
```
|
||||
|
||||
## 导出 Excel(MiniExcel)
|
||||
|
||||
将 `FastGridModelBase` 子类包装为 `DbDataReader`,配合 **MiniExcel** 实现 XLSX 导出:
|
||||
|
||||
```csharp
|
||||
// 将 TableModel 适配为 DbDataReader 供 MiniExcel 流式写入
|
||||
private sealed class TableModelDataReader(TableModelBase model) : DbDataReader
|
||||
{
|
||||
private int _currentRow = -1;
|
||||
|
||||
public override int FieldCount => model.ColumnCount;
|
||||
public override bool HasRows => model.RowCount > 0;
|
||||
|
||||
public override bool Read()
|
||||
{
|
||||
_currentRow++;
|
||||
return _currentRow < model.RowCount;
|
||||
}
|
||||
|
||||
public override string GetName(int ordinal) => model.GetColumnHeaderText(ordinal) ?? string.Empty;
|
||||
public override object GetValue(int ordinal) => model.GetCellText(_currentRow, ordinal) ?? string.Empty;
|
||||
public override string GetString(int ordinal) => model.GetCellText(_currentRow, ordinal) ?? string.Empty;
|
||||
|
||||
// 其余 DbDataReader 抽象成员使用默认实现
|
||||
public override int RecordsAffected => -1;
|
||||
public override bool IsClosed => false;
|
||||
public override int Depth => 0;
|
||||
// ...
|
||||
}
|
||||
|
||||
// 导出 Excel
|
||||
using FileStream fileStream = new(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
using TableModelDataReader reader = new(tableModel);
|
||||
MiniExcel.SaveAs(fileStream, reader, excelType: ExcelType.XLSX);
|
||||
```
|
||||
|
||||
纯文本导出则直接循环调用 `GetCellText()` 写入 CSV/TSV 即可。
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: reference-loading-status
|
||||
description: LoadingStatus 完整 API 参考和使用指南。当用户管理异步加载状态、显示加载进度、处理加载错误、绑定 LoadingCover 或 LoadingStatusIndicator 控件时自动加载。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# LoadingStatus 完整参考
|
||||
|
||||
`LoadingStatus` 位于 `PipeReviewReport.Framework.ViewModels` 命名空间。
|
||||
|
||||
## API 概览
|
||||
|
||||
```csharp
|
||||
namespace PipeReviewReport.Framework.ViewModels;
|
||||
|
||||
public class LoadingStatus : BindableBase
|
||||
{
|
||||
// 核心属性
|
||||
public double? Progress { get; set; } // 进度(null = 不确定)
|
||||
public string Message { get; set; } // 加载提示文字
|
||||
public string ErrorMessage { get; set; } // 错误信息
|
||||
public bool HasError { get; set; } // 是否发生错误
|
||||
public bool IsLoading { get; set; } // 是否正在加载
|
||||
|
||||
// 计算属性
|
||||
public bool IsLoadedSuccess => !IsLoading && !HasError;
|
||||
public bool IsLoadingActive => IsLoading || HasError;
|
||||
|
||||
// 状态切换
|
||||
public void Initialize(string message = "") // 开始加载,IsLoading = true
|
||||
public void Done(string message = "") // 加载完成,IsLoading = false
|
||||
public void Error(string errorMessage) // 发生错误,HasError = true
|
||||
public void SetProgress(double progress) // 更新进度
|
||||
public void Reset() // 重置到初始状态
|
||||
public void CopyErrorMessage() // 复制错误信息到剪贴板
|
||||
}
|
||||
```
|
||||
|
||||
## ViewModel 使用模式
|
||||
|
||||
```csharp
|
||||
internal class XxxViewModel : ViewModelBaseEx
|
||||
{
|
||||
public LoadingStatus LoadingStatus { get; } = new();
|
||||
|
||||
public IDelegateCommand LoadCommand => field ??= new AsyncCommand(Load);
|
||||
private async Task Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadingStatus.Initialize("读取文件...");
|
||||
await DoStuff();
|
||||
LoadingStatus.Done();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LoadingStatus.Reset();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
LoadingStatus.Error(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## View 绑定 — LoadingCover
|
||||
|
||||
在内容 Grid 之上放置 `LoadingCover` 作为覆盖层,自动处理三态显示:
|
||||
|
||||
```xml
|
||||
<Grid>
|
||||
<!-- 实际内容 -->
|
||||
<ContentControl Content="{Binding ...}"/>
|
||||
|
||||
<!-- 加载覆盖层 -->
|
||||
<f:LoadingCover
|
||||
LoadingStatus="{Binding LoadingStatus}"
|
||||
RefreshCommand="{Binding LoadCommand}"/>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
`LoadingCover` 三态:
|
||||
- **加载中**:半透明遮罩 + 环形进度 + `Message` 文字
|
||||
- **错误**:半透明遮罩 + 警告图标 + `ErrorMessage` + 刷新按钮(绑定 `RefreshCommand`)+ 复制错误按钮
|
||||
- **成功**:完全隐藏
|
||||
|
||||
## View 绑定 — LoadingStatusIndicator
|
||||
|
||||
轻量级内联状态指示:
|
||||
|
||||
```xml
|
||||
<f:LoadingStatusIndicator LoadingStatus="{Binding LoadingStatus}"/>
|
||||
```
|
||||
@@ -0,0 +1,350 @@
|
||||
---
|
||||
name: reference-timer
|
||||
description: >-
|
||||
MAX 内部定时器工具类完整 API 参考和使用指南。当用户编写定时任务、周期轮询、延时执行、
|
||||
使用 CompensatingTimer/DispatcherTimerController/SystemTimerController/ThreadingTimerController
|
||||
时自动加载。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# MAX 定时器工具类参考
|
||||
|
||||
项目提供了 4 个定时器工具类,分布在 `MAX.Common` 和 `MAX.Common.WPF` 中,封装了不同底层定时器并统一了反重入和异常处理。
|
||||
|
||||
---
|
||||
|
||||
## 选择决策树
|
||||
|
||||
```
|
||||
需要定时触发某操作?
|
||||
├── 需要在 UI 线程执行(操作 WPF 控件/绑定属性)
|
||||
│ └── DispatcherTimerController ✅
|
||||
│
|
||||
├── 需要自校准(消除累积漂移,如精确的周期采样)
|
||||
│ └── CompensatingTimer ✅
|
||||
│
|
||||
├── 需要首次触发延迟(due time)+ 固定周期
|
||||
│ └── ThreadingTimerController ✅
|
||||
│
|
||||
├── 简单的线程池周期执行,不需要特殊功能
|
||||
│ ├── SystemTimerController(有 Start/Stop,需手动控制)
|
||||
│ └── ThreadingTimerController(构造即启动,dispose 即停)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. CompensatingTimer — 自校准定时器
|
||||
|
||||
**命名空间:** `MAX.Common.Utilities`(位于 `MAX.Common` 库)
|
||||
**底层:** `System.Timers.Timer`(`AutoReset = false`,手动调度)
|
||||
**执行线程:** 线程池
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **自校准**:每次 tick 基于"预期执行时间"累加 interval,而非"实际经过时间"。这消除了累积漂移——即使单次回调执行时间有波动,长期也精确保持 interval 间隔。
|
||||
- **跳过错过的时间槽**:如果回调执行太久,错过了下一次触发时间,它会快进到未来的下一个时间槽,丢弃已错过的 tick,防止连续触发导致 CPU 满载。
|
||||
- **无异步支持**:回调是 `Action`(同步),不支持 `Func<Task>`。
|
||||
- **异常通过 `OnTickException` 事件外抛**(不会被吞)。
|
||||
|
||||
### 构造函数
|
||||
|
||||
```csharp
|
||||
public CompensatingTimer(TimeSpan interval, Action tickCallback)
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `interval` | 触发间隔,必须 > 0ms |
|
||||
| `tickCallback` | 每次触发时执行的委托 |
|
||||
|
||||
### API
|
||||
|
||||
```csharp
|
||||
public class CompensatingTimer : IDisposable
|
||||
{
|
||||
public event Action<Exception>? OnTickException;
|
||||
|
||||
public CompensatingTimer(TimeSpan interval, Action tickCallback);
|
||||
|
||||
public void Start(); // 启动(已启动则忽略)
|
||||
public void Stop(); // 停止
|
||||
public void Dispose(); // 停止并释放底层 Timer
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```csharp
|
||||
using MAX.Common.Utilities;
|
||||
|
||||
// 每 100ms 采集一次数据,精确间隔,无累积漂移
|
||||
CompensatingTimer timer = new(TimeSpan.FromMilliseconds(100), () =>
|
||||
{
|
||||
double value = sensor.Read();
|
||||
buffer.Add(value);
|
||||
});
|
||||
timer.OnTickException += ex => Debug.WriteLine(ex);
|
||||
timer.Start();
|
||||
|
||||
// 停止时
|
||||
timer.Dispose();
|
||||
```
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 数据采集(需要精确采样间隔)
|
||||
- 心跳检测
|
||||
- 需要长时间稳定周期、不随回调执行时间偏移的定时任务
|
||||
|
||||
---
|
||||
|
||||
## 2. DispatcherTimerController — UI 线程定时器
|
||||
|
||||
**命名空间:** `MAX.Common.WPF.Utilities.Timers`(位于 `MAX.Common.WPF` 库)
|
||||
**底层:** WPF `DispatcherTimer`
|
||||
**执行线程:** **UI 线程**(可直接操作 WPF 控件和绑定属性)
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **UI 线程执行**,可直接更新绑定的 MVVM 属性。
|
||||
- 支持 **`Action`(同步)** 和 **`Func<Task>`(异步)** 两种回调。
|
||||
- **防重入**:如果上一次回调仍在执行,新的 tick 会被跳过(`isTickRunning` 门控)。
|
||||
- 可通过 `DispatcherPriority` 控制调度优先级。
|
||||
- 异常通过 `ExceptionOccurred` 事件外抛。
|
||||
|
||||
### 构造函数
|
||||
|
||||
```csharp
|
||||
// 同步回调
|
||||
public DispatcherTimerController(DispatcherPriority dispatcherPriority, TimeSpan interval, Action action, Dispatcher dispatcher = null);
|
||||
|
||||
// 异步回调
|
||||
public DispatcherTimerController(DispatcherPriority dispatcherPriority, TimeSpan interval, Func<Task> actionAsync, Dispatcher dispatcher = null);
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `dispatcherPriority` | 调度优先级,常用 `DispatcherPriority.Background` 或 `DispatcherPriority.Normal` |
|
||||
| `interval` | 触发间隔 |
|
||||
| `action` / `actionAsync` | 回调委托(同步或异步) |
|
||||
| `dispatcher` | 可选,指定 Dispatcher;默认当前 UI 线程 |
|
||||
|
||||
### API
|
||||
|
||||
```csharp
|
||||
public class DispatcherTimerController : TimerControllerBase
|
||||
{
|
||||
public event TypedEventHandler<DispatcherTimerController, Exception> ExceptionOccurred;
|
||||
|
||||
public virtual void Start(); // 启动
|
||||
public virtual void Stop(); // 停止
|
||||
public override void Dispose(); // 停止、取消订阅、释放
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```csharp
|
||||
using MAX.Common.WPF.Utilities.Timers;
|
||||
using System.Windows.Threading;
|
||||
|
||||
internal class DashboardViewModel : ViewModelBaseEx
|
||||
{
|
||||
private DispatcherTimerController? _refreshTimer;
|
||||
|
||||
protected override void LoadedOnce()
|
||||
{
|
||||
// 每 5 秒异步刷新仪表盘数据
|
||||
_refreshTimer = new DispatcherTimerController(
|
||||
DispatcherPriority.Background,
|
||||
TimeSpan.FromSeconds(5),
|
||||
RefreshDashboardAsync
|
||||
);
|
||||
_refreshTimer.ExceptionOccurred += (s, ex) => Debug.WriteLine(ex);
|
||||
_refreshTimer.Start();
|
||||
}
|
||||
|
||||
private async Task RefreshDashboardAsync()
|
||||
{
|
||||
LatestData = await dataService.FetchLatestAsync();
|
||||
}
|
||||
|
||||
protected override void Unloaded()
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 适用场景
|
||||
|
||||
- UI 定时刷新(仪表盘、状态栏、时钟)
|
||||
- 轮询后更新绑定属性
|
||||
- 任何需要在 UI 线程执行的周期操作
|
||||
|
||||
---
|
||||
|
||||
## 3. SystemTimerController — System.Timers 定时器
|
||||
|
||||
**命名空间:** `MAX.Common.WPF.Utilities.Timers`(位于 `MAX.Common.WPF` 库)
|
||||
**底层:** `System.Timers.Timer`(`AutoReset = true`,自动重复触发)
|
||||
**执行线程:** 线程池
|
||||
|
||||
### 核心特性
|
||||
|
||||
- `AutoReset = true`,自动按 interval 重复触发。
|
||||
- 支持 **`Action`(同步)** 和 **`Func<Task>`(异步)** 两种回调。
|
||||
- **防重入**:如果上一次回调仍在执行,新的 tick 会被跳过。
|
||||
- 有 `Start()` / `Stop()` 控制生命周期。
|
||||
- 异常通过 `ExceptionOccurred` 事件外抛。
|
||||
|
||||
### 构造函数
|
||||
|
||||
```csharp
|
||||
// 同步回调
|
||||
public SystemTimerController(TimeSpan interval, Action action);
|
||||
|
||||
// 异步回调
|
||||
public SystemTimerController(TimeSpan interval, Func<Task> actionAsync);
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
```csharp
|
||||
public class SystemTimerController : TimerControllerBase
|
||||
{
|
||||
public event TypedEventHandler<SystemTimerController, Exception> ExceptionOccurred;
|
||||
|
||||
public void Start(); // 启动
|
||||
public void Stop(); // 停止
|
||||
public override void Dispose(); // 停止并释放底层 Timer
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```csharp
|
||||
using MAX.Common.WPF.Utilities.Timers;
|
||||
|
||||
// 每 30 秒在后台清理过期缓存
|
||||
SystemTimerController cleaner = new(TimeSpan.FromSeconds(30), () =>
|
||||
{
|
||||
cache.CleanExpired();
|
||||
});
|
||||
cleaner.ExceptionOccurred += (s, ex) => Debug.WriteLine(ex);
|
||||
cleaner.Start();
|
||||
|
||||
// 停止时
|
||||
cleaner.Dispose();
|
||||
```
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 后台定时清理/维护任务
|
||||
- 不需要 UI 线程的周期性工作
|
||||
- 需要 `Start`/`Stop` 显式控制(与 `ThreadingTimerController` 的区别)
|
||||
|
||||
---
|
||||
|
||||
## 4. ThreadingTimerController — System.Threading 定时器
|
||||
|
||||
**命名空间:** `MAX.Common.WPF.Utilities.Timers`(位于 `MAX.Common.WPF` 库)
|
||||
**底层:** `System.Threading.Timer`
|
||||
**执行线程:** 线程池
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 支持首次触发延迟(`due` 参数)和周期(`period` 参数)。
|
||||
- 支持 **`Action`(同步)** 和 **`Func<Task>`(异步)** 两种回调。
|
||||
- **防重入**:如果上一次回调仍在执行,新的 tick 会被跳过。
|
||||
- **无 `Start`/`Stop`**:构造函数创建后立即开始触发,`Dispose()` 时停止。
|
||||
- 异常通过 `ExceptionOccurred` 事件外抛。
|
||||
|
||||
### 构造函数
|
||||
|
||||
```csharp
|
||||
// 同步回调 — 无延迟
|
||||
public ThreadingTimerController(TimeSpan period, Action action);
|
||||
|
||||
// 同步回调 — 有延迟
|
||||
public ThreadingTimerController(TimeSpan period, TimeSpan due, Action action);
|
||||
|
||||
// 异步回调 — 无延迟
|
||||
public ThreadingTimerController(TimeSpan period, Func<Task> actionAsync);
|
||||
|
||||
// 异步回调 — 有延迟
|
||||
public ThreadingTimerController(TimeSpan period, TimeSpan due, Func<Task> actionAsync);
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `period` | 触发周期 |
|
||||
| `due` | 首次触发前的延迟(不传则 `TimeSpan.Zero`,立即执行首次) |
|
||||
| `action` / `actionAsync` | 回调委托 |
|
||||
|
||||
### API
|
||||
|
||||
```csharp
|
||||
public class ThreadingTimerController : TimerControllerBase
|
||||
{
|
||||
public event TypedEventHandler<ThreadingTimerController, Exception> ExceptionOccurred;
|
||||
|
||||
// 无 Start / Stop
|
||||
public override void Dispose(); // 释放底层 Timer
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```csharp
|
||||
using MAX.Common.WPF.Utilities.Timers;
|
||||
|
||||
// 延迟 10 秒后,每 1 分钟检查一次许可证状态
|
||||
ThreadingTimerController licenseChecker = new(
|
||||
period: TimeSpan.FromMinutes(1),
|
||||
due: TimeSpan.FromSeconds(10),
|
||||
actionAsync: CheckLicenseAsync
|
||||
);
|
||||
licenseChecker.ExceptionOccurred += (s, ex) => Debug.WriteLine(ex);
|
||||
|
||||
// 停止时(无需 Stop,直接 Dispose)
|
||||
licenseChecker.Dispose();
|
||||
```
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 需要"延迟启动"的周期性任务
|
||||
- 不需要显式 `Start`/`Stop`,生命周期跟随对象
|
||||
- 轻量级的 fire-and-forget 定时
|
||||
|
||||
---
|
||||
|
||||
## 5. 共同模式
|
||||
|
||||
### 防重入机制
|
||||
|
||||
`DispatcherTimerController`、`SystemTimerController`、`ThreadingTimerController` 都内置了 `isTickRunning` 门控:
|
||||
|
||||
- 每次 tick 进入时检查,如果上一次回调还未结束,**跳过本次 tick**。
|
||||
- 这防止了"回调执行慢于间隔"时积压多个并发执行。
|
||||
|
||||
### 异常处理
|
||||
|
||||
三个 `TimerControllerBase` 子类统一通过 `ExceptionOccurred` 事件外抛异常,同时会调用 `Debug.WriteLine(exception)` + `Debugger.Break()`。
|
||||
|
||||
`CompensatingTimer` 通过 `OnTickException` 事件外抛异常(仅 `Debug.WriteLine`,无 `Debugger.Break`)。
|
||||
|
||||
### Dispose 模式
|
||||
|
||||
所有四个类都实现 `IDisposable`。`Dispose()` 会自动停止定时器并释放底层资源。三个 `TimerControllerBase` 子类会额外调用 `GC.SuppressFinalize(this)`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 对照速查
|
||||
|
||||
| 类 | 底层 Timer | 执行线程 | 异步支持 | Start/Stop | 首次延迟 | 自校准 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `CompensatingTimer` | `System.Timers` | 线程池 | ❌ 仅 Action | ✅ | ❌ | ✅ |
|
||||
| `DispatcherTimerController` | WPF `DispatcherTimer` | **UI 线程** | ✅ | ✅ | ❌ | ❌ |
|
||||
| `SystemTimerController` | `System.Timers` | 线程池 | ✅ | ✅ | ❌ | ❌ |
|
||||
| `ThreadingTimerController` | `System.Threading` | 线程池 | ✅ | ❌ 构造即启 | ✅ `due` | ❌ |
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: review-rules
|
||||
description: >-
|
||||
根据项目的所有 coding rules 检查代码合规性,列出违规项并提供修复建议。
|
||||
当用户要求检查代码是否违反 rules、做代码合规审查、或写完代码后要求自查时使用。
|
||||
触发词:review-rules、规则检查、合规审查、检查 rules、违反规则。
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 代码规则合规审查
|
||||
|
||||
对照项目 `.agents/rules/` 中的全部规则文件,逐条检查用户提供的代码,输出违规清单和修复指导。
|
||||
|
||||
---
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1 — 加载规则
|
||||
|
||||
读取所有 `.agents/rules/` 下的 `.md` 规则文件。如果本次对话中这些规则已在上下文中,则直接引用,无需重新读取。
|
||||
|
||||
### Step 2 — 确认审查目标
|
||||
|
||||
用户通常以以下方式之一提供代码:
|
||||
|
||||
- 直接在消息中粘贴代码块
|
||||
- 引用文件路径(`@path/to/file.cs`)
|
||||
- 要求审查"刚才生成的代码"或"当前 diff"
|
||||
|
||||
确认目标后开始检查。如果目标不明确,追问用户。
|
||||
|
||||
### Step 3 — 逐规则检查
|
||||
|
||||
按文件逐一比对。将每个 `.md` 中的约束转化为可检查项,系统性地检查:
|
||||
|
||||
| 规则文件 | 核心检查维度 |
|
||||
|----------|-------------|
|
||||
| `csharp-syntax.md` | 大括号、`var` 禁用、null 模式匹配、字符串插值、`Debug.WriteLine(ex)`、集合表达式、`field` 关键字、namespace、JSON 库选择、await/async、接口注释 |
|
||||
| `wpf-xaml.md` | BindingProxy、`UpdateSourceTrigger`、`Mode=OneWay`、**输入框必须显式 MaxLength**、颜色 hex、布局注释、XAML 头部 |
|
||||
| `wpf-core.md` | MVVM 属性写法(`GetProperty`/`SetProperty`)、`AppFileHelper.PickFolder`、`LoadingStatus`、输入限长、路径片段 vs 完整路径 |
|
||||
| `icon-usage.md` | 禁止 Windows 内置字体图标、禁止 Unicode 魔术数、必须用 `Image` 而非 `TextBlock`、必须用已有枚举 |
|
||||
| `architecture.md` | 优先使用内部库、编译后校验、文件锁定处理 |
|
||||
| `editor-conventions.md` | 换行符/编码(仅新建文件触发) |
|
||||
|
||||
### Step 4 — 输出报告
|
||||
|
||||
使用以下格式输出:
|
||||
|
||||
```markdown
|
||||
## 规则合规检查报告
|
||||
|
||||
共发现 **N** 项违规。
|
||||
|
||||
| # | 严重度 | 规则文件 | 位置 | 问题 | 修复 |
|
||||
|---|--------|---------|------|------|------|
|
||||
| 1 | 🔴 高 | csharp-syntax | L45 | catch 中缺少 `Debug.WriteLine(ex)` | 在 `NLogger.Error` 之前加 `Debug.WriteLine(ex);` |
|
||||
| 2 | 🔴 高 | wpf-client | L55 | MVVM 属性使用了 `ref` 模式 | 改为 `GetProperty(()=>...)` / `SetProperty(()=>..., value)` |
|
||||
| 3 | 🟡 中 | csharp-syntax | L38 | 日志未使用字符串插值 | 改为 `$"保存失败: {ex.Message}"` |
|
||||
```
|
||||
|
||||
**严重度定义:**
|
||||
|
||||
- 🔴 **高**:编译不会报错但明显违反强制约束(如 `ref` 模式、缺少 `Debug.WriteLine(ex)`、使用被禁 API)
|
||||
- 🟡 **中**:风格违规,不影响功能但不符合项目规范(如 `var`、字符串插值、null 比较)
|
||||
- 🟢 **低**:代码坏味道、可优化项(如空 if 块、缺少注释、未使用集合表达式)
|
||||
|
||||
每个违规项都应包含**具体的修复代码**(before/after)。
|
||||
|
||||
### Step 5 — 询问是否修复
|
||||
|
||||
报告输出后,询问用户:
|
||||
|
||||
> 是否需要我自动修复以上违规项?
|
||||
|
||||
如果用户同意,逐项修复。修复过程中**严格遵守以下安全约束**:
|
||||
|
||||
#### 5a — 仅修改语法与格式,不得改变功能
|
||||
|
||||
绝大多数规则违规是纯粹的语法/格式问题(见下表),修复时**只改写法,不改行为**。这条是修复的核心铁律。
|
||||
|
||||
| 违规类型 | 纯格式? | 示例 |
|
||||
|---------|:--:|------|
|
||||
| `var` → 显式类型 | ✅ | 不改变运行时行为 |
|
||||
| `== null` → `is null` | ✅ | 语义等价 |
|
||||
| `string.Format(...)` → `$""` | ✅ | 语义等价 |
|
||||
| `SetProperty(ref _x, v)` → `SetProperty(() => X, v)` | ✅ | DevExpress 内部行为一致 |
|
||||
| `new List<T>()` → `[]` | ✅ | 编译结果可能不同但语义等价 |
|
||||
| 补充 `Debug.WriteLine(ex)` | ✅ | 仅增加副作用,不改控制流 |
|
||||
| `Task.FromResult(x)` → `await Task.CompletedTask; return x` | ⚠️ | 方法签名从同步包装变为真正 async,需确认调用方不受影响 |
|
||||
| `System.Text.Json` → `Newtonsoft.Json` | ⚠️ | 行为可能不同(默认值处理、命名策略等),需确认 |
|
||||
| 补 `Mode=OneWay` | ✅ | WPF 绑定行为不因 OneWay/TwoWay 而改变已有的数据流方向 |
|
||||
|
||||
#### 5b — 有功能变更风险时必须先问
|
||||
|
||||
当一个修复**不是纯格式替换**时(上述标记为 ⚠️ 的项目,或任何你拿不准的改动),**不要直接修改**。先向用户说明:
|
||||
|
||||
- 当前写法与目标写法的差异
|
||||
- 可能导致的功能变化(即使概率很低)
|
||||
- 提供 2~3 个方案供选择(如"保持现状"、"改为推荐写法并补充测试"、"改为带兼容代码的折中写法")
|
||||
|
||||
用户确认后再动手。
|
||||
|
||||
#### 5c — 修复后编译验证
|
||||
|
||||
所有修复完成后,如果改动涉及 C# 文件,按 `architecture.md` 的编译要求执行 `dotnet build` 验证。如遇到文件锁定按对应规则处理。
|
||||
|
||||
---
|
||||
|
||||
## 检查要点速查
|
||||
|
||||
### csharp-syntax.md 高频违规
|
||||
|
||||
- catch (Exception ex) 中有无 `Debug.WriteLine(ex)`(log/MessageBox 之前)
|
||||
- 是否使用了 `var`(必须显式类型)
|
||||
- null 检查是否用了 `is null`/`is not null`(禁用 `== null`)
|
||||
- 字符串拼接/日志是否用了 `$""` 插值
|
||||
- 是否使用了 `System.Text.Json`(应用 Newtonsoft.Json)
|
||||
- 是否用 `Task.FromResult`(应用 `await Task.CompletedTask`)
|
||||
- 集合初始化是否用了 `[]`(禁用 `new List<T>()`)
|
||||
- 是否在代码中写了全限定名
|
||||
- 接口成员是否有 `///` XML 文档注释
|
||||
- 是否省略了 if/else/for 的大括号
|
||||
- 文件级 namespace 是否正确
|
||||
|
||||
### wpf-core.md / wpf-xaml.md 高频违规
|
||||
|
||||
- MVVM 数据绑定属性是否用了 `GetProperty`/`SetProperty`(禁用 `ref` 和 `field`)
|
||||
- `field` 关键字是否仅用于 Command(未滥用)
|
||||
- 文件夹选择是否使用 `AppFileHelper.PickFolder`(禁用 WinForms 引用)
|
||||
- IsReadOnly 控件是否加了 `Mode=OneWay`
|
||||
- Run 绑定是否加了 `Mode=OneWay`
|
||||
- XAML 布局是否有分块注释
|
||||
- `d:DataContext` 和 `DataContext="{core:DISource...}"` 是否都存在
|
||||
- 异步操作是否使用 `LoadingStatus` 管理状态
|
||||
- **输入框是否显式写了 `MaxLength`**(见下方专项)
|
||||
|
||||
### 输入框 MaxLength(专项,🔴 高)
|
||||
|
||||
审查 XAML 时**必须**把下列标签全部找出来,缺 `MaxLength` 即违规:
|
||||
|
||||
- `TextBox`、`PasswordBox`
|
||||
- `hc:TextBox`、`hc:PasswordBox`、`hc:SearchBar`
|
||||
- DataGrid 文本列的 `EditingElementStyle` 里的编辑框
|
||||
|
||||
检查步骤:
|
||||
|
||||
1. 在目标 XAML 中搜索上述标签。
|
||||
2. 用户能打字的控件必须有 `MaxLength=`。没有 → 🔴 高,补上。
|
||||
3. 值必须是 `{x:Static shared:InputLimits.Path}` 或 `Text`。写成 `MaxLength="250"` 这类裸数字 → 🟡 中,改成常量。
|
||||
4. 按绑定属性名判断档位:
|
||||
- 文件名、文件夹名、项目名、会拼进路径的**名称** → `InputLimits.Path`(100)
|
||||
- 其余名称、描述、备注、密码、搜索关键字 → `InputLimits.Text`(250)
|
||||
5. **完整路径例外:** `IsReadOnly` 且绑定的是选择器选出的目录/文件路径 → 不要求 `MaxLength`(不要用数字去卡系统路径)。可编辑的完整路径输入框 → 🔴 高,改成选择器。
|
||||
6. `NumericUpDown` 跳过。日志 / JSON 大段文本不限长时,旁边必须有注释说明原因,否则仍算违规。
|
||||
|
||||
修复示例:
|
||||
|
||||
```xml
|
||||
<!-- ❌ -->
|
||||
<TextBox Text="{Binding ProjectName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<!-- ✅ -->
|
||||
<TextBox MaxLength="{x:Static shared:InputLimits.Path}"
|
||||
Text="{Binding ProjectName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
```
|
||||
|
||||
补 `MaxLength` 属于本规则要求的标准修复,审查发现后应直接改,不必再问是否改变输入行为。
|
||||
|
||||
### icon-usage.md 高频违规
|
||||
|
||||
- 是否用了 Segoe Fluent Icons / Segoe MDL2 Assets / Segoe UI Symbol 等 Windows 内置字体
|
||||
- 是否在代码或 XAML 中写了 `\uExxx` 或 `...;` 的 Unicode 硬编码图标
|
||||
- 图标是否用 `Image.Source` 绑定而非 `TextBlock.Text`
|
||||
- 图标枚举值是否来自 `IconNameEnum`(非自行编造)
|
||||
- 是否尝试通过 `Foreground` 修改图标颜色
|
||||
- `IconText` / `HorizontalIconText.IconSize` 的 `FontSize` 是否 < 16(禁止 12~14)
|
||||
- Button / RadioButton 内嵌 `IconText` 时是否过小或未设 `Padding="0"` 导致裁切
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
name: update-rules-skills
|
||||
description: >-
|
||||
更新项目 rules 和 skills 的专用入口。当用户要求修改 rules、更新 skills、添加新约束、
|
||||
强调已有规则、根据错误反例强化规范、或调整 rules/skills 的加载策略时使用。
|
||||
触发词:/update-rules-skills、修改 rules、更新 skill、强化规则、规则没生效、AI 老写错。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# 更新项目 Rules & Skills
|
||||
|
||||
本 skill 是修改项目 rules(`PipeReviewReport` AI 行为约束)和 skills(`PipeReviewReport` 专用技能)的唯一入口。
|
||||
|
||||
## 核心职责
|
||||
|
||||
1. **添加新内容**:按分层策略决定新约束/新参考放到哪个 rule 或 skill 中
|
||||
2. **修改已有内容**:外科手术式修改现有 rule/skill 文件,不破坏结构
|
||||
3. **强化已有规则**:当用户反馈"AI 还是写错 XX"时,分析原因并针对性强化
|
||||
4. **调整加载策略**:根据内容性质修改 `alwaysApply`/`globs`/`disable-model-invocation`
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — 了解当前架构
|
||||
|
||||
在修改前,必须读取并理解以下文件,确保不破坏现有结构:
|
||||
|
||||
### Rules 清单
|
||||
|
||||
| 文件 | 加载策略 | 内容范围 |
|
||||
|---|---|---|
|
||||
| `.agents/rules/wpf-core.md` | alwaysApply: true | 项目结构、命名约定、ViewModel、MVVM属性、Command、服务、DI、对话框、LoadingStatus、文件对话框/文件夹选择、输入限长、Mapster |
|
||||
| `.agents/rules/csharp-syntax.md` | alwaysApply: true | C# 语法硬约束(禁止 var、null检查、Debug.WriteLine、DRY等) |
|
||||
| `.agents/rules/architecture.md` | alwaysApply: true | 技术栈、项目架构、编译校验 |
|
||||
| `.agents/rules/editor-conventions.md` | alwaysApply: true | 换行符、编码规范 |
|
||||
| `.agents/rules/max-libraries-reference.md` | alwaysApply: true | MAX 系列及内部 fork 库源码路径、包名对照(含 MAX.HandyControl / MAX.FastWpfGrid / MAX.WPF.DragDrop) |
|
||||
| `.agents/rules/wpf-xaml.md` | globs: `**/*.xaml` | XAML 头部、BindingProxy、输入限长、DRY、颜色、枚举绑定、MarkupExtension |
|
||||
| `.agents/rules/icon-usage.md` | globs: `**/*.xaml`, `**/*.cs` | 图标系统完整规范 |
|
||||
| `.agents/rules/controls-reference.md` | globs: `**/*.xaml` | IconText、HorizontalIconText、TitleContent |
|
||||
|
||||
### Skills 清单
|
||||
|
||||
| Skill | disable-model-invocation | 类型 |
|
||||
|---|---|---|
|
||||
| `add-view` | false | 操作:新建 View+ViewModel |
|
||||
| `add-dialog` | false | 操作:新建对话框 |
|
||||
| `add-service` | false | 操作:新建 Service |
|
||||
| `add-module` | false | 操作:新建模块 |
|
||||
| `initialize-new-project` | true | 操作:初始化项目(需用户点名触发) |
|
||||
| `write-commit-message` | true | 工具:生成提交日志 |
|
||||
| `write-staged-commit-message` | true | 工具:生成暂存区提交日志 |
|
||||
| `review-rules` | true | 审查:代码合规检查 |
|
||||
| `reference-typed-ui-object-service` | false | 参考:TypedUIObjectService API |
|
||||
| `reference-loading-status` | false | 参考:LoadingStatus API |
|
||||
| `reference-fast-wpf-grid` | false | 参考:MAX.FastWpfGrid 用法 |
|
||||
| `reference-enum-display` | false | 参考:EnumDisplayService 内部实现 |
|
||||
| `reference-timer` | false | 参考:CompensatingTimer 及三个 TimerController 定时器用法 |
|
||||
| `reference-npoi-report` | false | 参考:sld Python 报告 / 自定义模板 / MiniExcel(须与参考软件完全一致) |
|
||||
| `update-rules-skills` | false | 元:修改 rules/skills(本 skill 自身) |
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — 决定内容去向
|
||||
|
||||
当用户提供了新内容(代码示例、新约束、新控件等),按以下决策树决定放哪里:
|
||||
|
||||
### 决策树
|
||||
|
||||
```
|
||||
用户提供的内容是什么?
|
||||
├── 行为约束("必须做 X"、"禁止做 Y"、"用 A 替代 B")
|
||||
│ ├── 与 C# 编码通用 → csharp-syntax.md
|
||||
│ ├── 与 WPF MVVM、属性、命令相关 → wpf-core.md
|
||||
│ └── 与 XAML 编写相关 → wpf-xaml.md
|
||||
│
|
||||
├── API 参考 / 代码模板(方法签名、属性列表、复制即用的代码块 >20 行)
|
||||
│ ├── 与控件/服务/工具类相关 → 新建或更新 reference-* skill(disable-model-invocation: false)
|
||||
│ └── 与已有 reference skill 高度相关 → 更新那个 skill
|
||||
│
|
||||
├── 图标相关 → icon-usage.md(图标系统已在其中完整覆盖)
|
||||
│
|
||||
├── 控件文档(作用不是约束而是教"怎么用") → controls-reference.md
|
||||
│
|
||||
└── 需要澄清 → 先向用户确认应该放在哪里
|
||||
```
|
||||
|
||||
### alwaysApply 阈值
|
||||
|
||||
`alwaysApply: true` 是稀缺资源。以下判断标准:
|
||||
|
||||
- **可以放 alwaysApply**:无论任务类型,缺了这条 AI 就会写错的硬约束
|
||||
- **不能放 alwaysApply**:只是参考手册、API 列表、代码片段、或仅在特定文件类型才需要的约束
|
||||
|
||||
当 alwaysApply 的 rules 总行数超过 1500 行时,必须向用户预警:"当前 alwaysApply 总量已达 X 行,新增内容建议放到 globs 规则或 reference skill 中"。
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — 修改策略
|
||||
|
||||
### 添加前:去重检查(强制,不可跳过)
|
||||
|
||||
**在添加任何新内容之前,必须先确认已有 rules 和 skills 中没有相同或高度相似的内容。** 跳过此检查会导致重复定义、矛盾冲突、或冗余 skill 触发。
|
||||
|
||||
#### 检查流程
|
||||
|
||||
```
|
||||
用户要求添加的内容
|
||||
├── 1. grep 所有 rules 和 skills,搜索关键词
|
||||
│ └── 关键词 = 用户需求中的核心术语(类名、方法名、约束主题等)
|
||||
│
|
||||
├── 2. 判断命中结果
|
||||
│ ├── 完全命中(已有文件明确写了同样的内容)
|
||||
│ │ └── 告知用户"这条规则已存在于 [文件名] 第 X 行,无需重复添加。是否需要强化/修改?"
|
||||
│ │
|
||||
│ ├── 部分命中(已有文件覆盖了相关主题,但未覆盖用户提出的具体细节)
|
||||
│ │ └── 告知用户"相关内容已在 [文件名] 中,我可以在此基础追加新细节。是否确认?"
|
||||
│ │
|
||||
│ └── 无命中 → 继续执行添加
|
||||
│
|
||||
└── 3. 特别检查:新建 skill 前
|
||||
└── 确认没有已有 skill 覆盖同样的主题(包括命名相似、功能重叠)
|
||||
```
|
||||
|
||||
#### 常见重复风险
|
||||
|
||||
| 用户说 | 实际已存在于 | 处理方式 |
|
||||
|---|---|---|
|
||||
| "加一条关于 TypedUIObjectService 的用法" | `wpf-core.md` + `reference-typed-ui-object-service` skill | 告知已有,询问是否需要补充细节 |
|
||||
| "建一个关于图标系统的 skill" | `icon-usage.md`(规则已完整覆盖) | 告知已有,图标类不需要拆分 skill |
|
||||
| "完善 MVVM 属性的规范" | `wpf-core.md`「MVVM 属性定义」 | 告知已有,确认是否要强调/补充反例 |
|
||||
| "增加 LoadingCover 的用法" | `wpf-core.md` + `wpf-xaml.md` + `reference-loading-status` skill | 告知已有,询问具体要补什么 |
|
||||
|
||||
### 添加新内容(追加到已有文件)
|
||||
|
||||
1. **(必须先执行去重检查)**
|
||||
2. 确定目标文件(按 Step 1 决策树)
|
||||
3. 用 `Read` 读取目标文件,找到最佳插入位置
|
||||
4. 在相关章节末尾追加新内容(保持与已有内容的间距一致)
|
||||
5. 如果是全新的主题(文件中没有对应章节),在合适位置插入新 `##` 段落
|
||||
6. **用户贴出业务/反例代码时**:不要整段复制进 rule/skill;提炼为一般性约束 + 精简模板/正反例(去掉业务字段、服务名、具体数值)
|
||||
|
||||
### 新建 Rule 或 Skill 文件
|
||||
|
||||
当 Step 1 决策树判断现有文件都不适合承载新内容时,**可以新建 rule 或 skill**。
|
||||
|
||||
#### 新建 Rule(`.agents/rules/xxx.md`)
|
||||
|
||||
**触发条件**:新内容是一个独立的行为约束或 XAML 约定,与现有 rule 文件没有概念交集。
|
||||
|
||||
**文件模板**:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: 简要描述规则用途(一句话)
|
||||
alwaysApply: true | false
|
||||
globs: "**/*.cs" # 仅 alwaysApply: false 时需要
|
||||
---
|
||||
|
||||
# 规则标题
|
||||
|
||||
规则内容(代码示例、反例对比等)
|
||||
```
|
||||
|
||||
**命名**:`{domain}-{focus}.md`,如 `wpf-core.md`、`api-conventions.md`。
|
||||
|
||||
#### 新建 Skill(`.cursor/skills/{name}/SKILL.md`)
|
||||
|
||||
**触发条件区分**:
|
||||
|
||||
| 新内容性质 | 应新建的 skill 类型 | disable-model-invocation |
|
||||
|---|---|---|
|
||||
| 操作类(创建文件/更改代码) | `{verb}-{noun}` 操作 skill | `false`(用户说"新建 XX"时自动触发) |
|
||||
| 参考类(API 手册/代码模板) | `reference-{topic}` 参考 skill | `false`(相关代码出现时自动加载) |
|
||||
| 工具类(分析/生成/审查) | `{verb}-{noun}` 工具 skill | `true`(需用户点名触发,避免误判) |
|
||||
|
||||
**文件模板**:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {skill-name}
|
||||
description: >-
|
||||
简要描述 skill 功能和触发场景。Use when {触发条件}。
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# {Skill 标题}
|
||||
|
||||
## 操作步骤
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
**命名**:小写字母 + 连字符,最长 64 字符。操作 skill 参考 `add-view`;参考 skill 以 `reference-` 开头;工具 skill 参考 `write-commit-message`。
|
||||
|
||||
**新建后**:把新 skill 追加到 Step 0 的 Skills 清单中,确保下次修改时能看到完整视角。
|
||||
|
||||
### 修改已有内容
|
||||
|
||||
1. 用 `StrReplace` 进行外科手术式精确替换
|
||||
2. **只改用户要求的部分**,不顺手优化相邻内容
|
||||
3. 保持原有缩进、格式、代码块语言标签一致
|
||||
|
||||
### 强化已有规则(用户反馈"AI 还是写错 XX")
|
||||
|
||||
当用户说 AI 没有遵守某条规则时,按以下优先级处理:
|
||||
|
||||
1. **检查规则是否存在**:在 rules 中 grep 相关内容
|
||||
2. **如果规则已存在但 AI 没遵守**:
|
||||
- 检查 loading strategy:是否被放在 globs 规则中而当前场景未触发?
|
||||
- 提升优先级:将关键反例从 globs 规则移到 alwaysApply 核心规则
|
||||
- 增加强调标记:在关键位置加 `**禁止**`、`❌` 标记
|
||||
- 增加正反例对比:如果没有反例代码块,补充 "❌ 错误 vs ✅ 正确" 对比
|
||||
3. **如果规则不存在**:按 Step 1 决策树添加新约束
|
||||
4. **如果规则正确但 AI 仍然忽略**:可能是上下文过长被稀释。考虑:
|
||||
- 精简 alwaysApply 中的冗余文字
|
||||
- 把这条规则提前到规则文件更靠前的位置
|
||||
- 在 description 中加入触发关键词
|
||||
|
||||
### 强调/标记策略
|
||||
|
||||
以下标记按严重程度递减:
|
||||
|
||||
| 标记 | 含义 | 示例 |
|
||||
|---|---|---|
|
||||
| `**强制遵守**` | 最高优先级,放章节最前 | `**强制遵守:** 所有图标必须从 iconfont 获取` |
|
||||
| `**禁止**` | 绝对不能做的 | `**禁止**使用 var` |
|
||||
| `**必须**` | 必须要做的 | `**必须**在 .xaml 中加名注释` |
|
||||
| `❌ 错误 / ✅ 正确` | 正反例对比 | 代码块标注 |
|
||||
| `(详见 xxx skill)` | 内容引用,避免重复 | `TypedUIObjectService 完整 API 详见 reference-xxx` |
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — 编译验证(仅在修改 rule/skill 本身代码示例时)
|
||||
|
||||
如果修改涉及 **rule/skill 中的 C# 代码示例可能影响编译**(如修改了方法的签名、引入了新的类型引用等),需要运行 `dotnet build -v q` 验证。
|
||||
|
||||
如果只是调整规则文字、标记、排版等,不需要编译。
|
||||
|
||||
---
|
||||
|
||||
## Key Constraints
|
||||
|
||||
1. **外科手术**:只改用户要求的部分,不顺手优化不相干内容
|
||||
2. **不破坏分层**:不把 API 参考写进 alwaysApply,不把硬约束降级到 globs
|
||||
3. **不重复内容**:同一段信息不在两个地方出现(用 "详见 xxx" 引用)
|
||||
4. **不改已有文件的换行符/编码**(`editor-conventions.md` 约束)
|
||||
5. **操作后汇报**:简要说明改了哪个文件、加/改了什么内容、为什么放在那里
|
||||
6. **勿整段粘贴用户代码**:用户提供的业务/反例代码只作素材;写入 rule/skill 时做总结与一般性约束,用精简模板或正反例表达,禁止原样复制大段业务实现
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: write-commit-message
|
||||
description: >-
|
||||
根据 git 变更生成简体中文提交日志,遵循固定格式(首行概括,必要时空行加分点简述)。
|
||||
当用户要求写提交日志、commit message、提交说明,或在提交前需要整理变更摘要时使用。
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 写提交日志
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. 若尚未掌握变更内容,先并行执行:
|
||||
- `git status` 查看变更文件
|
||||
- `git diff` 查看未暂存改动
|
||||
- `git diff --cached` 查看已暂存改动(如有)
|
||||
2. 阅读 diff,识别新增功能、重要修改与实际影响,忽略构建产物、IDE 缓存等无关文件
|
||||
3. 按下方格式生成提交日志;用户要求直接提交时,将生成的文本作为 commit message 使用
|
||||
|
||||
## 输出规范(必须严格遵守)
|
||||
|
||||
回复中只允许出现提交日志正文,禁止输出任何其他内容,包括但不限于:
|
||||
|
||||
- 前言、说明、标题(如「提交日志如下」)
|
||||
- 对变更的分析或评论
|
||||
- 提醒拆提交、后续建议等附加说明
|
||||
- Markdown 格式(代码块、加粗、标题、列表语法以外的装饰等)
|
||||
|
||||
目的:用户可一键全选复制,直接用于 `git commit -m`。
|
||||
|
||||
## 正文格式
|
||||
|
||||
- 使用简体中文
|
||||
- 第一行:一段简短的描述性句子,概括本次更改的核心内容(必填)
|
||||
- 分点简述:可选。仅当变更多处、第一行无法充分表达、或需要补充关键细节时才添加
|
||||
- 若使用分点:第二行留空,第三行起用 `- `(横杠 + 空格)列出 1~2 个要点,每点一两句话
|
||||
- 若改动单一、第一行已说清楚:只输出第一行,不要空行,不要硬凑分点
|
||||
- 不要使用加粗或其他强调格式
|
||||
- 英文类名、方法名、变量名等可直接保留原样(非必要时用描述性文字即可)
|
||||
- 重点突出新增功能、重要修改和实际影响
|
||||
|
||||
两种模板,按实际情况选用:
|
||||
|
||||
单行(改动简单、第一行已足够):
|
||||
|
||||
```
|
||||
<一句话概括核心变更>
|
||||
```
|
||||
|
||||
多行(有必要补充细节时):
|
||||
|
||||
```
|
||||
<一句话概括核心变更>
|
||||
|
||||
- <主要改动 1>
|
||||
- <主要改动 2>
|
||||
```
|
||||
|
||||
## 写作原则
|
||||
|
||||
- 第一行说「做了什么、解决什么问题」,避免空泛的「更新代码」「修复 bug」
|
||||
- 分点是补充说明,不是必填项;第一行能独立表达清楚时,省略分点
|
||||
- 不要为了凑格式而拆分无意义的细项
|
||||
- 分点只写对使用者或维护者有意义的变更,不写琐碎重命名、格式化
|
||||
- 多项无关改动时,在内部自行取舍重点,仍只输出一条提交日志正文,不附加说明
|
||||
|
||||
## 示例
|
||||
|
||||
示例 1(改动较多,需要分点)
|
||||
|
||||
变更:新增测试台拓扑视图,并修复参数网格刷新问题
|
||||
|
||||
正确输出:
|
||||
|
||||
新增测试台拓扑可视化视图并修复参数表格刷新异常
|
||||
|
||||
- 添加 Topo1~Topo4 四种拓扑布局视图及对应 ViewModel
|
||||
- 修复切换测试台后参数网格未重新加载的问题
|
||||
|
||||
示例 2(改动单一,无需分点)
|
||||
|
||||
变更:仅修正一处空指针
|
||||
|
||||
正确输出:
|
||||
|
||||
修复未选择设备时点击导入导致的崩溃
|
||||
|
||||
错误输出(含多余说明或 Markdown,禁止):
|
||||
|
||||
以下是提交日志:
|
||||
|
||||
```
|
||||
新增测试台...
|
||||
```
|
||||
|
||||
或:「已根据暂存区生成提交日志,请复制使用:……」
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: write-staged-commit-message
|
||||
description: >-
|
||||
仅根据 git 暂存区(staged)变更生成简体中文提交日志,遵循固定格式(首行概括,必要时空行加分点简述)。
|
||||
当用户要求根据暂存区写提交日志、staged commit message、已 add 的提交说明,或在 git add 后提交前需要整理摘要时使用。
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 写提交日志(仅暂存区)
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. **只读取暂存区内容**,并行执行:
|
||||
- `git diff --cached` 查看已暂存改动
|
||||
- `git status --short` 确认哪些文件在暂存区(辅助识别路径,不以未暂存内容为准)
|
||||
2. **禁止**执行 `git diff`(无 `--cached`)读取工作区未暂存改动
|
||||
3. 若 `git diff --cached` 为空,回复中只输出一行:`暂存区无变更,请先 git add`
|
||||
4. 阅读暂存区 diff,识别新增功能、重要修改与实际影响,忽略构建产物、IDE 缓存等无关文件
|
||||
5. 按下方格式生成提交日志;用户要求直接提交时,将生成的文本作为 commit message 使用
|
||||
|
||||
## 输出规范(必须严格遵守)
|
||||
|
||||
回复中只允许出现提交日志正文,禁止输出任何其他内容,包括但不限于:
|
||||
|
||||
- 前言、说明、标题(如「提交日志如下」)
|
||||
- 对变更的分析或评论
|
||||
- 提醒拆提交、后续建议等附加说明
|
||||
- Markdown 格式(代码块、加粗、标题、列表语法以外的装饰等)
|
||||
|
||||
目的:用户可一键全选复制,直接用于 `git commit -m`。
|
||||
|
||||
## 正文格式
|
||||
|
||||
- 使用简体中文
|
||||
- 第一行:一段简短的描述性句子,概括本次更改的核心内容(必填)
|
||||
- 分点简述:可选。仅当变更多处、第一行无法充分表达、或需要补充关键细节时才添加
|
||||
- 若使用分点:第二行留空,第三行起用 `- `(横杠 + 空格)列出 1~2 个要点,每点一两句话
|
||||
- 若改动单一、第一行已说清楚:只输出第一行,不要空行,不要硬凑分点
|
||||
- 不要使用加粗或其他强调格式
|
||||
- 英文类名、方法名、变量名等可直接保留原样(非必要时用描述性文字即可)
|
||||
- 重点突出新增功能、重要修改和实际影响
|
||||
- **只描述暂存区内的变更**,不要写入未 `git add` 的内容
|
||||
|
||||
两种模板,按实际情况选用:
|
||||
|
||||
单行(改动简单、第一行已足够):
|
||||
|
||||
```
|
||||
<一句话概括核心变更>
|
||||
```
|
||||
|
||||
多行(有必要补充细节时):
|
||||
|
||||
```
|
||||
<一句话概括核心变更>
|
||||
|
||||
- <主要改动 1>
|
||||
- <主要改动 2>
|
||||
```
|
||||
|
||||
## 写作原则
|
||||
|
||||
- 第一行说「做了什么、解决什么问题」,避免空泛的「更新代码」「修复 bug」
|
||||
- 分点是补充说明,不是必填项;第一行能独立表达清楚时,省略分点
|
||||
- 不要为了凑格式而拆分无意义的细项
|
||||
- 分点只写对使用者或维护者有意义的变更,不写琐碎重命名、格式化
|
||||
- 多项无关改动时,在内部自行取舍重点,仍只输出一条提交日志正文,不附加说明
|
||||
- 暂存区只含部分文件时,提交日志应反映**即将提交的内容**,而非整个工作区
|
||||
|
||||
## 与 write-commit-message 的区别
|
||||
|
||||
| | write-commit-message | 本 skill |
|
||||
|---|---|---|
|
||||
| 数据来源 | status + 未暂存 diff + 暂存 diff | **仅** `--cached` 暂存 diff |
|
||||
| 适用场景 | 整理全部本地变更 | `git add` 之后、提交之前 |
|
||||
|
||||
## 示例
|
||||
|
||||
示例 1(暂存区改动较多,需要分点)
|
||||
|
||||
暂存区变更:新增测试台拓扑视图,并修复参数网格刷新问题
|
||||
|
||||
正确输出:
|
||||
|
||||
新增测试台拓扑可视化视图并修复参数表格刷新异常
|
||||
|
||||
- 添加 Topo1~Topo4 四种拓扑布局视图及对应 ViewModel
|
||||
- 修复切换测试台后参数网格未重新加载的问题
|
||||
|
||||
示例 2(暂存区改动单一,无需分点)
|
||||
|
||||
暂存区变更:仅修正一处空指针
|
||||
|
||||
正确输出:
|
||||
|
||||
修复未选择设备时点击导入导致的崩溃
|
||||
|
||||
示例 3(暂存区为空)
|
||||
|
||||
正确输出:
|
||||
|
||||
暂存区无变更,请先 git add
|
||||
|
||||
错误输出(含多余说明或 Markdown,禁止):
|
||||
|
||||
以下是提交日志:
|
||||
|
||||
```
|
||||
新增测试台...
|
||||
```
|
||||
|
||||
或:「已根据暂存区生成提交日志,请复制使用:……」
|
||||
Reference in new issue
Block a user