Files
Store.Management/.agents/rules/wpf-core.md
T
2026-10-27 19:01:56 +08:00

10 KiB
Raw Blame History

description, alwaysApply
description alwaysApply
WPF MVVM 核心行为约束(项目结构、命名、ViewModel、属性、命令、服务、DI、对话框、输入限长、Framework 基类等),始终生效 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 强相关,使用其中一个必须搭配另一个。

// 普通页面
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,把这些逻辑放进去(该方法由基类构造流程调用,时机等同于原构造函数体):

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:

// ✅ 正确
 private string _title;
 public string Title
 {
     get => _title;
     set => SetProperty(ref _title, value);
 }


Command 写法

使用 field ??= new ... 懒加载,handler 与定义共置,禁止分离到文件底部:

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 模板

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 在关窗前调用)
// 单例服务
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(见「初始化与主构造函数」),不要为此改用传统构造函数:

internal class XxxViewModel(IConfigService configService) : ViewModelBaseEx
{
    // 直接使用 configService
}

IDialogService


internal class XxxViewModel(IMessageBoxService messageBoxService) : ViewModelBaseEx
{
    private void ShowWarning()
    {
        messageBoxService.Show("操作成功", "提示", MessageButton.OK, MessageIcon.Information);
    }
}

复杂对话框使用 add-dialog skill 创建自定义 DialogService。

LoadingStatus

异步加载统一用 LoadingStatus,禁止自建 IsBusy/HasError:

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 服务:

<!-- XAML 注册 -->
<dxmvvm:Interaction.Behaviors>
    <dxmvvm:OpenFileDialogService Name="OpenFileDialog"/>
    <dxmvvm:SaveFileDialogService Name="SaveFileDialog"/>
</dxmvvm:Interaction.Behaviors>
// 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:

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)。

<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:

using PipeReviewReport.Shared;

var dto = model.Adapt<XxxDto>();
var model = dto.Adapt<XxxModel>();

资源引用(忽略)

var icon = ImageResources.Icon_XXX_PNG;
var text = AppStrings.Xxx_Title;
var color = CustomColors.AccentColor;