依赖属性和附加属性

依赖属性是扩展对象自身的属性功能,比如说是数据绑定、动画,属性是属于定义它的类的,最经典的场景是Button的background。依赖属性的主要作用是提供一种比普通的 .NET 属性更强大的内存管理和功能支持。

  • 数据绑定: 只有依赖属性才能作为 Data Binding 的目标。
  • 样式与模板: 支持通过 Style 或 ControlTemplate 进行统一修改。
  • 属性值优先级: 它可以根据优先级(如动画 > 本地值 > 样式 > 默认值)自动计算最终结果。
  • 资源节约: 如果属性没有被赋值,它不会占用实例内存,而是指向默认值。

示例: Button 的 Content 属性。按钮自己拥有这个属性,并负责显示它。

怎样自己定义一个依赖属性呢?C#属性的定义大家再熟悉不过了:

// 1. 使类型继承DependencyObject类
    public class Person : DependencyObject
    {
        // 2. 声明一个静态只读的DependencyProperty 字段
        public static readonly DependencyProperty nameProperty;
       
        static Person()
        {
            // 3. 注册定义的依赖属性
            nameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Person), 
                new PropertyMetadata("Learning Hard",OnValueChanged)); 
        }

        // 4. 属性包装器,通过它来读取和设置我们刚才注册的依赖属性
        public string Name
        {
            get { return (string)GetValue(nameProperty); }
            set { SetValue(nameProperty, value); }
        }

        private static void OnValueChanged(DependencyObject dpobj, DependencyPropertyChangedEventArgs e)
        {
            // 当只发生改变时回调的方法
        }

    }

依赖属性的优先级

WPF允许在多个地方设置依赖属性的值,则自然就涉及到依赖属性获取值的优先级问题。例如下面XMAL代码,我们在三个地方设置了按钮的背景颜色,那最终按钮会读取那个设置的值呢?是Green、Yellow还是Red?

<Window x:Class="DPSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Button x:Name="myButton" Background="Green" Width="100" Height="30">
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="Background" Value="Yellow"/>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="Red" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
            Click Me 
        </Button>
    </Grid>
</Window>

上面按钮的背景颜色是Green。之所以背景色是Green,是因为WPF每访问一个依赖属性,它都会按照下面的顺序由高到底处理该值。具体优先级如下图所示:

![[Pasted image 20251213045459.png]]

按钮的本地值设置的是Green,自定义Style Trigger设置的为Red,自定义的Style Setter设置的为Yellow,由于这里的本地值的优先级最高,所以按钮的背景色或者的是Green值。如果此时把本地值Green去掉的话,此时按钮的背景颜色是Yellow而不是Red。这里尽管Style Trigger的优先级比Style Setter高,但是由于此时Style Trigger的IsMouseOver属性为false,即鼠标没有移到按钮上,一旦鼠标移到按钮上时,此时按钮的颜色就为Red。此时才会体现出Style Trigger的优先级比Style Setter优先级高。所以上图中优先级是比较理想情况下,很多时候还需要具体分析。

依赖属性的继承

依赖属性是可以被继承的,即父元素的相关设置会自动传递给所有的子元素。下面代码演示了依赖属性的继承。

<Window x:Class="Custom_DPInherited.DPInherited"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
      FontSize="18"
      Title="依赖属性的继承">
    <StackPanel >
        <Label Content="继承自Window的FontSize" />
        <Label Content="显式设置FontSize" 
               TextElement.FontSize="36"/>
        <StatusBar>Statusbar没有继承自Window的FontSize</StatusBar>
    </StackPanel>
</Window>

![[Pasted image 20251213045731.png]]

在上面XAML代码中。Window.FontSize设置会影响所有内部子元素字体大小,这就是依赖属性的继承。如第一个Label没有定义FontSize,所以它继承了Window.FontSize值。但一旦子元素提供了显式设置,这种继承就会被打断,所以Window.FontSize值对于第二个Label不再起作用。

适用场景: - 自定义控件的属性(如 Button.Content、TextBox.Text)。 - 需要支持数据绑定、动画、样式等 WPF 功能的属性。

