HandyControl中的依赖属性:自定义控件开发基础

HandyControl中的依赖属性:自定义控件开发基础

【免费下载链接】HandyControl Contains some simple and commonly used WPF controls 【免费下载链接】HandyControl 项目地址: https://gitcode.com/gh_mirrors/ha/HandyControl

1. 依赖属性(Dependency Property)概述

依赖属性是WPF(Windows Presentation Foundation)中一种特殊的属性系统,它允许属性值通过绑定(Binding)、样式(Style)、模板(Template)和动画(Animation)等方式动态获取,同时支持属性值继承和资源引用。在HandyControl框架中,依赖属性是构建灵活、可定制控件的核心技术。

1.1 依赖属性 vs 普通CLR属性

特性依赖属性普通CLR属性
存储方式由DependencyObject管理,共享存储私有字段存储
值解析支持从多个来源(样式、模板、绑定等)解析直接返回字段值
变更通知内置INotifyPropertyChanged支持需手动实现通知接口
样式与模板支持可在XAML中通过样式设置仅支持代码中修改
默认值可指定默认值回调固定初始值

1.2 依赖属性的核心优势

  • 属性值继承:子元素可继承父元素的属性值(如FontSize)
  • 动态值解析:运行时根据优先级自动解析最终值
  • 资源引用:支持静态/动态资源引用({StaticResource}、{DynamicResource})
  • 数据绑定:支持单向/双向绑定(OneWay/TwoWay)
  • 动画支持:可直接作为动画目标属性

2. HandyControl中依赖属性的实现模式

HandyControl框架中的控件依赖属性通常遵循以下实现模式,以确保代码一致性和可维护性:

2.1 依赖属性定义规范

public class CustomControl : Control
{
    // 1. 声明静态依赖属性标识符
    public static readonly DependencyProperty IsEnabledProperty = 
        DependencyProperty.Register(
            "IsEnabled",                  // 属性名称
            typeof(bool),                 // 属性类型
            typeof(CustomControl),        // 拥有者类型
            new PropertyMetadata(         // 属性元数据
                true,                     // 默认值
                OnIsEnabledChanged,       // 属性变更回调
                CoerceIsEnabledValue      // 值强制转换回调
            )
        );

    // 2. 包装为CLR属性(可选但推荐)
    public bool IsEnabled
    {
        get => (bool)GetValue(IsEnabledProperty);
        set => SetValue(IsEnabledProperty, value);
    }

    // 3. 属性变更回调(可选)
    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (CustomControl)d;
        // 处理属性值变更逻辑
        control.UpdateVisualState((bool)e.NewValue);
    }

    // 4. 值强制转换回调(可选)
    private static object CoerceIsEnabledValue(DependencyObject d, object baseValue)
    {
        var control = (CustomControl)d;
        // 强制值范围或依赖关系处理
        return control.IsReadOnly ? false : baseValue;
    }
}

2.2 依赖属性元数据(PropertyMetadata)配置

HandyControl中常用的元数据选项包括:

// 基础元数据配置
new PropertyMetadata(
    defaultValue: false,  // 默认值
    propertyChangedCallback: OnValueChanged,  // 值变更回调
    coerceValueCallback: CoerceValue,  // 值修正回调
    isAnimationProhibited: false  // 是否禁止动画
)

// 框架元素专用元数据
new FrameworkPropertyMetadata(
    defaultValue: Brushes.Transparent,
    flags: FrameworkPropertyMetadataOptions.AffectsRender | 
           FrameworkPropertyMetadataOptions.Inherits,  // 属性标志
    propertyChangedCallback: OnBackgroundChanged
)

常用的FrameworkPropertyMetadataOptions标志:

  • AffectsRender:属性变更时触发重绘
  • AffectsMeasure:属性变更时触发布局测量
  • AffectsArrange:属性变更时触发布局排列
  • Inherits:启用属性值继承
  • NotDataBindable:禁止数据绑定

3. HandyControl控件中的依赖属性实战分析

3.1 文本框控件(TextBox)依赖属性案例

