外观
WPF扩展控件
背景需求
在 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>SearchBoxUserControl.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MorphoScan.UiCore.Controls
{
/// <summary>
/// SearchBoxUserControl.xaml 的交互逻辑
/// </summary>
public partial class SearchBoxUserControl : UserControl
{
public SearchBoxUserControl()
{
InitializeComponent();
}
#region 关键字
public string Keyword
{
get => (string)GetValue(KeywordProperty);
set => SetValue(KeywordProperty, value);
}
public static readonly DependencyProperty KeywordProperty =
DependencyProperty.Register("Keyword", typeof(string), typeof(SearchBoxUserControl),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
#endregion
public string QueryType
{
get => (string)GetValue(QueryTypeProperty);
set => SetValue(QueryTypeProperty, value);
}
public static readonly DependencyProperty QueryTypeProperty =
DependencyProperty.Register("QueryType", typeof(string), typeof(SearchBoxUserControl),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public ICommand QueryCommand
{
get => (ICommand)GetValue(QueryCommandProperty);
set => SetValue(QueryCommandProperty, value);
}
public QueryConditions QueryConditions
{
get => (QueryConditions)GetValue(QueryConditionsProperty);
set => SetValue(QueryConditionsProperty, value);
}
public static readonly DependencyProperty QueryCommandProperty =
DependencyProperty.Register("QueryCommand", typeof(ICommand), typeof(SearchBoxUserControl),
new PropertyMetadata(null));
public static readonly DependencyProperty QueryConditionsProperty =
DependencyProperty.Register("QueryConditions", typeof(QueryConditions), typeof(SearchBoxUserControl),
new PropertyMetadata(new QueryConditions()));
}
public class QueryConditions
{
//public bool ShowKeyword { get; set; } = true;
//public bool ShowQueryType { get; set; } = true;
public List<string> AvailableQueryTypes { get; set; } = new List<string> { "一类", "二类" };
}
}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 模式
- 支持数据绑定和样式化
✅ 代码复用性
- 模板可在多处使用
- 附加属性可应用于其他控件
- 扩展逻辑独立封装
适用场景
- ✅ 需要在现有控件基础上添加功能
- ✅ 不能或不想修改原始控件源码
- ✅ 需要保持向后兼容性
- ✅ 扩展功能相对独立且稳定
总结
通过控件模板重写 + 附加属性的方式,实现了对现有控件的优雅扩展。这种方案保持了代码的简洁性,具备良好的可维护性和扩展性
对于简单的功能扩展需求,推荐使用此方案,避免过度设计。当扩展需求变得复杂时,再考虑引入更高级的抽象模式。
