13 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"
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
<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强相关,必须搭配使用
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)。
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>
布局注释
主要布局分块/区域前加 <!-- --> 注释标明用途。复杂视图(多面板、多 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}"/>
枚举绑定
EnumDisplayConverter(XAML)
<UserControl.Resources>
<conv:EnumDisplayConverter x:Key="EnumDisplayConverter"/>
</UserControl.Resources>
<!-- 直接绑定枚举值 -->
<TextBlock Text="{Binding Status, Converter={StaticResource EnumDisplayConverter}}"/>
ComboBox 绑定枚举(推荐:ViewModel 提供列表)
// ViewModel
public IReadOnlyList<MyStatus> StatusList { get; } = Enum.GetValues(typeof(MyStatus)).Cast<MyStatus>().ToList();
public MyStatus SelectedStatus
{
get => GetProperty(() => SelectedStatus);
set => SetProperty(() => SelectedStatus, value);
}
<ComboBox ItemsSource="{Binding StatusList}"
SelectedItem="{Binding SelectedStatus}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ., Converter={StaticResource EnumDisplayConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
C# 获取枚举显示文本
string displayText = MyStatus.Active.GetEnumDisplay();
EnumDisplayService 内部实现和自定义逻辑详见 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}"/>