HandyControl的TextBox控件扩展了原生WPF TextBox,增加了Watermark(水印)依赖属性:

public class TextBox : System.Windows.Controls.TextBox
{
    // 水印文本依赖属性
    public static readonly DependencyProperty WatermarkProperty = 
        DependencyProperty.Register(
            "Watermark", 
            typeof(object), 
            typeof(TextBox), 
            new FrameworkPropertyMetadata(
                null, 
                FrameworkPropertyMetadataOptions.AffectsRender  // 影响渲染
            )
        );

    // 水印模板依赖属性
    public static readonly DependencyProperty WatermarkTemplateProperty = 
        DependencyProperty.Register(
            "WatermarkTemplate", 
            typeof(DataTemplate), 
            typeof(TextBox),
            new FrameworkPropertyMetadata(
                null, 
                FrameworkPropertyMetadataOptions.AffectsRender
            )
        );

    public object Watermark
    {
        get => GetValue(WatermarkProperty);
        set => SetValue(WatermarkProperty, value);
    }

    public DataTemplate WatermarkTemplate
    {
        get => (DataTemplate)GetValue(WatermarkTemplateProperty);
        set => SetValue(WatermarkTemplateProperty, value);
    }

    // 重写OnRender方法绘制水印
    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        
        // 当文本为空且控件未获得焦点时绘制水印
        if (string.IsNullOrEmpty(Text) && !IsFocused && Watermark != null)
        {
            // 水印绘制逻辑
            var visual = new ContentPresenter
            {
                Content = Watermark,
                ContentTemplate = WatermarkTemplate,
                Foreground = Brushes.Gray
            };
            
            visual.Measure(RenderSize);
            visual.Arrange(new Rect(RenderSize));
            visual.ApplyTemplate();
            
            drawingContext.DrawVisual(visual, new TranslateTransform(5, 2));
        }
    }
}

3.2 按钮控件(Button)依赖属性案例

HandyControl的Button控件通过依赖属性实现了多种视觉状态控制:

public class Button : System.Windows.Controls.Button
{
    // 按钮类型依赖属性
    public static readonly DependencyProperty ButtonTypeProperty = 
        DependencyProperty.Register(
            "ButtonType", 
            typeof(ButtonType), 
            typeof(Button), 
            new PropertyMetadata(
                ButtonType.Default,
                OnButtonTypeChanged  // 类型变更时更新样式
            )
        );

    // 加载状态依赖属性
    public static readonly DependencyProperty IsLoadingProperty = 
        DependencyProperty.Register(
            "IsLoading", 
            typeof(bool), 
            typeof(Button), 
            new PropertyMetadata(
                false,
                OnIsLoadingChanged  // 加载状态变更时显示加载动画
            )
        );

    public ButtonType ButtonType
    {
        get => (ButtonType)GetValue(ButtonTypeProperty);
        set => SetValue(ButtonTypeProperty, value);
    }

    public bool IsLoading
    {
        get => (bool)GetValue(IsLoadingProperty);
        set => SetValue(IsLoadingProperty, value);
    }

    private static void OnButtonTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var button = (Button)d;
        button.UpdateButtonStyle((ButtonType)e.NewValue);
    }

    private static void OnIsLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var button = (Button)d;
        button.IsEnabled = !(bool)e.NewValue;  // 加载时禁用按钮
        button.ShowLoadingIndicator((bool)e.NewValue);
    }
}

// 按钮类型枚举
public enum ButtonType
{
    Default,
    Primary,
    Success,
    Info,
    Warning,
    Danger,
    Light,
    Dark,
    Link
}

4. 自定义依赖属性的完整实现流程

4.1 开发流程概览

mermaid

4.2 自定义依赖属性示例:带清除按钮的搜索框

以下是一个完整的自定义依赖属性实现示例,创建一个带清除按钮的搜索框控件:

public class SearchTextBox : TextBox
{
    // 搜索文本依赖属性
    public static readonly DependencyProperty SearchTextProperty = 
        DependencyProperty.Register(
            "SearchText", 
            typeof(string), 
            typeof(SearchTextBox), 
            new FrameworkPropertyMetadata(
                string.Empty,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,  // 默认双向绑定
                OnSearchTextChanged
            )
        );

