Skip to content

WPF扩展控件

约 1332 字大约 4 分钟

WPF

2025-09-16

背景需求

在 WPF 开发中,经常需要在现有控件基础上添加新功能。本文以为 SearchBoxUserControl 添加状态查询功能为例,展示如何通过控件模板重写 + 附加属性的方式实现优雅的功能扩展。

核心技术方案

技术栈

  • 控件模板重写ControlTemplate 重新定义控件外观
  • 附加属性DependencyProperty.RegisterAttached 扩展现有控件
  • 模板绑定RelativeSource={RelativeSource TemplatedParent} 跨越模板边界

架构设计

原始控件 (SearchBoxUserControl)
    ↓ 通过附加属性扩展
扩展属性 (StatusQueryExtension)
    ↓ 通过控件模板重写
自定义UI布局 (ExQueryTemplate)

实现步骤

原控件

SearchBoxUserControl.xaml
<UserControl x:Class="MorphoScan.UiCore.Controls.SearchBoxUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:MorphoScan.UiCore.Controls"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             x:Name="root"
             d:DesignHeight="450"
             d:DesignWidth="800"
             mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <!--  关键词输入  -->
        <Label VerticalAlignment="Center" Content="关键词:" />
        <!--  Visibility="{Binding QueryConditions.ShowKeyword, ElementName=root, Converter={StaticResource BooleanToVisibilityConverter}}"  -->
        <TextBox x:Name="keywordTextBox"
                 Grid.Column="1"
                 Width="200"
                 Margin="5"
                 Text="{Binding Keyword, ElementName=root, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <!--  Visibility="{Binding QueryConditions.ShowKeyword, ElementName=root, Converter={StaticResource BooleanToVisibilityConverter}}"  -->

        <!--  查询类型选择  -->
        <Label Grid.Column="2"
               VerticalAlignment="Center"
               Content="类型:" />
        <!--  Visibility="{Binding QueryConditions.ShowQueryType, ElementName=root, Converter={StaticResource BooleanToVisibilityConverter}}"  -->
        <ComboBox x:Name="queryTypeComboBox"
                  Grid.Column="3"
                  Width="200"
                  Margin="5"
                  ItemsSource="{Binding QueryConditions.AvailableQueryTypes, ElementName=root}"
                  SelectedItem="{Binding QueryType, ElementName=root, Mode=TwoWay}" />
        <!--  Visibility="{Binding QueryConditions.ShowQueryType, ElementName=root, Converter={StaticResource BooleanToVisibilityConverter}}"  -->

        <!--  查询按钮  -->
        <Button Grid.Column="4"
                Width="120"
                Height="30"
                Margin="5"
                Command="{Binding QueryCommand, ElementName=root}"
                Content="查询" />
    </Grid>
</UserControl>

1. 扩展 - 定义附加属性

