16 KiB
description, alwaysApply, globs
| description | alwaysApply | globs |
|---|---|---|
| XAML 视图编写约定(头部模板、数据绑定、输入限长、BindingProxy、DRY、颜色、布局注释、枚举绑定等),编辑 .xaml 文件时生效 | false | **/*.xaml |
XAML 视图编写约定
View XAML 头部模板
<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"
Style="{DynamicResource UserControlStyle}"
DataContext="{core:DISource Type=local:XxxViewModel}"
d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"
d:Background="White"
d:DesignHeight="450"
d:DesignWidth="800">
Dialog View XAML
<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}绑定 ViewModeld:DataContext="{d:DesignInstance Type=local:XxxViewModel}"提供设计时数据UserControlStyle与ViewModelBaseEx强相关,必须搭配使用
控件必须有设计数据
例如 TextBlock > d:Text="TextBlock" ComboBox > d:ItemsSource="{d:SampleData ItemCount=5}"
BindingProxy + CommandProxy(DataTemplate/ContextMenu 绑定)
在 DataTemplate、ContextMenu、DataGridTemplateColumn.CellTemplate 中禁止使用 RelativeSource AncestorType,必须用 BindingProxy + CommandProxy。
UserControl.Resources 注册
<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)。
文字只读显示
尽量使用 TextBlock 而不是 Label
DataTemplate(CellTemplate)中
Command 用 CommandProxy,行数据用 {Binding}:
<Button Command="{StaticResource ToggleCurveCommand}" CommandParameter="{Binding}"/>
ContextMenu 中
Command 用 CommandProxy,CommandParameter 用 BindingProxy 取 ViewModel 属性:
<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
<!-- 行右键 -->
<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>
禁止写法
<!-- ❌ 禁止 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 的「配色覆盖机制」:
<!-- ✅ 走主题资源键,改配色只需改一处 -->
<Border Background="{DynamicResource RegionBrush}" BorderBrush="{DynamicResource BorderBrush}"/>
<TextBlock Foreground="{DynamicResource SecondaryTextBrush}"/>
<Border Background="{DynamicResource TitleBarBrush}"/>
<!-- ❌ 硬编码主题色,改配色要满仓库搜替换 -->
<Border Background="#c8ddee"/>
确实需要写死色值时(一次性的装饰、遮罩等),用十六进制字符串,
禁止 Color.FromRgb()/Color.FromArgb():
<!-- ✅ -->
<Rectangle Fill="#80000000"/>
<!-- AA 为 0xFF 时可省略 -->
<Border Background="#FF5733"/>
C# 侧:"#FF5733".toColor()、"#80000000".toColor()。
绑定模式(Mode)与更新触发(UpdateSourceTrigger)
双向绑定:必须加 UpdateSourceTrigger=PropertyChanged
默认情况下 WPF 双向绑定在控件失去焦点时才更新源属性,这会导致 ViewModel 属性值与 UI 不同步。所有双向绑定的属性必须在 Binding 中明确添加 UpdateSourceTrigger=PropertyChanged,确保输入内容即时同步:
<!-- ✅ 双向绑定加 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,避免双向绑定产生的警告和不必要开销:
<!-- ✅ IsReadOnly 控件加 Mode=OneWay -->
<TextBox Text="{Binding StatusText, Mode=OneWay}" IsReadOnly="True"/>
<TextBlock Text="{Binding Description, Mode=OneWay}"/>
Run 绑定:强制 OneWay
<TextBlock> 的 <Run> 绑定时必须加 Mode=OneWay,否则会引发警告:
<!-- ✅ 正确: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。
<!-- ✅ 路径片段 -->
<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 的复杂场景
<!-- ✅ 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>
顺序排列与间距布局
页面排版需要横向、纵向顺序排列或简单自动换行时,优先使用 wpf:SpacingStackPanel。 普通堆叠布局通过 Orientation 指定方向,需要换行时设置 ChildWrapping="True"。
- 优先用
HorizontalSpacing/VerticalSpacing统一控制子项间距,避免逐项设置重复的Margin;容器外边距和个别元素的特殊偏移仍按需使用Margin。 - 需要统一子项尺寸或对齐时,按需设置
ItemWidth、ItemHeight、ItemHorizontalAlignment、ItemVerticalAlignment。 - “文字 + 简单内容”可由
wpf:TitleContent组成,再用wpf:SpacingStackPanel排列多个字段。 - 需要行列划分、跨行跨列、重叠或虚拟化时,使用满足这些要求的布局容器,不强行替换为
SpacingStackPanel。 - 属性及用法见 max-common-wpf SpacingStackPanel 参考。
文字与简单内容布局
页面排版遇到“说明文字 + 简单内容”(如输入框、选择器、按钮等输入或点击控件)时,优先使用 wpf:TitleContent。 将说明文字放在 Title,交互控件作为内容,避免为同一组合另用 TextBlock 配合 Grid / StackPanel 手工拼接。
- 按布局设置
Placement;左侧标题表单优先使用Placement="Left",并按需设置TitleWidth、TitleVerticalAlignment="Center"和LeftTitlePadding,保持同组字段对齐。 - 内容控件仍自行配置绑定、命令、输入长度和可访问名称;
TitleContent只负责标题与内容的布局。 - 属性及用法见 max-common-wpf TitleContent 参考。
布局注释
主要布局分块/区域前加 <!-- --> 注释标明用途。复杂视图(多面板、多 Tab、对话框)必须注释;简单单控件视图可省略:
<Grid>
<!-- 顶部工具栏 -->
<Border Grid.Row="0"> ... </Border>
<!-- 左侧导航树 -->
<TreeView Grid.Column="0"/>
<!-- 主内容区 -->
<ContentControl Grid.Column="1" Content="{Binding CurrentView}"/>
<!-- 底部状态栏 -->
<StatusBar Grid.Row="2"/>
</Grid>
LoadingCover / LoadingStatusIndicator
<!-- 在内容 Grid 之上放置 LoadingCover 作为覆盖层 -->
<Grid>
<ContentControl Content="{Binding ...}"/>
<f:LoadingCover LoadingStatus="{Binding LoadingStatus}" RefreshCommand="{Binding LoadCommand}"/>
</Grid>
LoadingCover 三态:
- 加载中:半透明遮罩 + 环形进度 + Message 文字
- 错误:半透明遮罩 + 警告图标 + ErrorMessage + 刷新按钮 + 复制错误按钮
- 成功:完全隐藏
轻量级内联状态:
<f:LoadingStatusIndicator LoadingStatus="{Binding LoadingStatus}"/>
枚举绑定
页面的枚举选项绑定优先使用 EnumBindingMarkup(EnumBindingMarkupExtension)生成 ItemsSource,配合 EnumBindingExtension.SelectedEnum 绑定选中的枚举值。 不要仅为列出枚举值,在 ViewModel 中额外定义 Enum.GetValues 列表或在 XAML 中硬编码选项。
- 显示名优先使用 MAX.Common.WPF 自带的
EnumDisplayConverter,显式传入标记扩展的Converter;未传入时默认显示枚举成员名称。 DisplayMemberPath="Display"用于显示名称,SelectedValuePath="Value"对应枚举值本身。- 系统初始化、视图加载前配置一次
EnumDisplayConverter.EnumDisplayManager = new EnumDisplayManagerWPF(null);;已有自定义显示管理器时复用现有配置。 - 仅显示单个枚举值的文本时,直接绑定该属性并使用
EnumDisplayConverter,无需生成选项列表。
ComboBox 绑定枚举
xmlns:wpf="MAX.WPF",xmlns:local 指向业务枚举命名空间:
<UserControl.Resources>
<wpf:EnumDisplayConverter x:Key="EnumDisplayConverter" />
</UserControl.Resources>
<ComboBox
ItemsSource="{wpf:EnumBindingMarkup EnumType={x:Type local:MyStatus},
Converter={StaticResource EnumDisplayConverter}}"
DisplayMemberPath="Display"
SelectedValuePath="Value"
wpf:EnumBindingExtension.SelectedEnum="{Binding SelectedStatus, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<!-- 只读显示单个枚举值 -->
<TextBlock Text="{Binding Status, Mode=OneWay, Converter={StaticResource EnumDisplayConverter}}" />
初始化命名空间、Description 显示、空选项等完整用法见 max-common-wpf 枚举绑定参考。自定义枚举显示管理器见 reference-enum-display skill。
MarkupExtension 示例
IconEnumExtension(返回 ImageSource)
[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();
}
}
<Image Source="{f:IconEnumExtension Icon=Check1}" Width="20" Height="20"/>
MenuItem.Icon 专用扩展(详见 icon-usage 规则)
ImageIconExtension:直接返回Image控件(彩色矢量图),用于MenuItem.IconGlyphIconExtension:直接返回IconText控件(文字图标,支持 Foreground 调色),用于MenuItem.Icon
<MenuItem Header="HELLO" Icon="{f:ImageIcon Icon=Folder}"/>
<MenuItem Header="WORLD" Icon="{f:GlyphIcon Icon=GuestAdd, Foreground=Red}"/>