    // 清除按钮可见性依赖属性
    public static readonly DependencyProperty ClearButtonVisibilityProperty = 
        DependencyProperty.Register(
            "ClearButtonVisibility", 
            typeof(Visibility), 
            typeof(SearchTextBox), 
            new FrameworkPropertyMetadata(
                Visibility.Collapsed,
                FrameworkPropertyMetadataOptions.AffectsRender
            )
        );

    public string SearchText
    {
        get => (string)GetValue(SearchTextProperty);
        set => SetValue(SearchTextProperty, value);
    }

    public Visibility ClearButtonVisibility
    {
        get => (Visibility)GetValue(ClearButtonVisibilityProperty);
        set => SetValue(ClearButtonVisibilityProperty, value);
    }

    static SearchTextBox()
    {
        // 重写默认样式
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(SearchTextBox), 
            new FrameworkPropertyMetadata(typeof(SearchTextBox))
        );
    }

    private static void OnSearchTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var searchBox = (SearchTextBox)d;
        var newText = (string)e.NewValue;
        
        // 根据文本是否为空更新清除按钮可见性
        searchBox.ClearButtonVisibility = string.IsNullOrEmpty(newText) 
            ? Visibility.Collapsed 
            : Visibility.Visible;
        
        // 触发搜索事件
        searchBox.OnSearchTextChanged(newText);
    }

    // 清除按钮点击处理
    public void ClearSearchText()
    {
        SearchText = string.Empty;
        Focus();  // 清除后重新获取焦点
    }

    // 搜索文本变更事件
    public event EventHandler<string> SearchTextChanged;

    protected virtual void OnSearchTextChanged(string text)
    {
        SearchTextChanged?.Invoke(this, text);
    }
}

对应的XAML样式模板(Generic.xaml):

<Style TargetType="{x:Type local:SearchTextBox}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:SearchTextBox}">
                <Border 
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    CornerRadius="4">
                    <Grid>
                        <!-- 文本框 -->
                        <ScrollViewer x:Name="PART_ContentHost" 
                                      Margin="8,0,32,0"/>
                        
                        <!-- 清除按钮 -->
                        <Button x:Name="PART_ClearButton"
                                Width="24"
                                Height="24"
                                Margin="0,0,4,0"
                                HorizontalAlignment="Right"
                                VerticalAlignment="Center"
                                Visibility="{TemplateBinding ClearButtonVisibility}"
                                Command="{x:Static local:SearchTextBox.ClearCommand}">
                            <Path Width="12" Height="12" 
                                  Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
                                  Fill="{TemplateBinding Foreground}"/>
                        </Button>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

5. 依赖属性高级应用技巧

5.1 属性值 coercion(强制转换)

coercion机制允许在设置属性值时对值进行修正,确保其符合控件的业务规则:

// 数值范围限制示例
public static readonly DependencyProperty ValueProperty = 
    DependencyProperty.Register(
        "Value", 
        typeof(double), 
        typeof(NumericUpDown), 
        new PropertyMetadata(
            0.0,
            null,
            CoerceValue  // 强制转换回调
        )
    );

private static object CoerceValue(DependencyObject d, object baseValue)
{
    var control = (NumericUpDown)d;
    double value = (double)baseValue;
    
    // 确保值在Min和Max之间
    if (value < control.Minimum)
        return control.Minimum;
    if (value > control.Maximum)
        return control.Maximum;
    
    return value;
}

5.2 附加属性(Attached Property)

附加属性是一种特殊的依赖属性,允许将属性附加到其他对象上。HandyControl中的布局控件广泛使用了附加属性:

public class GridHelper
{
    // 行跨度附加属性
    public static readonly DependencyProperty RowSpanProperty = 
        DependencyProperty.RegisterAttached(
            "RowSpan", 
            typeof(int), 
            typeof(GridHelper),
            new PropertyMetadata(1, OnRowSpanChanged)
        );

