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

13 KiB
Raw Blame History

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} 绑定 ViewModel
  • d: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.Icon
  • GlyphIconExtension:直接返回 IconText 控件(文字图标,支持 Foreground 调色),用于 MenuItem.Icon
<MenuItem Header="HELLO" Icon="{f:ImageIcon Icon=Folder}"/>
<MenuItem Header="WORLD" Icon="{f:GlyphIcon Icon=GuestAdd, Foreground=Red}"/>