WPF 之 依赖属性与附加属性(五)
一、CLR 属性
程序的本质是“数据+算法”,或者说用算法来处理数据以期得到输出结果。在程序中,数据表现为各种各样的变量,算法则表现为各种各样的函数(操作符是函数的简记法)。
类的作用是把散落在程序中的变量和函数进行归档封装并控制它们的访问。被封装在类里的变量称为字段(Field),它表示的是类或实例的状态;被封装在类里的函数称为方法(Method),它表示类或实例的功能。
字段(Field)被封装在实例里,要么能被外界访问(非 Private修饰),要么不能(使用 Private 修饰),这种直接把数据暴露给外界的做法很不安全,很容易把错误的数值写入字段。为了解决此问题,.NET Framework 推出了属性(Property),这种 .NET Framework 属性又称为 CLR 属性。
属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们实际上是称为访问器的特殊方法。 这使得我们不仅可以轻松访问数据,还有助于提高方法的安全性和灵活性。具体使用如下:
private double _seconds;
public double Hours
{
get { return _seconds / 3600; }
set {
if (value < 0 || value > 24)
throw new ArgumentOutOfRangeException(
$"{nameof(value)} must be between 0 and 24.");
_seconds = value * 3600;
}
}
二、依赖属性(Dependency Property)
实例中每个 CLR 都包装着一个非静态的字段(或者说由一个非静态的字段在后台支持)。如果一个 TextBox 有 100 个属性,每个属性都包装着一个 4 byte 的字段,那如果程序运行创建 10000 个 TexBox 时,属性将占用 100*4**10000≈3.8M 的内存。在这 100 个属性中,最常用的是 Text 属性,这意味着大多数的内存都会被浪费掉。为了解决此问题,WPF 推出了依赖属性。
依赖属性(Dependency Property),就是一种可以自己没有值,但能通过 Binding 从数据源获得值(依赖在别人身上)的属性。拥有依赖属性的对象被称为“依赖对象”。
WPF 中允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据时能够获得默认值、借用其他对象数据或实时分配空间的能力——这种对象被称为“依赖对象(Dependency Object)”,这种实时获取数据的能力依靠依赖属性(Dependency Property)来实现。
WPF 中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的 Binding 目标被数据所驱动。依赖对象的概念由 DependencyObject 类实现,依赖属性的概念由 DependencyProperty 类实现。DependencyObject 类具有 GetValue 和 SetValue 两个方法。具体实现一个依赖属性如下图所示(在 Visual Studio 中可以使用 “propdp” 按 Tab 键快捷生成):
public class StudentObject : DependencyObject
{
// CLR包装
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int), typeof(StudentObject), new PropertyMetadata(0));
}
WPF 中控件的属性大多数都为依赖属性,例如,Window 窗体的Title 属性,我们查看代码后如下:
/// <summary>获取或设置窗口的标题。</summary>
/// <returns>
/// 一个 <see cref="T:System.String" /> ,其中包含窗口的标题。
/// </returns>
[Localizability(LocalizationCategory.Title)]
public string Title
{
get
{
this.VerifyContextAndObjectState();
return (string) this.GetValue(Window.TitleProperty);
}
set
{
this.VerifyContextAndObjectState();
this.SetValue(Window.TitleProperty, (object) value);
}
}
WPF 中控件的继承关系: Control -----> FrameworkElement -----> UIElment -----> Visual -----> DependencyObject 。即 WPF 中所有 UI 控件都是依赖对象,UI 控件的大多数属性都已经依赖化了。
当我们为依赖属性添加 CLR 包装时,就相当于为依赖对象准备了暴露数据的 Binding Path,即该依赖对象具备扮演数据源(Source)和数据目标(Target)的能力。该依赖对象虽然没有实现 INotifyPropertyChanged 接口,但当属性的值发生改变的时候与之关联的 Binding 对象依然可以得到通知,依赖属性默认带有这样的功能,具体如下:
我们声明一个自定义控件,控件的依赖属性为 DisplayText:
public class MorTextBox : TextBox
{
public string DipalyText
{
get { return (string)GetValue(DipalyTextProperty); }
set { SetValue(DipalyTextProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DipalyTextProperty =
DependencyProperty.Register("DipalyText", typeof(string), typeof(MorTextBox), new PropertyMetadata(""));
public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
{
return BindingOperations.SetBinding(this, dp, binding);
}
}
我们先把该依赖属性作为数据目标获取第一个 TextBox 的 Text 属性,然后把自己的 DisplayText 依赖属性的值作为第二个 TextBox 的 Text 属性的数据源:
<StackPanel>
<TextBox Margin="5" Height="50" x:Name="t1"></TextBox>
<!--自定义依赖属性作为 Target-->
<local:MorTextBox x:Name="t2" Visibility="Collapsed" DipalyText="{Binding ElementName=t1,Path=Text,UpdateSourceTrigger=PropertyChanged}"></local:MorTextBox>
<!--自定义依赖属性作为 Source-->
<local:MorTextBox Margin="5" Height="50" Text="{Binding ElementName=t2,Path=DipalyText,UpdateSourceTrigger=PropertyChanged}"></local:MorTextBox>
</StackPanel>
当我们运行程序后,第二个 TextBox 的数值随着第一个 TextBox 数值的改变而改变。
三、附加属性(Attached Properties)
实际开发中,我们会经常遇到这样的情况,一个人在学校的时候需要记录班级等信息,在公司需要记录职业等信息,那么如果我们在设计 Human 类的时候,在类里面直接定义 Grade、Position 属性合适吗?
显然不合适!首先,当我们在学校上学的时候完全用不到公司等信息,那么Position 所占的内存就被浪费了。为了解决此问题,我们首先想到依赖属性,但解决了内存浪费问题,还存在一个问题,即一旦流程改变,那么 Human 类就需要做出改动,例如:当我们乘车的时候,有车次信息;去医院看病的时候,有排号信息等。这意味着应用场景的不断变化,导致我们所属的信息不断发生变化。为了解决此问题,.NET 推出了附加属性(Attached Properties)。
附加属性(Attached Properties)是说一个属性本来不属于某个对象,但由于某种需求而被后来附加上。也就是说把对象放入一个特定环境后对象才具有的属性(表现出来就是被环境赋予的某种属性)。上述例子,我们可以使用附加属性去解决这个问题(添加附加属性时,可以在 Visual studio 中输入 "propa" 然后按 Tab 键快捷生成):
class Human : DependencyObject
{
public string Name { get; set; }
public int Age { get; set; }
}
class School : DependencyObject
{
public static string GetGrade(DependencyObject obj)
{
return (string) obj.GetValue(GradeProperty);
}
public static void SetGrade(DependencyObject obj, string value)
{
obj.SetValue(GradeProperty, value);
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GradeProperty =
DependencyProperty.RegisterAttached("Grade", typeof(string), typeof(School), new PropertyMetadata(""));
}
class Company : DependencyObject
{
public static string GetPosition(DependencyObject obj)
{
return (string) obj.GetValue(PositionProperty);
}
public static void SetPosition(DependencyObject obj, string value)
{
obj.SetValue(PositionProperty, value);
}
// Using a DependencyProperty as the backing store for Position. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PositionProperty =
DependencyProperty.RegisterAttached("PPosition", typeof(string), typeof(Company), new PropertyMetadata(""));
}
使用依赖属性的方式如下:
// Attached Properties
{
Human human0 = new Human() { Name = "John", Age = 10, };
School.SetGrade(human0, "四年级二班");
Human human1 = new Human() { Name = "Andy", Age = 26, };
Company.SetPosition(human1, "软件工程师");
Human human2 = new Human() { Name = "Kolity", Age = 25, };
Company.SetPosition(human2, "产品经理");
TextBoxAttached.Text += $"{human0.Name},{human0.Age},{School.GetGrade(human0)}\r\n";
TextBoxAttached.Text += $"{human1.Name},{human1.Age},{Company.GetPosition(human1)}\r\n";
TextBoxAttached.Text += $"{human2.Name},{human2.Age},{Company.GetPosition(human2)}\r\n";
}
输出结果,如下所示:
John,10,四年级二班
Andy,26,软件工程师
Kolity,25,产品经理
从附加属性的实现中,我们可以看出附加属性(Attached Properties)的本质就是依赖属性(Dependency Property)。附加属性通过声明与依赖属性相关的 Get 与 Set 方法实现寄宿在宿主类(例如:Human)上,这意味宿主类也必须实现 DependencyObject 类。
其实,WPF 控件的布局控件的许多属性就为附加属性,例如:当把一个 TextBox 放入 Grid中时,对于 TextBox 而言我们可以使用 Grid 的 Row 和 Column 属性,如下:
<Grid >
<TextBox Grid.Row="0" Grid.Column="0"></TextBox>
</Grid>
放入 Canvas 中,可以使用 Canvas 的 Left 等附加属性:
<Canvas>
<TextBox Canvas.Left="0" Canvas.Right="100" Canvas.Bottom="20" Canvas.Top="8"></TextBox>
</Canvas>
附加属性(Attached Properties)的本质是依赖属性(Dependency Property),因此,附加属性也可以使用 Binding 依赖在其他对象的数据上,例如:我们通过两个 Slider 来控制矩形在 Canvas 中的横纵坐标:
<Canvas x:Name="c1">
<Slider x:Name="s1" Width="200" Height="50" Canvas.Top="10" Canvas.Left="50" Maximum="300"></Slider>
<Slider x:Name="s2" Width="200" Height="50" Canvas.Top="40" Canvas.Left="50" Maximum="400"></Slider>
<Rectangle Fill="CadetBlue" Width="30" Height="30" Canvas.Left="{Binding ElementName=s1,Path=Value}" Canvas.Top="{Binding ElementName=s2,Path=Value}"></Rectangle>
</Canvas>