    public static void SetRowSpan(UIElement element, int value)
    {
        element.SetValue(RowSpanProperty, value);
    }

    public static int GetRowSpan(UIElement element)
    {
        return (int)element.GetValue(RowSpanProperty);
    }

    private static void OnRowSpanChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is UIElement element)
        {
            Grid.SetRowSpan(element, (int)e.NewValue);
        }
    }
}

XAML中使用附加属性:

<Grid>
    <Button local:GridHelper.RowSpan="2" Content="跨两行显示"/>
</Grid>

5.3 依赖属性的元数据重写

当继承控件并需要修改依赖属性的行为时,可以重写其元数据:

public class CustomButton : Button
{
    static CustomButton()
    {
        // 重写ButtonType属性的元数据
        ButtonTypeProperty.OverrideMetadata(
            typeof(CustomButton),
            new PropertyMetadata(
                ButtonType.Primary,  // 新的默认值
                OnButtonTypeChanged  // 新的变更回调
            )
        );
    }

    private static new void OnButtonTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // 自定义变更逻辑
    }
}

6. 依赖属性调试与性能优化

6.1 依赖属性值调试

在开发过程中,可以使用DependencyPropertyHelper类调试依赖属性值来源:

// 获取属性值来源信息
var valueSource = DependencyPropertyHelper.GetValueSource(this, ButtonTypeProperty);

// 输出值来源
Debug.WriteLine($"属性值来源: {valueSource.BaseValueSource}");

常见的属性值来源(BaseValueSource):

  • Default:默认值
  • Inherited:继承自父元素
  • Style:来自样式
  • Template:来自控件模板
  • Binding:来自数据绑定
  • Local:本地设置值

6.2 性能优化建议

  1. 合理设置元数据标志:仅在属性确实影响布局时才设置AffectsMeasureAffectsArrange标志

  2. 使用属性值验证:通过ValidateValueCallback验证属性值,避免无效值导致的性能问题

public static readonly DependencyProperty ValueProperty = 
    DependencyProperty.Register(
        "Value",
        typeof(double),
        typeof(NumericControl),
        new PropertyMetadata(0.0),
        ValidateValue  // 值验证回调
    );

private static bool ValidateValue(object value)
{
    double val = (double)value;
    return !double.IsNaN(val) && !double.IsInfinity(val);
}
  1. 避免过度使用依赖属性:对于简单的值存储,普通CLR属性性能更优

  2. 使用冻结(Freezable)对象:对于画笔、变换等静态资源,调用Freeze()方法提高性能

7. 总结与最佳实践

7.1 依赖属性使用场景

  • 控件需要支持样式、模板或数据绑定
  • 属性值需要从多个来源动态解析
  • 需要属性变更通知
  • 需要支持动画或故事板
  • 实现自定义布局行为

7.2 最佳实践清单

  • 命名规范:属性标识符命名为PropertyNameProperty
  • 元数据配置:始终显式指定属性的拥有者类型
  • CLR包装:为依赖属性提供CLR包装器,便于代码访问
  • 变更回调:保持变更回调方法简洁高效
  • 默认值:选择合理的默认值,减少初始化开销
  • 类型安全:确保依赖属性的类型与CLR包装器一致
  • 元数据重写:继承控件时通过重写元数据而非重新定义属性

7.3 常见陷阱与解决方案

问题解决方案
属性值不更新确保设置了正确的元数据标志(如AffectsRender)
绑定不生效检查是否设置了BindsTwoWayByDefault标志
内存泄漏避免在变更回调中创建强引用循环
性能问题减少不必要的属性变更通知,使用缓存机制

通过掌握HandyControl中的依赖属性实现方式,开发者可以构建出高度可定制、性能优良的WPF控件,为用户提供丰富的界面交互体验。依赖属性作为WPF的核心技术,是HandyControl框架灵活性和可扩展性的基础,深入理解其原理和实践对于自定义控件开发至关重要。

【免费下载链接】HandyControl Contains some simple and commonly used WPF controls 【免费下载链接】HandyControl 项目地址: https://gitcode.com/gh_mirrors/ha/HandyControl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值