附加属性(Attached Property)

用途​​:​​为其他控件(尤其是无法修改源码的控件)扩展功能​​(如 Grid、Canvas)中为子控件定义额外的属性。通过静态方法跨类附加属性,解耦功能与控件。

public static readonly DependencyProperty MyAttachedPropertyProperty =
    DependencyProperty.RegisterAttached(
        "MyAttachedProperty",        // 属性名
        typeof(int),                 // 属性类型
        typeof(OwnerClass),          // 定义该属性的类
        new PropertyMetadata(0));    // 默认值

访问方式:

// Getter
public static int GetMyAttachedProperty(DependencyObject obj)
{
    return (int)obj.GetValue(MyAttachedPropertyProperty);
}

// Setter
public static void SetMyAttachedProperty(DependencyObject obj, int value)
{
    obj.SetValue(MyAttachedPropertyProperty, value);
}
  1. 适用场景:
    • 布局控件的子元素属性(如 Grid.Row、Canvas.Left)。
    • 扩展已有控件的功能(如 ToolTipService.ToolTip)。

关键区别

![[Pasted image 20251213181131.png]]

时钟控制法

这个方法的原理核心可以总结为:“借鸡生蛋”(借用动画系统的计算能力,手动执行属性赋值)。

通常你写 <DoubleAnimation Storyboard.TargetProperty="Width" .../> 时,WPF 内部发生的事情是:

  1. 计算: 动画系统根据时间(0s -> 1s)和缓动函数(Easing),算出一个当前的数值(比如 100 -> 150)。
  2. 赋值: 动画系统直接把这个 double 数值赋给目标属性。 但这种方法是对GridLength 没用,因为这个类型体是GridLength而不是double,这个是区别。 说回到时钟控制上,这个实际上是吧计算和赋值给分开,
var animation = new DoubleAnimation { To = 5, ... };
var clock = animation.CreateClock(); // 创建一个时钟

这里这个clock就是计时器,他知道当前的技术,也知道当前的进度,可以给我们计算后面的数值。

clock.CurrentTimeInvalidated += (s, e) =>  
{  
    var currentVal = (double)clock.CurrentProgress * (To - fromValue) + fromValue;  
    if (currentVal < 0) currentVal = 0;  
    if (target is ColumnDefinition c)  
        c.Width = new GridLength(currentVal, GridUnitType.Star); // 强制设为 Star 单位  
    else if (target is RowDefinition r)  
        r.Height = new GridLength(currentVal, GridUnitType.Star);  
};

这里他会拦截每一个帧数,把进度转换为double,然后计算出当前的数值,然后赋值。它的优势就是分开去计算。

传入动画达到复用率

public DoubleAnimation Animation { get; set; }  
  
protected override void Invoke(object parameter)  
{  
    var target = TargetObject as UIElement ?? AssociatedObject as UIElement;  
    if (target == null || string.IsNullOrEmpty(PropertyName)) return;  
    DoubleAnimation animToRun;  
    if (this.Animation != null)  
    {        animToRun = this.Animation.Clone();  
    }    else  
    {  
        animToRun = new DoubleAnimation  
        {  
            EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut }  
        };    }    if (!double.IsNaN(this.To))  
    {        animToRun.To = this.To;  
    }    // 当 Action 设置了 Duration 时才覆盖  
    if (this.Duration != default(Duration))  
    {        animToRun.Duration = this.Duration;  
    }    target.BeginAnimation(  
        DependencyPropertyDescriptor.FromName(PropertyName, target.GetType(), target.GetType()).DependencyProperty,  
        Animation);  
}

我们定义一个叫做Animation的属性,用来接收,如果没有传入那就使用提前设定好的默认动画。

if (!double.IsNaN(this.To)) : 如果开发者在外层Action上手动填了To的值,在 C# 和 WPF 里,double 类型的默认值通常是 0。

if (this.Duration != default(Duration)):Duration 是一个结构体(Struct),它不能为空(null),它的默认状态叫 Automatic。如果用户在外层标签写了 Duration="0:0:1",那么它就不等于默认值,我们就用这个时长强制覆盖内部动画的时长。