WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[2] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[3] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[4] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[5] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[6] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
八. 只读依赖属性
我们以前在对简单属性的封装中,经常会对那些希望暴露给外界只读操作的字段封装成只读属性,同样在WPF中也提供了只读属性的概念,如一些WPF控件的依赖属性是只读的,它们经常用于报告控件的状态和信息,像IsMouseOver等属性, 那么在这个时候对它赋值就没有意义了。 或许你也会有这样的疑问:为什么不使用一般的.Net属性提供出来呢?一般的属性也可以绑定到元素上呀?这个是由于有些地方必须要用到只读依赖属性,比如Trigger等,同时也因为内部可能有多个提供者修改其值,所以用.Net属性就不能完成天之大任了。
那么一个只读依赖属性怎么创建呢?其实创建一个只读的依赖属性和创建一个一般的依赖属性大同小异(研究源码你会发现,其内部都是调用的同一个Register方法)。仅仅是用DependencyProperty.RegisterReadonly替换了DependencyProperty.DependencyProperty而已。和前面的普通依赖属性一样,它将返回一个DependencyPropertyKey。该键值在类的内部暴露一个赋值的入口,同时只提供一个GetValue给外部,这样便可以像一般属性一样使用了,只是不能在外部设置它的值罢了。
下面我们就用一个简单的例子来概括一下:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
//内部用SetValue的方式来设置值
DispatcherTimer timer =
new DispatcherTimer(TimeSpan.FromSeconds(1),
DispatcherPriority.Normal,
(object sender, EventArgs e)=>
{
int newValue = Counter == int.MaxValue ? 0 : Counter + 1;
SetValue(counterKey, newValue);
},
Dispatcher);
}
//属性包装器,只提供GetValue,这里你也可以设置一个private的SetValue进行限制
public int Counter
{
get { return (int)GetValue(counterKey.DependencyProperty); }
}
//用RegisterReadOnly来代替Register来注册一个只读的依赖属性
private static readonly DependencyPropertyKey counterKey =
DependencyProperty.RegisterReadOnly("Counter",
typeof(int),
typeof(Window1),
new PropertyMetadata(0));
}
XAML中代码:
<Window x:Name="winThis" x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Read-Only Dependency Property" Height="300" Width="300">
<Grid>
<Viewbox>
<TextBlock Text="{Binding ElementName=winThis, Path=Counter}" />
</Viewbox>
</Grid>
</Window>
效果如下图所示:
前面我们讲了依赖属性。现在我们再继续探讨另外一种特殊的Dependency属性——附加属性。附加属性是一种特殊的依赖属性。他允许给一个对象添加一个值,而该对象可能对此值一无所知。
最好的例子就是布局面板。每一个布局面板都需要自己特有的方式来组织它的子元素。如Canvas需要Top和left来布局,DockPanel需要Dock来布局。当然你也可以写自己的布局面板(在上一篇文章中我们对布局进行了比较细致的探讨,如果有不清楚的朋友也可以再回顾一下)。
下面代码中的Button 就是用了Canvas的Canvas.Top和Canvas.Left="20" 来进行布局定位,那么这两个就是传说中的附加属性。
<Canvas>
<Button Canvas.Top="20" Canvas.Left="20" Content="Knights Warrior!"/>
</Canvas>
在最前面的小节中,我们是使用DependencyProperty.Register来注册一个依赖属性,同时依赖属性本身也对外提供了 DependencyProperty.RegisterAttached方法来注册附加属性。这个RegisterAttached的参数和 Register是完全一致的,那么Attached(附加)这个概念又从何而来呢?
其实我们使用依赖属性,一直在Attached(附加)。我们注册(构造)一个依赖属性,然后在DependencyObject中通过 GetValue和SetValue来操作这个依赖属性,也就是把这个依赖属性通过这样的方法关联到了这个DependencyObject上,只不过是通过封装CLR属性来达到的。那么RegisterAttached又是怎样的呢?
下面我们来看一个最简单的应用:首先我们注册(构造)一个附加属性:
public class AttachedPropertyChildAdder
{
//通过使用RegisterAttached来注册一个附加属性
public static readonly DependencyProperty IsAttachedProperty =
DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedPropertyChildAdder),
new FrameworkPropertyMetadata((bool)false));
//通过静态方法的形式暴露读的操作
public static bool GetIsAttached(DependencyObject dpo)
{
return (bool)dpo.GetValue(IsAttachedProperty);
}
//通过静态方法的形式暴露写的操作
public static void SetIsAttached(DependencyObject dpo, bool value)
{
dpo.SetValue(IsAttachedProperty, value);
}
}
在XAML中就可以使用刚才注册(构造)的附加属性了:
在上面的例子中,AttachedPropertyChildAdder 中并没有对IsAttached采用CLR属性形式进行封装,而是使用了静态SetIsAttached方法和GetIsAttached方法来存取IsAttached值,当然如果你了解它内部原理,你就会看到实际上还是调用的SetValue与GetValue来进行操作(只不过拥有者不同而已)。这里我们不继续深入下去,详细在后面的内容会揭开谜底。
十. 清除本地值
在很多时候,由于我们的业务逻辑和UI操作比较复杂,所以一个庞大的页面会进行很多诸如动画、3D、多模板及样式的操作,这个时候页面的值已经都被改变了,如果我们想让它返回默认值,可以用ClearValue 来清除本地值,但是遗憾的是,很多时候由于WPF依赖属性本身的设计,它往往会不尽如人意(详细就是依赖属性的优先级以及依赖属性EffectiveValueEntry 的影响)。ClearValue 方法为在元素上设置的依赖项属性中清除任何本地应用的值提供了一个接口。但是,调用 ClearValue 并不能保证注册属性时在元数据中指定的默认值就是新的有效值。值优先级中的所有其他参与者仍然有效。只有在本地设置的值才会从优先级序列中移除。例如,如果您对同时也由主题样式设置的属性调用 ClearValue,主题值将作为新值而不是基于元数据的默认值进行应用。如果您希望取消过程中的所有属性值,而将值设置为注册的元数据默认值,则可以通过查询依赖项属性的元数据来最终获得默认值,然后使用该默认值在本地设置属性并调用 SetValue来实现,这里我们得感得PropertyMetadata类为我们提供了诸如DefaultValue这样的外部可访问的属性。
上面讲了这么多,现在我们就简单用一个例子来说明上面的原理(例子很直观,相信大家能很容易看懂)
XAML中代码如下:
<Window x:Class="WpfApplication1.DPClearValue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="400" Width="400">
<StackPanel Name="root">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Height" Value="20"/>
<Setter Property="Width" Value="250"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style TargetType="Ellipse">
<Setter Property="Height" Value="50"/>
<Setter Property="Width" Value="50"/>
<Setter Property="Fill" Value="LightBlue"/>
</Style>
<Style TargetType="Rectangle">
<Setter Property="Height" Value="50"/>
<Setter Property="Width" Value="50"/>
<Setter Property="Fill" Value="MediumBlue"/>
</Style>
<Style x:Key="ShapeStyle" TargetType="Shape">
<Setter Property="Fill" Value="Azure"/>
</Style>
</StackPanel.Resources>
<DockPanel Name="myDockPanel">
<Ellipse Height="100" Width="100" Style="{StaticResource ShapeStyle}"/>
<Rectangle Height="100" Width="100" Style="{StaticResource ShapeStyle}" />
</DockPanel>
<Button Name="RedButton" Click="MakeEverythingAzure" Height="39" Width="193">改变所有的值</Button>
<Button Name="ClearButton" Click="RestoreDefaultProperties" Height="34" Width="192"> 清除本地值</Button>
</StackPanel>
</Window>
后台代码:
public partial class DPClearValue
{
//清除本地值,还原到默认值
void RestoreDefaultProperties(object sender, RoutedEventArgs e)
{
UIElementCollection uic = myDockPanel.Children;
foreach (Shape uie in uic)
{
LocalValueEnumerator locallySetProperties = uie.GetLocalValueEnumerator();
while (locallySetProperties.MoveNext())
{
DependencyProperty propertyToClear = (DependencyProperty)locallySetProperties.Current.Property;
if (!propertyToClear.ReadOnly) { uie.ClearValue(propertyToClear); }
}
}
}
//修改本地值
void MakeEverythingAzure(object sender, RoutedEventArgs e)
{
UIElementCollection uic = myDockPanel.Children;
foreach (Shape uie in uic) { uie.Fill = new SolidColorBrush(Colors.Azure); }
}
}
当按下”改变所有的值“按钮的时候,就会把之前的值都进行修改了,这个时候按下”清除本地值“就会使原来的所有默认值生效。
十一. 依赖属性元数据
前面我们看到一个依赖属性的注册最全的形式是下面这样子的:
public static DependencyProperty Register(string name,
Type propertyType,
Type ownerType,
PropertyMetadata typeMetadata,
ValidateValueCallback validateValueCallback);
第一个参数是该依赖属性的名字,第二个参数是依赖属性的类型,第三个参数是该依赖属性的所有者的类型,第五个参数就是一个验证值的回调委托,那么最使我们感兴趣的还是这个可爱的 PropertyMetadata ,也就是我们接下来要讲的元数据。 提到WPF属性元数据,大家可能第一想到的是刚才的PropertyMetadata,那么这个类到底是怎样的呢?我们应该怎样使用它呢?首先我们看它的构造函数(我们选参数最多的来讲):
public PropertyMetadata(object defaultValue,
PropertyChangedCallback propertyChangedCallback,
CoerceValueCallback coerceValueCallback);
其中的第一个参数是默认值,最后两个分别是PropertyChanged(变化通知)以及Coerce(强制)的两个委托变量,我们在实例化的时候,只需要把这两个委托变量关联到具体的方法上即可。
事实上,除了PropertyMetadata以外,常见的还有FrameworkPropertyMetadata,UIPropertyMetadata。他们的继承关系是F->U->P。其中以FrameworkPropertyMetadata参数最多,亦最为复杂。
FrameworkPropertyMetadata的构造函数提供了很多重载,我们挑选最为复杂的重载来看它到底有哪些参数以及提供了哪些功能:
public FrameworkPropertyMetadata(object defaultValue,
FrameworkPropertyMetadataOptions flags,
PropertyChangedCallback propertyChangedCallback,
CoerceValueCallback coerceValueCallback,
bool isAnimationProhibited,
UpdateSourceTrigger defaultUpdateSourceTrigger);
其中第一个参数是默认值,最后两个参数分别是是否允许动画,以及绑定时更新的策略(在Binding当中相信大家并不陌生),这个不详细解释了。重点看一下里第三、四两个参数,两个 CallBack的委托。结合前面Register的时候提到的ValidateValueCallback共组成三大”金刚“,这三个Callback分别代表Validate(验证),PropertyChanged(变化通知)以及Coerce(强制)。当然,作为 Metadata,FrameworkPropertyMetadata只是储存了该依赖属性的策略信息,WPF属性系统会根据这些信息来提供功能并在适当的时机回调传入的delegate,所以最重要的还是我们定义的这些方法,通过他们传入委托才能起到真正的作用。
上面讲了元数据暴露给我们的构造函数,其实在其内部还提供了两个方法,这个在做自定义控件的时候,也很值得注意:
protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
{
// 实现元数据继承之间的合并
}
protected virtual void OnApply(DependencyProperty dependencyProperty, Type targetType)
{
// 当元数据被这个属性应用,OnApply就会被触发,在此时元数据也将被密封起来。
}
前面讲了这么多,那么我们现在就来看看依赖属性回调、验证及强制值到底是怎么使用的呢?大家千万要坚持住,后面内容更加精彩!