依赖属性是WPF的重要组成部分,动画、数据绑定及样式等都需要依赖属性的支持才能得以实现。实际上我们在WPF控件中用到的属性,基本上都是依赖属性。
依赖属性只能定义在依赖对象(继承DependencyObject
的类)中。
注意,自己定义的依赖属性或依赖附加属性,要么在定义时通过FrameworkPropertyMetadata
设定绑定模式(双向或没有),要么在绑定表达式中进行设置。
依赖属性的定义
依赖属性的定义分为声明、注册、包装三个部分。
一、声明
依赖属性类型
依赖属性只能定义在继承了DependencyObject
的类(即依赖属性类型)中,因此声明依赖属性需要先创建一个依赖属性类型。这里以Window
控件为例子,为Window
控件添加依赖属性。
直接创建一个WPF窗体,进入后台代码
- 可以看到WPF窗体类型继承了
Window
类,而Window
类也是DependencyObject
的子类,其实WPF中的绝大多数类型都间接的继承了DependencyObject
,因此WPF的内置控件可以使用依赖属性。
依赖属性命名
根据WPF的依赖属性命名规范,在命名依赖属性时候最好是按照name+Property的格式来。
public partial class RelayClass : Window
{
//声明依赖属性
public static DependencyProperty ValueProperty;
......
}
二、注册
Register()
依赖属性的注册需要使用DependencyProperty
类型的静态方法Register()
。
DependencyProperty Register(string name, Type propertyType, Type ownerType)
:注册一个依赖属性,并返回注册后的DependencyProperty
对象。
- name:注册名,也就式在xaml中使用时的属性名,这个名字在归属的依赖属性类型中必须是唯一的。
- propertyType:属性的基础类型,可以通过
typeof()
来获取,例如typeof(double)
。 - ownerType:归属于哪个依赖属性类型,例如
typeof(RelayClass)
。
WPF的源码中,依赖属性的注册式放在静态构造函数中的,这里模仿一波。
public partial class RelayClass : Window
{
//声明依赖属性
public static DependencyProperty ValueProperty;
static RelayClass()
{
ValueProperty = DependencyProperty.Register("Value", typeof(int),typeof(RelayClass));
}
......
}
三、包装
由于WPF的安全机制,对于依赖属性(DependencyProperty
类型的成员)的读取与设置,只能通过DependencyObject
类中的实例方法GetValue
和SetValue
来完成,这也是为什么只能在依赖属性类型中定义依赖属性的原因之一。
但这样一来,虽然说是属性,但却无法真正像属性那样在C#中使用,要克服这一点,可以使用类的属性对其进行包装。
public partial class RelayClass : Window
{
public static DependencyProperty valueProperty;
static RelayClass()
{
valueProperty = DependencyProperty.Register("Value", typeof(int), typeof(RelayClass));
}
//对依赖属性进行包装
public int Value
{
get { return (int)GetValue(valueProperty); }
set { SetValue(valueProperty, value); }
}
......
}
依赖属性初始化
一、默认值与回调函数
依赖属性可以在通过在注册时给Register
函数传入PropertyMetadata
对象来设置默认值及回调函数。
Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata)
PropertyMetadata(object defaultValue[, PropertyChangedCallback propertyChangedCallback])
defaultValue
:为依赖属性设置初始值,new PropertyMetadata(0)
。需要注意传入的参数要与注册时的类型匹配,否则会在运行时报错。如果初始化值是该类型的对应默认值,可以使用defaulte
关键字,new PropertyMetadata(default)
。propertyChangedCallback
:为依赖属性设置回调函数,当依赖属性发生变化时调用,需要注意的是,当依赖属性注册的类型是一个引用类型时,只有当依赖属性的引用发生变法时才会触发回调函数。PropertyChangedCallback
为一个委托类型,接收两个参数。第一个参数DependencyObject
对象为依赖属性所在的对象;第二个参数DependencyPropertyChangedEventArgs
对象为变化动作中所关联的数据对象,该对象内含有NewValue
、OldValue
等属性。
public partial class RelayClass : Window
{
private static DependencyProperty valueProperty;
public int Value
{
get { return (int)GetValue(valueProperty); }
set { SetValue(valueProperty, value); }
}
static RelayClass()
{
valueProperty = DependencyProperty.Register("Value", typeof(int), typeof(RelayClass),
new PropertyMetadata(default, new PropertyChangedCallback(OnPropertyChanges)));
}
private static void OnPropertyChanges(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
......
}
......
}
二、验证回调
有时候在修改依赖属性前需要对依赖属性的值进行校验。
虽然使用前文中依赖属性的回调函数也可以对值进行校验,但是依赖属性的回调函数是在依赖属性值改变之后调用的,而回调函数中无法将依赖属性的值进行回溯,因此在依赖属性值被改变之前对其进行校验是非常必要的,这个时候就需要用到依赖属性的验证回调函数。
验证回调函数也是通过Register
函数在注册依赖属性时进行设置的。
Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback)
validateValueCallback
:对所注册的依赖属性设置值校验回调函数,当进行依赖属性的修改时,依赖属性值改变之前会先调用回调函数,并根据返回的true或false来确定要不要使用该值来作为依赖属性的值。ValidateValueCallback
为一个委托类型,接收一个Object
类型的参数,并返回一个bool
类型值。Object
类型参数为将要用来修改依赖属性的值,返回的bool
值则决定了是否要采用该值。
public partial class RelayClass : Window
{
......
private static DependencyProperty valueProperty;
static RelayClass()
{
valueProperty = DependencyProperty.Register("Value", typeof(int), typeof(RelayClass),
new PropertyMetadata(default, new PropertyChangedCallback(OnPropertyChanges)), new ValidateValueCallback(DataValidate));
}
private static bool DataValidate(object obj)
{
if (int.Parse(obj.ToString()) > 1000)
{
return false;
}
return true;
}
......
}
三、强制回调
除了上文中说的,回调函数和校验回调函数,依赖属性还可以设置强制回调函数。
与回调函数一样,强制回调函数的设置也是在使用Register
函数注册依赖属性时传入对应的PropertyMetadata
对象来进行设置的。
PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback)
coerceValueCallback
:对注册的依赖函数设置强制回调函数。CoerceValueCallback
为一个委托类型,该类型接收两个参数并返回一个object
对象。两个参数分别为:DependencyObject
对象为依赖属性所在的对象,object
为将要设置给依赖属性的值。返回的object
则是要设置依赖属性的值。
public partial class RelayClass : Window
{
private static DependencyProperty valueProperty;
......
static RelayClass()
{
valueProperty = DependencyProperty.Register("Value", typeof(int), typeof(RelayClass),
new PropertyMetadata(default, new PropertyChangedCallback(OnPropertyChanges), new CoerceValueCallback(CoerCallBack)),
new ValidateValueCallback(DataValidate));
}
private static object CoerCallBack(DependencyObject d, object baseValue)
{
//当设置的值大于1000,强制将依赖属性改为1000
if ((int)baseValue > 1000)
return 1000;
return baseValue;
}
......
}
四、回调顺序
当对依赖属性进行初始化时,会进入两次校验回调函数(具体是什么引起的还没深究),这个阶段如果校验回调函数返回false
,会报异常导致程序终止。
初始化完成后,对依赖属性进行修改时的顺序为:校验回调函数->强制回调函数->回调函数。
- 当校验回调函数返回
false
时,不会进入强制回调函数和回调函数,程序也不会终止。 - 当强制回调函数返回
null
时,不会进入回调函数。
FrameworkPropertyMetadata
上文中所设置的依赖属性默认值、回调函数等都是通过属性元数据PropertyMetadata
对象来进行设置的,其实PropertyMetadata
还有一个子类FrameworkPropertyMetadata
(框架属性元数据),其用法与PropertyMetadata
是一样的,不过FrameworkPropertyMetadata
提供了一个可以对依赖属性设置更多特性的构造函数,即可以在第二个参数的位置上通过传入FrameworkPropertyMetadataOptions
枚举来对依赖属性进行更多的设置。
FrameworkPropertyMetadata(object defaultValue, FrameworkPropertyMetadataOptions flags[,...])
FrameworkPropertyMetadataOptions
:枚举类型、依赖属性的一些特性选项,如果需要同时设置多个,可以通过或符号|
来实现。AffectsMeasure
、AffectsArrange
、AffectsParentMeasure
、AffectsParentArrange
:属性发生变化时,通知容器进行重新测量和排列,Margin
值变化时,会把相邻的对象打开。AffectsRender
:属性的变化导致元素重新渲染、重新绘制。BindsTwoWayByDefault
:默认情况下以双向绑定的方式处理绑定行为(可设可不设,最终会以Bing
的Mode
为准)Inherits
:继承特性。例如FontSize
属性,父对象对这个属性进行设置的时候会对子对象进行相同的影响。NotDataBindable
:不能使用表达式来设置依赖属性。SubPropertiesDoNotAffectRender
:对象属性子属性发生变化时,不去重新渲染对象。Journal
:Page
开发,属性的值会被保存到日志。
继承特性
这里重点学习一下继承特性Inherits
,注册时如果设置了Inherits
继承特性,那么在xaml中,父类元素对这个依赖属性的修改会对子类元素的这个依赖属性进行相同的影响。(需要注意的是,这里的父子关系最终是根据xaml的元素结构来确定的)
设置继承特性,除了首次注册该依赖属性的类外,其他类在定义这个依赖属性的时候还需要通过首次定义时的注册的依赖属性对象的AddOwner
方法来进行从属类型的添加。
DependencyProperty AddOwner(Type ownerType[, PropertyMetadata typeMetadata])
ownerType
:依赖属性所在的类型,直接通过typeof
获取当前类型即可。typeMetadata
:属性元数据,可以参考上文。
这里以两个ContentControl
控件类型作为例子,注意观察里中MyControl2中使用了MyControl1中的依赖属性对象的AddOwner方法来添加从属类型并获取依赖属性对象。
定义依赖属性
public class MyControl1:ContentControl
{
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(MyControl1),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.Inherits));
}
public class MyControl2 : ContentControl
{
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
MyControl1.ValueProperty.AddOwner(typeof(MyControl2),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.Inherits));
}
xaml中使用
<Grid>
<StackPanel>
<local:MyControl2 Value="123">
<local:MyControl1 x:Name="control1"/>
</local:MyControl2>
<TextBlock Text="{Binding ElementName=control1,Path=Value}" Foreground="Blue" FontSize="20"/>
</StackPanel>
</Grid>
再次强调,在本次例子中,MyControl1和MyControl2在定义依赖属性对象时,顺序上有前后关系,但是在父子关系是根据在xaml中元素的结构来确定的。
对于继承特性,最经典的可以说是FontSize
属性,Window
控件一旦设置了,窗体下的所有控件的FontSize
属性都受到同样的影响。
依赖附加属性
有些控件属性是不能做绑定的,例如PasswordBox
中的Password
属性,这个时候可以通过依赖附加属性来实现动态的数据交互。
有时候会给子控件提供属性,通过子控件对该属性的设置来实现某种需求(例如布局控件做动态子项时),这个时候也需要用到依赖附加属性。
简单的说就是依赖附加属性是一个可以让其他控件对象使用的依赖属性。
依赖附加属性不一定要定义在依赖属性类型中。
一、依赖附加属性的定义
依赖附加属性的定义步骤跟依赖属性是一样的,分别是声明、注册、包装,其中注册和包装与依赖属性的处理上有所不同。
注册
依赖附加属性的注册使用的是DependencyProperty
类型的静态方法RegisterAttached
,其使用方式与Register
方法是完全一样的。
public class MyControl
{
......
public static readonly DependencyProperty PasswordValueProperty =
DependencyProperty.RegisterAttached("PasswordValue", typeof(string), typeof(MyControl), new PropertyMetadata(default));
......
}
包装
依赖附加属性的包装与依赖属性的包装有较大的区别。
依赖属性是通过普通的属性进行包装的;依赖附加属性是通过方法进行包装的,通过向方法传入DependencyProperty
对象从而让这个过程与所从属的类型实例解耦,从而使得依赖附加属性可以在不是依赖属性的对象中定义。
也就是说,依赖属性关注的是属性所从属的对象自身,而依赖附加属性是关注所附加的对象。
由于依赖附加属性是通过使用向方法传入的DependencyProperty
对象的GetValue
与SetValue
方法,因此依赖附加属性可以在非依赖属性类型中定义。
public class MyControl
{
public static string GetPasswordValue(DependencyObject obj)
{
return (string)obj.GetValue(PasswordValueProperty);
}
public static void SetPasswordValue(DependencyObject obj, string value)
{
obj.SetValue(PasswordValueProperty, value);
}
public static readonly DependencyProperty PasswordValueProperty =
DependencyProperty.RegisterAttached("PasswordValue", typeof(string), typeof(MyControl), new PropertyMetadata(default));
}
二、Password控件依赖附加属性实例
思考一个问题,查看PasswordBox
控件的代码,可以发现Password
是一个普通属性,无法像依赖属性那样进行绑定来实现数据交互,而在某些业务需求下,需要对Password
属性进行动态修改,并进行数据交互,此时就可以借用依赖附加属性来实现了。
本例使用静态资源来进行绑定,实际场景中可以绑定到特定对象的属性上。
重点:依赖附加属性的回调函数中使用的依赖属性对象,其实就是PasswordBox
控件对象,在调用依赖附加属性时传递过来的
定义依赖附加属性
public class MyControl : ContentControl
{
public static string GetPasswordValue(DependencyObject obj)
{
return (string)obj.GetValue(PasswordValueProperty);
}
public static void SetPasswordValue(DependencyObject obj, string value)
{
obj.SetValue(PasswordValueProperty, value);
}
public static readonly DependencyProperty PasswordValueProperty =
DependencyProperty.RegisterAttached("PasswordValue", typeof(string), typeof(MyControl),
new PropertyMetadata(default, new PropertyChangedCallback(OnPropertyChanged)));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//这里的依赖属性对象d,其实就是PasswordBox控件对象,在调用依赖附加属性时传递过来的
(d as PasswordBox).Password = e.NewValue.ToString();
}
}
xaml中使用
<Window ......
xmlns:sys="clr-namespace:System;assembly=mscorlib"
......
>
<Window.Resources>
<sys:String x:Key="password">123456</sys:String>
</Window.Resources>
<Grid>
<PasswordBox local:MyControl.PasswordValue="{StaticResource password}"/>
</Grid>
</Window>
类型转换器
在定义依赖属性的时候,并不是所有的依赖属性都是string
类型(如Margin
的类型是Thickness
),而我们在xaml中使用如Margin
、Width
、Text
等常见属性时,都是直接给设置一个字符串就完事了,例如Margin=”0 0 0 0”
。那么这个字符串是如何变成依赖属性所对应的类型的?这个转换过程就是由类型转换器来完成的。类型装转换器的具体使用过程如下:
创建类型装转换器
创建类型转换器需要继承TypeConvert
类并重写其ConvertFrom
函数。
object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
:转换函数,接收关联的依赖属性的设置值,执行完成后返回object
对象作为最终用来修改依赖属性的值。
value
:xaml中设置依赖属性的值,一般为一个字符串类型。
public class CPTypeConverter : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var strArr = value.ToString().Split(',');
return new MyControlProperty()
{
Width = Convert.ToInt32(strArr[0]),
Height = Convert.ToInt32(strArr[1])
};
}
}
关联转换器
在需要进行处理的目标类型上通过特性标识[TypeConverter(Type)]
来进行转换器的关联
[TypeConverter(typeof(CPTypeConverter))]
public class MyControlProperty
{
public int Width { get; set; }
public int Height { get; set; }
}
定义依赖属性
public class MyControl : ContentControl
{
public MyControlProperty CP
{
get { return (MyControlProperty)GetValue(CPProperty); }
set { SetValue(CPProperty, value); }
}
public static readonly DependencyProperty CPProperty =
DependencyProperty.Register("CP", typeof(MyControlProperty), typeof(MyControl));
}
在Xaml中使用依赖属性
<Grid>
<local:MyControl CP="2,3"></local:MyControl>
</Grid>