diff --git a/.agents/rules/architecture.md b/.agents/rules/architecture.md new file mode 100644 index 0000000..cd475fc --- /dev/null +++ b/.agents/rules/architecture.md @@ -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`;读取路径不应触发不必要的文件创建。 +- 当前 `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 是框架示例,不证明商品业务已接入。新增商品字段、上架状态或权限须依据具体需求与服务端契约。 diff --git a/.agents/rules/controls-reference.md b/.agents/rules/controls-reference.md new file mode 100644 index 0000000..4488077 --- /dev/null +++ b/.agents/rules/controls-reference.md @@ -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,维护依赖方向,验证默认值、资源和生命周期。 diff --git a/.agents/rules/csharp-syntax.md b/.agents/rules/csharp-syntax.md new file mode 100644 index 0000000..4d5103b --- /dev/null +++ b/.agents/rules/csharp-syntax.md @@ -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 改写无关代码。 diff --git a/.agents/rules/editor-conventions.md b/.agents/rules/editor-conventions.md new file mode 100644 index 0000000..574f656 --- /dev/null +++ b/.agents/rules/editor-conventions.md @@ -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**。 \ No newline at end of file diff --git a/.agents/rules/handycontrol-usage.md b/.agents/rules/handycontrol-usage.md new file mode 100644 index 0000000..10e89dd --- /dev/null +++ b/.agents/rules/handycontrol-usage.md @@ -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。 diff --git a/.agents/rules/icon-usage.md b/.agents/rules/icon-usage.md new file mode 100644 index 0000000..3b4b070 --- /dev/null +++ b/.agents/rules/icon-usage.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;不为规则迁移新增图标依赖。 diff --git a/.agents/rules/max-libraries-reference.md b/.agents/rules/max-libraries-reference.md new file mode 100644 index 0000000..36be474 --- /dev/null +++ b/.agents/rules/max-libraries-reference.md @@ -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 +``` diff --git a/.agents/rules/wpf-core.md b/.agents/rules/wpf-core.md new file mode 100644 index 0000000..7fe45a3 --- /dev/null +++ b/.agents/rules/wpf-core.md @@ -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` | + +`UserControlStyle` 与 `ViewModelBaseEx` 强相关,使用其中一个必须搭配另一个。 + +```csharp +// 普通页面 +internal class XxxViewModel : ViewModelBaseEx +{ + protected override void LoadedOnce() + { + // 首次加载初始化 + } +} + +// 对话框 +internal class XxxDialogViewModel : DialogViewModelBase +{ + 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 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 中用 `` 绑定。API 详情见 `reference-loading-status` skill。 + +## 表格控件选择 + +| 场景 | 控件 | +|---|---| +| 普通配置表格、数据库记录 | 常规 DataGrid | +| 百万行级别大数据 | MAX.FastWpfGrid + FastGridModelBase | + +MAX.FastWpfGrid 完整用法见 `reference-fast-wpf-grid` skill(**禁止**旧包名 `FastWpfGrid`)。 + +## 文件对话框服务 + +**禁止** `Microsoft.Win32.OpenFileDialog` / `SaveFileDialog`,用 DevExpress 服务: + +```xml + + + + + +``` + +```csharp +// ViewModel 使用 +public IOpenFileDialogService OpenFileDialog => GetService(nameof(OpenFileDialog)); +public ISaveFileDialogService SaveFileDialog => GetService(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 + + +``` + +XAML 细则见 `wpf-xaml.mdc`。 + +## 数据映射(忽略) + +使用 Mapster: + +```csharp +using PipeReviewReport.Shared; + +var dto = model.Adapt(); +var model = dto.Adapt(); +``` + +## 资源引用(忽略) + +```csharp +var icon = ImageResources.Icon_XXX_PNG; +var text = AppStrings.Xxx_Title; +var color = CustomColors.AccentColor; +``` diff --git a/.agents/rules/wpf-xaml.md b/.agents/rules/wpf-xaml.md new file mode 100644 index 0000000..a12b8df --- /dev/null +++ b/.agents/rules/wpf-xaml.md @@ -0,0 +1,381 @@ +--- +description: XAML 视图编写约定(头部模板、数据绑定、输入限长、BindingProxy、DRY、颜色、布局注释、枚举绑定等),编辑 .xaml 文件时生效 +alwaysApply: false +globs: "**/*.xaml" +--- + +# XAML 视图编写约定 + +## View XAML 头部模板 + +```XML + +``` + +## Dialog View XAML + +```XML + +``` + +- `{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 + + + + + + + +``` + +**`BindingProxy` 映射关系**:`Data="{Binding}"` 把 ViewModel 存到 `BindingProxy.Data`。引用时 `Path=Data.SelectedItem` 中 `Data` 对应 `BindingProxy.Data`(即 ViewModel)。 + +### DataTemplate(CellTemplate)中 + +Command 用 CommandProxy,行数据用 `{Binding}`: + +```xml +