暂时更改一部分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,禁止):
|
||||||
|
|
||||||
|
以下是提交日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
新增测试台...
|
||||||
|
```
|
||||||
|
|
||||||
|
或:「已根据暂存区生成提交日志,请复制使用:……」
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 项目规则入口
|
||||||
|
|
||||||
|
本文件适用于当前仓库及其子目录。规则正文统一维护在 `.agents/rules/`;以下路径均相对于仓库根目录。
|
||||||
|
|
||||||
|
规则文件使用 `.md` 格式,按以下入口要求加载。
|
||||||
|
|
||||||
|
## 常驻规则
|
||||||
|
|
||||||
|
开始任务前,读取并遵循:
|
||||||
|
|
||||||
|
- `.agents/rules/architecture.md`
|
||||||
|
- `.agents/rules/csharp-syntax.md`
|
||||||
|
- `.agents/rules/editor-conventions.md`
|
||||||
|
- `.agents/rules/handycontrol-usage.md`
|
||||||
|
- `.agents/rules/max-libraries-reference.md`
|
||||||
|
- `.agents/rules/wpf-core.md`
|
||||||
|
|
||||||
|
## 按需规则
|
||||||
|
|
||||||
|
- 编辑 `.xaml` 文件前,读取 `.agents/rules/wpf-xaml.md` 和 `.agents/rules/controls-reference.md`。
|
||||||
|
- 编辑 `.xaml` 或 `.cs` 文件且涉及图标时,读取 `.agents/rules/icon-usage.md`。
|
||||||
|
- 新增规则文件后,应在本入口补充其适用场景与路径。
|
||||||
|
|
||||||
|
## 适用范围与冲突
|
||||||
|
|
||||||
|
- 当前仓库为 `Store.Management`。项目结构、依赖、目标框架和已实现能力以当前源码及配置为准。
|
||||||
|
- 规则约束当前任务,不因提及可选能力而新增业务模块、替换依赖或改变产品需求。
|
||||||
|
- 用户当前明确要求优先于本入口及引用规则;子目录存在 `AGENTS.md` 时,其针对该目录的具体规则优先于上层通用规则。
|
||||||
|
|
||||||
|
## Ponytail:最小完整实现
|
||||||
|
|
||||||
|
来源:[Ponytail AGENTS.md](https://github.com/DietrichGebert/ponytail/blob/main/AGENTS.md),基于已安装的 5.3.2 版本合并。以下通用原则补充现有项目规则;涉及语言版本、编码规范、分层、依赖选择与验证方式时,以前述项目规则及具体任务要求为准。
|
||||||
|
|
||||||
|
You are a lazy senior developer. The best code is the code never written. You solve the whole problem with the least new code. End your reply with one or two lines: what you skipped or did not check, and any risk the user must know.
|
||||||
|
|
||||||
|
### Before you write
|
||||||
|
|
||||||
|
Read the task and the code it touches. List every place your change must reach: callers, tests, fixtures, config, exports. Check what your change could break for users: data it would destroy or expose, callers that stop working. That is scope. Extra features are not.
|
||||||
|
|
||||||
|
### The smallest complete change
|
||||||
|
|
||||||
|
Take the first option that fully works:
|
||||||
|
|
||||||
|
1. Does it need to exist? Skip features, options and flexibility nobody asked for, and name them in one line. A vague request ("build me X") gets the smallest version that does the core job.
|
||||||
|
2. Already in this codebase (a helper, component, service, pattern)? Use it the way the surrounding code does.
|
||||||
|
3. Standard library or a platform feature? Use it, unless the project has its own. A house component beats a native widget.
|
||||||
|
4. An installed dependency? Use it. Never add a dependency for a few lines.
|
||||||
|
5. Can it be one line a reader gets at a glance? One line.
|
||||||
|
6. Otherwise: the minimum code that works.
|
||||||
|
|
||||||
|
- Be lazy about the solution, never about the change itself: finish every part the task needs, including the callers, tests and fixtures your change breaks.
|
||||||
|
- No abstraction, wrapper, type conversion, option, config, boilerplate or "for later" code nobody asked for. Keep values in the form the platform already gives you. Deletion beats addition. Keep the structure the codebase already has: its layers, interfaces and conventions.
|
||||||
|
- The shortest working diff wins, once you know everything it must touch. A one-liner that needs decoding is not short.
|
||||||
|
- Comment only the why the code cannot show, in one line.
|
||||||
|
- Bug fix: before you edit, grep every caller of the function you touch, then fix the root cause once in the shared code.
|
||||||
|
- Code you move or merge keeps its error handling and validation.
|
||||||
|
- Between options of equal size, take the one that is correct on edge cases.
|
||||||
|
- Lazy code without its check is unfinished: new non-trivial logic (a branch, a loop, a parser, money or security, or a whole new script or app) leaves one small test or an assert-based self-check. Trivial changes need none.
|
||||||
|
- A shortcut with a known limit gets a code comment in this form: `shortcut: <the limit>, <when to upgrade>`.
|
||||||
|
|
||||||
|
Never cut: validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs, anything the user asked for.
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DefaultTargetFramework>net8.0</DefaultTargetFramework>
|
<DefaultTargetFramework>net8.0</DefaultTargetFramework>
|
||||||
<TargetFramework>$(DefaultTargetFramework)-windows</TargetFramework>
|
<TargetFramework>$(DefaultTargetFramework)-windows</TargetFramework>
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>14.0</LangVersion>
|
||||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
||||||
<CheckEolTargetFramework>false</CheckEolTargetFramework>
|
<CheckEolTargetFramework>false</CheckEolTargetFramework>
|
||||||
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
|
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
|
||||||
|
|||||||
@@ -7,3 +7,65 @@ public interface IPriority
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
int Priority { get; }
|
int Priority { get; }
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 优先级
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// </remarks>
|
||||||
|
public static class IntPriority
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 最高优先级 (5000)
|
||||||
|
/// </summary>
|
||||||
|
public const int Highest = 5000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非常高优先级 (4000)
|
||||||
|
/// </summary>
|
||||||
|
public const int VeryHigh = 4000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 较高优先级 (3000)
|
||||||
|
/// </summary>
|
||||||
|
public const int Higher = 3000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 高优先级 (2000)
|
||||||
|
/// </summary>
|
||||||
|
public const int High = 2000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 高于正常优先级 (1000)
|
||||||
|
/// </summary>
|
||||||
|
public const int AboveNormal = 1000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正常优先级 (0)
|
||||||
|
/// </summary>
|
||||||
|
public const int Normal = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 低于正常优先级 (-1000)
|
||||||
|
/// </summary>
|
||||||
|
public const int BelowNormal = -1000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 低优先级 (-2000)
|
||||||
|
/// </summary>
|
||||||
|
public const int Low = -2000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 较低优先级 (-3000)
|
||||||
|
/// </summary>
|
||||||
|
public const int Lower = -3000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非常低优先级 (-4000)
|
||||||
|
/// </summary>
|
||||||
|
public const int VeryLow = -4000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最低优先级 (-5000)
|
||||||
|
/// </summary>
|
||||||
|
public const int Lowest = -5000;
|
||||||
|
}
|
||||||
@@ -88,8 +88,8 @@ public class AutofacContainerExtension : IContainerExtension<IContainer>, IConta
|
|||||||
item.Register(Builder);
|
item.Register(Builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<IPreRegisterTypes> PreRegisterTypes = ReflectionHelper.GetInstancesByInterface<IPreRegisterTypes>(assemblies);
|
IEnumerable<IPrismRegisterTypes> PreRegisterTypes = ReflectionHelper.GetInstancesByInterface<IPrismRegisterTypes>(assemblies);
|
||||||
foreach (IPreRegisterTypes item in PreRegisterTypes)
|
foreach (IPrismRegisterTypes item in PreRegisterTypes)
|
||||||
{
|
{
|
||||||
item.RegisterTypes(this);
|
item.RegisterTypes(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace MAX.Framework.Prism.Autofac;
|
namespace MAX.Framework.Prism.Autofac;
|
||||||
|
|
||||||
public interface IPreRegisterTypes
|
public interface IPrismRegisterTypes
|
||||||
{
|
{
|
||||||
void RegisterTypes(IContainerRegistry containerRegistry);
|
void RegisterTypes(IContainerRegistry containerRegistry);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
using Autofac;
|
||||||
|
using MAX.Core;
|
||||||
|
using MAX.Framework.Interfaces;
|
||||||
|
|
||||||
|
namespace Store.Admin
|
||||||
|
{
|
||||||
|
public class Startup : IStartup
|
||||||
|
{
|
||||||
|
public int Priority => IntPriority.Normal;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Store.Management.Framework\Store.Management.Framework.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Autofac;
|
||||||
|
using MAX.Core;
|
||||||
|
using MAX.Framework.Interfaces;
|
||||||
|
using MAX.Framework.Prism.Autofac;
|
||||||
|
using Prism.Ioc;
|
||||||
|
|
||||||
|
namespace Store.ListingProduct
|
||||||
|
{
|
||||||
|
public class Startup : IStartup, IPrismRegisterTypes
|
||||||
|
{
|
||||||
|
public int Priority => IntPriority.Normal;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(ContainerBuilder builder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||||
|
{
|
||||||
|
//containerRegistry.RegisterForNavigation<OnceBrakeDetailView, OnceBrakeDetailViewModel>(NavigationTokens.OnceBrakeDetailContext);
|
||||||
|
//containerRegistry.RegisterForNavigation<SummaryView, SummaryViewModel>(NavigationTokens.SummaryViewContext);
|
||||||
|
|
||||||
|
//containerRegistry.RegisterDialog<SummaryColumnDialog, SummaryColumnDialogViewModel>(nameof(SummaryColumnDialog));
|
||||||
|
//containerRegistry.RegisterDialog<SummaryLogDialog, SummaryLogDialogViewModel>(nameof(SummaryLogDialog));
|
||||||
|
|
||||||
|
//containerRegistry.RegisterDialog<ArchiveDialog, ArchiveDialogViewModel>(nameof(ArchiveDialog));
|
||||||
|
//containerRegistry.RegisterDialog<UnArchiveDialog, UnArchiveDialogViewModel>(nameof(UnArchiveDialog));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Store.Management.Framework\Store.Management.Framework.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
[assembly: XmlnsDefinition("Store.Management.Framework", "Store.Management.Framework.Controls")]
|
[assembly: XmlnsDefinition("Store.Management.Framework", "Store.Management.Framework.Controls")]
|
||||||
[assembly: XmlnsDefinition("Store.Management.Framework", "Store.Management.Framework.Controls.Attach")]
|
[assembly: XmlnsDefinition("Store.Management.Framework", "Store.Management.Framework.Controls.Attach")]
|
||||||
|
|
||||||
[assembly: XmlnsPrefix("Store.Management.Framework", "mf")]
|
[assembly: XmlnsPrefix("Store.Management.Framework", "f")]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace Store.Management.Framework.Configs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用级常量配置:代码标识与显示名称分离。
|
||||||
|
/// </summary>
|
||||||
|
public static class AppConfig
|
||||||
|
{
|
||||||
|
/// <summary>代码用软件名称(程序集/目录/Mutex 等标识)。</summary>
|
||||||
|
public const string AppName = "Store.Management";
|
||||||
|
|
||||||
|
/// <summary>显示与输出用软件名称(启动页、对话框标题、安装器显示名等)。</summary>
|
||||||
|
public const string AppDisplayName = "Store.Management";
|
||||||
|
|
||||||
|
/// <summary>是否是发布版本。</summary>
|
||||||
|
public static bool IsRelease
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
#if RELEASE
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>当前构建模式显示名(Release / Debug)。</summary>
|
||||||
|
public static string BuildMode
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return IsRelease ? "Release" : "Debug";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ public sealed class FolderConfig : IFolderConfig
|
|||||||
string IFolderConfig.LoggingFolder => LoggingFolder;
|
string IFolderConfig.LoggingFolder => LoggingFolder;
|
||||||
string IFolderConfig.CacheFolder => CacheFolder;
|
string IFolderConfig.CacheFolder => CacheFolder;
|
||||||
|
|
||||||
public const string AppName = "Store.Management";
|
public const string AppName = "StoreManagement";
|
||||||
|
|
||||||
/// <summary>安装目录,用于读取随程序分发的资源。</summary>
|
/// <summary>安装目录,用于读取随程序分发的资源。</summary>
|
||||||
public static string ExeFolder => AppContext.BaseDirectory;
|
public static string ExeFolder => AppContext.BaseDirectory;
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ public partial class DialogWindow : WindowBase, IDialogWindow
|
|||||||
public DialogWindow()
|
public DialogWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Owner = Application.Current.MainWindow;
|
if (Application.Current.MainWindow is Window owner && !ReferenceEquals(owner, this))
|
||||||
|
{
|
||||||
|
Owner = owner;
|
||||||
|
}
|
||||||
isConstructorSuccessful = true;
|
isConstructorSuccessful = true;
|
||||||
dialogWindowStyle = (Style)TryFindResource("DialogWindowStyle");
|
dialogWindowStyle = (Style)TryFindResource("DialogWindowStyle");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Store.Management.Framework.Interfaces
|
||||||
|
{
|
||||||
|
|
||||||
|
public interface IMessageBoxService
|
||||||
|
{
|
||||||
|
|
||||||
|
//MessageResult Show(string messageBoxText, string caption, MessageBoxButton button, MessageIcon icon, MessageResult defaultResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="HandyControl" Version="3.6.2.1" />
|
<PackageReference Include="HandyControl" Version="3.6.2.1" />
|
||||||
|
<PackageReference Include="MAX.Common.WPF" Version="1.4.23" />
|
||||||
<ProjectReference Include="..\Core\MAX.Core\MAX.Core.csproj" />
|
<ProjectReference Include="..\Core\MAX.Core\MAX.Core.csproj" />
|
||||||
<ProjectReference Include="..\Core\MAX.Framework\MAX.Framework.csproj" />
|
<ProjectReference Include="..\Core\MAX.Framework\MAX.Framework.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Prism.Mvvm;
|
||||||
|
|
||||||
|
namespace Store.Management.Framework.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>异步操作的加载消息与错误状态。</summary>
|
||||||
|
public sealed class LoadingStatus : BindableBase
|
||||||
|
{
|
||||||
|
private bool _isLoading;
|
||||||
|
private string _message = "";
|
||||||
|
private string _errorMessage = "";
|
||||||
|
|
||||||
|
public bool IsLoading { get => _isLoading; private set => SetProperty(ref _isLoading, value); }
|
||||||
|
public string Message { get => _message; private set => SetProperty(ref _message, value); }
|
||||||
|
public string ErrorMessage { get => _errorMessage; private set => SetProperty(ref _errorMessage, value); }
|
||||||
|
|
||||||
|
public void Initialize(string message)
|
||||||
|
{
|
||||||
|
ErrorMessage = "";
|
||||||
|
Message = message;
|
||||||
|
IsLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Done()
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
Message = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message)
|
||||||
|
{
|
||||||
|
Done();
|
||||||
|
ErrorMessage = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
|
using Prism.Services.Dialogs;
|
||||||
using MAX.Notification;
|
using MAX.Notification;
|
||||||
using Store.Management.Notifications;
|
using Store.Management.Notifications;
|
||||||
using Store.Management.Framework;
|
using Store.Management.Framework;
|
||||||
@@ -6,12 +7,47 @@ using Store.Management.Services;
|
|||||||
using Store.Management.ViewModels;
|
using Store.Management.ViewModels;
|
||||||
using Store.Management.Views;
|
using Store.Management.Views;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using Store.Management.Api;
|
||||||
namespace Store.Management;
|
namespace Store.Management;
|
||||||
public partial class App : FrameworkApplication
|
public partial class App : FrameworkApplication
|
||||||
{
|
{
|
||||||
protected override Window CreateShell() => Container.Resolve<MainWindow>();
|
private readonly StoreApiClient? _apiClient;
|
||||||
|
|
||||||
|
public App() { }
|
||||||
|
|
||||||
|
public App(StoreApiClient apiClient) => _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||||
|
|
||||||
|
private StoreApiClient CreateApiClient() => _apiClient ?? new(new ApiClientOptions
|
||||||
|
{
|
||||||
|
BaseUrl = new Uri("http://localhost:14312/api/")
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override Window CreateShell() => null!;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
// 登录弹窗关闭与主窗口创建之间不触发 WPF 的最后窗口退出。
|
||||||
|
ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||||
|
Container.Resolve<IDialogService>().ShowDialog(
|
||||||
|
nameof(LoginWindow), new DialogParameters(), result =>
|
||||||
|
{
|
||||||
|
if (result.Result != ButtonResult.OK)
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow = Container.Resolve<MainWindow>();
|
||||||
|
ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||||
|
MainWindow.Show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override void RegisterTypes(IContainerRegistry registry)
|
protected override void RegisterTypes(IContainerRegistry registry)
|
||||||
{
|
{
|
||||||
|
registry.RegisterInstance(CreateApiClient());
|
||||||
|
registry.Register<LoginViewModel>(provider => new LoginViewModel(provider.Resolve<StoreApiClient>().Auth));
|
||||||
registry.RegisterSingleton<SingletonProbe>();
|
registry.RegisterSingleton<SingletonProbe>();
|
||||||
registry.Register<TransientProbe>();
|
registry.Register<TransientProbe>();
|
||||||
registry.RegisterSingleton<NotificationExampleState>();
|
registry.RegisterSingleton<NotificationExampleState>();
|
||||||
@@ -24,6 +60,13 @@ public partial class App : FrameworkApplication
|
|||||||
new ProductCacheHandler(provider.Resolve<NotificationExampleState>()),
|
new ProductCacheHandler(provider.Resolve<NotificationExampleState>()),
|
||||||
new ProductAuditHandler(provider.Resolve<NotificationExampleState>())
|
new ProductAuditHandler(provider.Resolve<NotificationExampleState>())
|
||||||
});
|
});
|
||||||
|
registry.RegisterDialog<LoginWindow, LoginViewModel>();
|
||||||
registry.RegisterDialog<FrameworkTestDialog, FrameworkTestDialogViewModel>();
|
registry.RegisterDialog<FrameworkTestDialog, FrameworkTestDialogViewModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
try { Container?.Resolve<StoreApiClient>().Dispose(); }
|
||||||
|
finally { base.OnExit(e); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Prism.Commands;
|
||||||
|
using MAX.Framework.Controls;
|
||||||
|
using Prism.Services.Dialogs;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Store.Management.Api.Authentication;
|
||||||
|
using Store.Management.Api.Transport;
|
||||||
|
using Store.Management.Framework.ViewModels;
|
||||||
|
|
||||||
|
namespace Store.Management.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>登录校验、认证请求和窗口生命周期内的请求取消。</summary>
|
||||||
|
public sealed class LoginViewModel(IAuthApi auth) : ConfirmDialogViewModel, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IAuthApi _auth = auth ?? throw new ArgumentNullException(nameof(auth));
|
||||||
|
private readonly CancellationTokenSource _lifetime = new();
|
||||||
|
private string _userName = "";
|
||||||
|
private string _passwordHash = "";
|
||||||
|
private bool _disposed;
|
||||||
|
private bool _authenticated;
|
||||||
|
|
||||||
|
public const int InputMaxLength = 250;
|
||||||
|
public LoadingStatus LoadingStatus { get; } = new();
|
||||||
|
public string UserName { get => _userName; set => SetProperty(ref _userName, value); }
|
||||||
|
public bool CanEdit => !LoadingStatus.IsLoading && !_disposed && !_authenticated;
|
||||||
|
public string LoginButtonText => LoadingStatus.IsLoading ? "正在登录…" : "登录";
|
||||||
|
|
||||||
|
public event EventHandler? ClearPasswordRequested;
|
||||||
|
|
||||||
|
/// <summary>仅由密码控件桥接调用,不保存明文密码。</summary>
|
||||||
|
public void UpdatePassword(string password)
|
||||||
|
{
|
||||||
|
if (!CanEdit)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务端要求 UTF-8 密码的 32 位大写 MD5,保留首尾空格。
|
||||||
|
_passwordHash = string.IsNullOrEmpty(password)
|
||||||
|
? "" : Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(password)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ICommand ConfirmCommand => LoginCommand;
|
||||||
|
|
||||||
|
public override void OnDialogOpened(IDialogParameters parameters)
|
||||||
|
{
|
||||||
|
Title = "门店管理 · 登录";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnDialogClosed() => Dispose();
|
||||||
|
|
||||||
|
public AsyncDelegateCommand LoginCommand => field ??= new AsyncDelegateCommand(LoginAsync, () => CanEdit);
|
||||||
|
|
||||||
|
private async Task LoginAsync()
|
||||||
|
{
|
||||||
|
if (!CanEdit)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(UserName))
|
||||||
|
{
|
||||||
|
LoadingStatus.Error("请输入用户名。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(_passwordHash))
|
||||||
|
{
|
||||||
|
LoadingStatus.Error("请输入密码。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingStatus.Initialize("正在验证账号,请稍候…");
|
||||||
|
NotifyStateChanged();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _auth.LoginAsync(new LoginRequest
|
||||||
|
{
|
||||||
|
UserName = UserName.Trim(),
|
||||||
|
Password = _passwordHash
|
||||||
|
}, _lifetime.Token);
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_authenticated = true;
|
||||||
|
OnRequestClose(new DialogResult(ButtonResult.OK));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_disposed)
|
||||||
|
{
|
||||||
|
// 关闭窗口产生的取消无需提示。
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
LoadingStatus.Error(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
LoadingStatus.Error("登录已取消,请重试。");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
LoadingStatus.Error("登录失败,请稍后重试。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_passwordHash = "";
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
LoadingStatus.Done();
|
||||||
|
ClearPasswordRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
NotifyStateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyStateChanged()
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(CanEdit));
|
||||||
|
RaisePropertyChanged(nameof(LoginButtonText));
|
||||||
|
LoginCommand.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
_passwordHash = "";
|
||||||
|
_lifetime.Cancel();
|
||||||
|
_lifetime.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Store.Management.Views.LoginWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:vm="clr-namespace:Store.Management.ViewModels"
|
||||||
|
Width="520"
|
||||||
|
Height="620"
|
||||||
|
MinWidth="440"
|
||||||
|
MinHeight="540"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:LoginViewModel}"
|
||||||
|
Background="{DynamicResource BackgroundBrush}"
|
||||||
|
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||||
|
Loaded="OnLoaded"
|
||||||
|
Unloaded="OnUnloaded"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
|
<Grid Margin="32" VerticalAlignment="Center">
|
||||||
|
<StackPanel MaxWidth="360">
|
||||||
|
<!-- 系统标识与登录说明 -->
|
||||||
|
<TextBlock
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource PrimaryBrush}"
|
||||||
|
Text="STORE MANAGEMENT" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
FontSize="28"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="登录门店管理" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,8,0,28"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||||
|
Text="请输入您的用户名和密码"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<!-- 登录表单:密码只保留在 PasswordBox 中 -->
|
||||||
|
<Border
|
||||||
|
Padding="24"
|
||||||
|
Background="{DynamicResource RegionBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8">
|
||||||
|
<StackPanel>
|
||||||
|
<Label
|
||||||
|
Padding="0,0,0,8"
|
||||||
|
Content="用户名"
|
||||||
|
Target="{Binding ElementName=UserNameBox, Mode=OneWay}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="UserNameBox"
|
||||||
|
Height="40"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
AutomationProperties.Name="用户名"
|
||||||
|
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||||
|
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||||
|
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<Label
|
||||||
|
Padding="0,20,0,8"
|
||||||
|
Content="密码"
|
||||||
|
Target="{Binding ElementName=PasswordBox, Mode=OneWay}" />
|
||||||
|
<PasswordBox
|
||||||
|
x:Name="PasswordBox"
|
||||||
|
PasswordChanged="OnPasswordChanged"
|
||||||
|
Height="40"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
AutomationProperties.Name="密码"
|
||||||
|
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||||
|
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
AutomationProperties.LiveSetting="Polite"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||||
|
Text="{Binding LoadingStatus.Message, Mode=OneWay}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<!-- 保留错误提示空间,避免登录失败时表单跳动 -->
|
||||||
|
<TextBlock
|
||||||
|
x:Name="ErrorText"
|
||||||
|
MinHeight="42"
|
||||||
|
Margin="0,12,0,8"
|
||||||
|
AutomationProperties.LiveSetting="Polite"
|
||||||
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
|
Text="{Binding LoadingStatus.ErrorMessage, Mode=OneWay}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<Button
|
||||||
|
x:Name="LoginButton"
|
||||||
|
Height="40"
|
||||||
|
Command="{Binding LoginCommand, Mode=OneWay}"
|
||||||
|
Content="{Binding LoginButtonText, Mode=OneWay}"
|
||||||
|
IsDefault="True"
|
||||||
|
Style="{StaticResource ButtonPrimary}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,20,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||||
|
Text="请使用管理员分配的账号登录"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Store.Management.ViewModels;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace Store.Management.Views;
|
||||||
|
|
||||||
|
public partial class LoginWindow : UserControl
|
||||||
|
{
|
||||||
|
public LoginWindow() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is LoginViewModel viewModel)
|
||||||
|
{
|
||||||
|
viewModel.ClearPasswordRequested += OnClearPasswordRequested;
|
||||||
|
}
|
||||||
|
UserNameBox.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPasswordChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is LoginViewModel viewModel)
|
||||||
|
{
|
||||||
|
viewModel.UpdatePassword(PasswordBox.Password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClearPasswordRequested(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
PasswordBox.Clear();
|
||||||
|
PasswordBox.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is LoginViewModel viewModel)
|
||||||
|
{
|
||||||
|
viewModel.ClearPasswordRequested -= OnClearPasswordRequested;
|
||||||
|
}
|
||||||
|
PasswordBox.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,17 @@
|
|||||||
<Project Path="Source/Core/MAX.Framework/MAX.Framework.csproj" />
|
<Project Path="Source/Core/MAX.Framework/MAX.Framework.csproj" />
|
||||||
<Project Path="Source/Core/MAX.Notification/MAX.Notification.csproj" />
|
<Project Path="Source/Core/MAX.Notification/MAX.Notification.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/Modules/">
|
||||||
|
<Project Path="Source/Modules/Store.Admin/Store.Admin.csproj" Id="16668499-9a77-443d-b82a-1c2177943117" />
|
||||||
|
<Project Path="Source/Modules/Store.ListingProduct/Store.ListingProduct.csproj" Id="88447096-25a2-46b5-aae7-aaa902b18faf" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/Tests/">
|
<Folder Name="/Tests/">
|
||||||
<Project Path="Tests/MAX.Core.SmokeTests/MAX.Core.SmokeTests.csproj" />
|
<Project Path="Tests/MAX.Core.SmokeTests/MAX.Core.SmokeTests.csproj" />
|
||||||
<Project Path="Tests/MAX.Notification.SmokeTests/MAX.Notification.SmokeTests.csproj" />
|
<Project Path="Tests/MAX.Notification.SmokeTests/MAX.Notification.SmokeTests.csproj" />
|
||||||
<Project Path="Tests/Store.Management.Api.SmokeTests/Store.Management.Api.SmokeTests.csproj" />
|
<Project Path="Tests/Store.Management.Api.SmokeTests/Store.Management.Api.SmokeTests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/_SolutionItems/">
|
<Folder Name="/_SolutionItems/">
|
||||||
|
<File Path="AGENTS.md" />
|
||||||
<File Path="Directory.Build.props" />
|
<File Path="Directory.Build.props" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Project Path="Source/Store.Management.Api/Store.Management.Api.csproj" />
|
<Project Path="Source/Store.Management.Api/Store.Management.Api.csproj" />
|
||||||
|
|||||||
@@ -4,18 +4,97 @@ using Store.Management;
|
|||||||
using Store.Management.Views;
|
using Store.Management.Views;
|
||||||
using Store.Management.ViewModels;
|
using Store.Management.ViewModels;
|
||||||
using Store.Management.Framework.Controls;
|
using Store.Management.Framework.Controls;
|
||||||
|
using Store.Management.Api;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using Prism.Ioc;
|
||||||
|
using Prism.Services.Dialogs;
|
||||||
|
using DialogWindow = Store.Management.Framework.Controls.DialogWindow;
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
[STAThread]
|
[STAThread]
|
||||||
static int Main()
|
static int Main(string[] args)
|
||||||
{
|
{
|
||||||
var app = new App();
|
var handler = new LoginHandler();
|
||||||
|
using var httpClient = new HttpClient(handler);
|
||||||
|
using var client = new StoreApiClient(new ApiClientOptions { BaseUrl = new Uri("http://localhost:14312/api/") }, httpClient);
|
||||||
|
var app = new App(client);
|
||||||
app.InitializeComponent();
|
app.InitializeComponent();
|
||||||
int exitCode = 0;
|
int exitCode = 0;
|
||||||
app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () =>
|
app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
for (int attempt = 0; attempt < 100 && !app.Windows.OfType<DialogWindow>().Any(window => window.Content is LoginWindow && window.IsLoaded); attempt++)
|
||||||
|
{
|
||||||
|
await Task.Delay(20);
|
||||||
|
}
|
||||||
|
DialogWindow loginHost = app.Windows.OfType<DialogWindow>().Single(window => window.Content is LoginWindow);
|
||||||
|
var login = (LoginWindow)loginHost.Content;
|
||||||
|
LoginViewModel loginVm = (LoginViewModel)login.DataContext;
|
||||||
|
if (args.Contains("--cancel-login") || args.Contains("--close-login"))
|
||||||
|
{
|
||||||
|
if (args.Contains("--close-login"))
|
||||||
|
{
|
||||||
|
loginHost.Close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loginVm.CancelCommand.Execute(null);
|
||||||
|
}
|
||||||
|
Check(!app.Windows.OfType<MainWindow>().Any(), "取消登录不得创建主窗口");
|
||||||
|
Console.WriteLine("PASS: startup login cancellation/window-close shuts down without opening the main window.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var userName = (System.Windows.Controls.TextBox)login.FindName("UserNameBox");
|
||||||
|
var password = (System.Windows.Controls.PasswordBox)login.FindName("PasswordBox");
|
||||||
|
var error = (System.Windows.Controls.TextBlock)login.FindName("ErrorText");
|
||||||
|
foreach (string key in new[] { "PrimaryTextBrush", "SecondaryTextBrush", "RegionBrush", "BorderBrush", "DangerBrush", "ButtonPrimary" })
|
||||||
|
{
|
||||||
|
Check(login.TryFindResource(key) is not null, $"登录主题资源缺失:{key}");
|
||||||
|
}
|
||||||
|
Check(loginHost.IsVisible && loginHost.DataContext == login.DataContext, "Prism 弹窗与登录页面共享 ViewModel");
|
||||||
|
Check(userName.MaxLength == LoginViewModel.InputMaxLength && password.MaxLength == LoginViewModel.InputMaxLength, "登录输入限长");
|
||||||
|
await loginVm.LoginCommand.Execute();
|
||||||
|
Check(error.Text == "请输入用户名。", "用户名校验");
|
||||||
|
userName.Text = " admin ";
|
||||||
|
await loginVm.LoginCommand.Execute();
|
||||||
|
Check(error.Text == "请输入密码。", "密码校验");
|
||||||
|
password.Password = "wrong";
|
||||||
|
await loginVm.LoginCommand.Execute();
|
||||||
|
Check(error.Text == "用户名或密码错误" && login.IsVisible, "失败后保留登录窗口");
|
||||||
|
Check(password.Password.Length == 0, "失败后清空密码");
|
||||||
|
Check(((System.Windows.Controls.Button)login.FindName("LoginButton")).IsEnabled && userName.IsEnabled && password.IsEnabled, "失败后恢复输入和提交");
|
||||||
|
password.Password = " secret ";
|
||||||
|
Check(ReferenceEquals(((System.Windows.Controls.Button)login.FindName("LoginButton")).Command, loginVm.LoginCommand), "登录按钮绑定 ViewModel 命令");
|
||||||
|
var pending = loginVm.LoginCommand.Execute();
|
||||||
|
Check(loginVm.LoadingStatus.IsLoading && !loginVm.CanEdit && !loginVm.LoginCommand.CanExecute(), "登录期间禁用输入与命令");
|
||||||
|
await loginVm.LoginCommand.Execute();
|
||||||
|
await pending;
|
||||||
|
Check(handler.RequestCount == 2, "阻止重复提交");
|
||||||
|
Check(client.Session.IsAuthenticated, "保存登录会话");
|
||||||
|
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||||
|
Check(!loginHost.IsVisible && password.Password.Length == 0, "成功后关闭登录窗口并清空密码");
|
||||||
|
IDialogService dialogs = app.Container.Resolve<IDialogService>();
|
||||||
|
ButtonResult? closeResult = null;
|
||||||
|
dialogs.Show(nameof(LoginWindow), new DialogParameters(), result => closeResult = result.Result);
|
||||||
|
DialogWindow closingHost = app.Windows.OfType<DialogWindow>().Single();
|
||||||
|
LoginWindow closingView = (LoginWindow)closingHost.Content;
|
||||||
|
LoginViewModel closingVm = (LoginViewModel)closingView.DataContext;
|
||||||
|
closingVm.UserName = "admin";
|
||||||
|
var closingPassword = (System.Windows.Controls.PasswordBox)closingView.FindName("PasswordBox");
|
||||||
|
closingPassword.Password = " secret ";
|
||||||
|
int lateSuccessCount = 0;
|
||||||
|
closingVm.RequestClose += result => { if (result.Result == ButtonResult.OK) lateSuccessCount++; };
|
||||||
|
Task closingRequest = closingVm.LoginCommand.Execute();
|
||||||
|
closingHost.Close();
|
||||||
|
await closingRequest;
|
||||||
|
Check(closeResult == ButtonResult.None && lateSuccessCount == 0 && closingPassword.Password.Length == 0, "关闭窗口取消登录且不触发迟到成功");
|
||||||
|
Check(!closingVm.CanEdit && !closingVm.LoginCommand.CanExecute(), "关闭后不可再次登录");
|
||||||
|
dialogs.Show(nameof(LoginWindow), new DialogParameters(), result => closeResult = result.Result);
|
||||||
|
((LoginViewModel)app.Windows.OfType<DialogWindow>().Single().DataContext).CancelCommand.Execute(null);
|
||||||
|
Check(closeResult == ButtonResult.Cancel, "登录取消返回 Prism Cancel");
|
||||||
var page = (FrameworkTestPage)((MainWindow)app.MainWindow).Content;
|
var page = (FrameworkTestPage)((MainWindow)app.MainWindow).Content;
|
||||||
var vm = (FrameworkTestPageViewModel)page.DataContext;
|
var vm = (FrameworkTestPageViewModel)page.DataContext;
|
||||||
vm.TestIocCommand.Execute();
|
vm.TestIocCommand.Execute();
|
||||||
@@ -60,4 +139,29 @@ class Program
|
|||||||
static void Check(bool success, string message) { if (!success) throw new Exception(message); }
|
static void Check(bool success, string message) { if (!success) throw new Exception(message); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class LoginHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
public int RequestCount { get; private set; }
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
RequestCount++;
|
||||||
|
if (request.RequestUri?.AbsoluteUri != "http://localhost:14312/api/v1/Auth/Login")
|
||||||
|
throw new Exception("登录路由错误");
|
||||||
|
var body = System.Text.Json.JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken));
|
||||||
|
if (body.RootElement.GetProperty("userName").GetString() != "admin")
|
||||||
|
throw new Exception("用户名未去除首尾空格");
|
||||||
|
await Task.Delay(30, cancellationToken);
|
||||||
|
var submittedPassword = body.RootElement.GetProperty("password").GetString()!;
|
||||||
|
if (!System.Text.RegularExpressions.Regex.IsMatch(submittedPassword, "^[0-9A-F]{32}$"))
|
||||||
|
throw new Exception("密码必须为 32 位大写 MD5");
|
||||||
|
var success = submittedPassword == "320BB1D43EC042096A83DBAC6DC9294A";
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(success
|
||||||
|
? "{\"code\":200,\"data\":{\"tokenType\":\"Bearer\",\"token\":\"test-token\",\"expiresInMinutes\":60}}"
|
||||||
|
: "{\"code\":400,\"msg\":\"用户名或密码错误\"}", Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user