public static class ExQueryExtension
{
    // 可用状态列表
    public static readonly DependencyProperty AvailableStatusesProperty =
        DependencyProperty.RegisterAttached(
            "AvailableStatuses", typeof(List<string>), typeof(ExQueryExtension),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    // 选中状态
    public static readonly DependencyProperty SelectedStatusProperty =
        DependencyProperty.RegisterAttached(
            "SelectedStatus", typeof(string), typeof(ExQueryExtension),
            new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    // Get/Set 方法
    public static List<string> GetAvailableStatuses(DependencyObject obj) =>
        (List<string>)obj.GetValue(AvailableStatusesProperty);
    
    public static void SetAvailableStatuses(DependencyObject obj, List<string> value) =>
        obj.SetValue(AvailableStatusesProperty, value);
}

2. 创建控件模板

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:controls="clr-namespace:MorphoScan.UiCore.Controls;assembly=MorphoScan.UiCore"
                    xmlns:ext="clr-namespace:MorphoScan.UICustomModule.Extensions">
<ControlTemplate x:Key="ExQueryTemplate" TargetType="controls:SearchBoxUserControl">
    <Grid>
        <!-- 原有功能:关键词输入 -->
        <Label Content="关键词:" />
        <TextBox Text="{Binding Keyword, RelativeSource={RelativeSource TemplatedParent}}" />

        <!-- 原有功能:类型选择 -->
        <Label Content="类型:" />
        <ComboBox ItemsSource="{Binding QueryConditions.AvailableQueryTypes, RelativeSource={RelativeSource TemplatedParent}}"
                  SelectedItem="{Binding QueryType, RelativeSource={RelativeSource TemplatedParent}}" />

        <!-- 新增功能:状态查询 -->
        <Label Content="状态:" />
        <ComboBox ItemsSource="{Binding Path=(ext:ExQueryExtension.AvailableStatuses), RelativeSource={RelativeSource TemplatedParent}}"
                  SelectedItem="{Binding Path=(ext:ExQueryExtension.SelectedStatus), RelativeSource={RelativeSource TemplatedParent}}" />

        <!-- 查询按钮 -->
        <Button Content="查询" Command="{Binding QueryCommand, RelativeSource={RelativeSource TemplatedParent}}" />
    </Grid>
</ControlTemplate>
</ResourceDictionary>

3. 使用扩展控件

<!-- 引用资源字典 -->
<UserControl.Resources>
    <ResourceDictionary Source="/MorphoScan.UICustomModule;component/ExControls/ExQuery.xaml" />
</UserControl.Resources>

<!-- 使用扩展后的控件 -->
<controls:SearchBoxUserControl 
    ext:ExQueryExtension.AvailableStatuses="{Binding AvailableStatuses}"
    ext:ExQueryExtension.SelectedStatus="{Binding SelectedStatus}"
    Keyword="{Binding Keyword}"
    QueryCommand="{Binding QueryCommand}"
    QueryType="{Binding QueryType}"
    Template="{StaticResource ExQueryTemplate}" />

4. ViewModel 配置

public class ViewAViewModel : BindableBase
{
    public string Keyword { get; set; }
    public string QueryType { get; set; }
    public List<string> AvailableStatuses { get; set; }
    public string SelectedStatus { get; set; }
    public ICommand QueryCommand { get; }

    public ViewAViewModel()
    {
        AvailableStatuses = new() { "状态1", "状态2", "状态3" };
        QueryCommand = new DelegateCommand(() =>
        {
            // 处理查询逻辑,可以获取到所有查询条件
            Debug.WriteLine($"关键词: {Keyword}, 类型: {QueryType}, 状态: {SelectedStatus}");
        });
    }
}

关键技术点

1. RelativeSource TemplatedParent

<!-- 在控件模板中绑定到使用模板的控件实例 -->
Text="{Binding Keyword, RelativeSource={RelativeSource TemplatedParent}}"

作用:跨越模板边界,从模板内部访问控件属性

2. FrameworkPropertyMetadataOptions.BindsTwoWayByDefault

new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)

作用:设置附加属性默认为双向绑定,XAML 中可省略 Mode=TwoWay

3. 附加属性语法

<!-- 设置附加属性 -->
ext:ExQueryExtension.AvailableStatuses="{Binding AvailableStatuses}"

<!-- 绑定附加属性 -->
ItemsSource="{Binding Path=(ext:ExQueryExtension.AvailableStatuses), RelativeSource={RelativeSource TemplatedParent}}"

方案优势

✅ 无侵入性

  • 不修改原始控件代码
  • 保持原有功能完整性
  • 向后兼容

✅ 高扩展性

  • 可添加多个附加属性
  • 支持复杂数据绑定
  • 易于维护和测试

✅ 符合 WPF 设计理念

  • 利用依赖属性系统
  • 遵循 MVVM 模式
  • 支持数据绑定和样式化

✅ 代码复用性

  • 模板可在多处使用
  • 附加属性可应用于其他控件
  • 扩展逻辑独立封装

适用场景

  • ✅ 需要在现有控件基础上添加功能
  • ✅ 不能或不想修改原始控件源码
  • ✅ 需要保持向后兼容性
  • ✅ 扩展功能相对独立且稳定

总结

通过控件模板重写 + 附加属性的方式,实现了对现有控件的优雅扩展。这种方案保持了代码的简洁性,具备良好的可维护性和扩展性

对于简单的功能扩展需求,推荐使用此方案,避免过度设计。当扩展需求变得复杂时,再考虑引入更高级的抽象模式。