八. 只读依赖属性

  我们以前在对简单属性的封装中,经常会对那些希望暴露给外界只读操作的字段封装成只读属性,同样在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>

效果如下图所示: 

ReadOnly_DPs

九. 附加属性

  前面我们讲了依赖属性。现在我们再继续探讨另外一种特殊的Dependency属性——附加属性。附加属性是一种特殊的依赖属性。他允许给一个对象添加一个值,而该对象可能对此值一无所知。

  最好的例子就是布局面板。每一个布局面板都需要自己特有的方式来组织它的子元素。如Canvas需要Top和left来布 局,DockPanel需要Dock来布局。当然你也可以写自己的布局面板(在上一篇文章中我们对布局进行了比较细致的探讨,如果有不清楚的朋友也可以再 回顾一下)。

下面代码中的Button 就是用了CanvasCanvas.TopCanvas.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); }}}

 

当按下”改变所有的值“按钮的时候,就会把之前的值都进行修改了,这个时候按下”清除本地值“就会使原来的所有默认值生效。

ClearLocallySetValues