WPF DependencyProperty
o Section1 :Brief introduction
1. CLR properties are really just safe wrappers around a Private
member variable:
privateint x;
publicint X
{
get { return x; }
set { x = value; }
}
DependencyProperty(DP) is more than just simple CLR properties, The following table illustrates some of the things that can be acheived by the use of DPs:
Achievable items thanks to DPs |
Change Notification |
Callbacks |
Property value validation |
Property value inheritence * |
Participation in animations * |
Participation in Styles * |
Participation in Templates * |
Databinding |
Layout changes * |
Overriding default data values * |
2. Declare of DependencyProperty:
- Declare a dependencyProperty (Always public static readonly)
- Initialise the dependencyProperty, either using DependencyProperty.RegisterAttached/DependencyProperty.Register/DependencyProperty.RegisterReadOnly/DependencyPropertyRegisterAttachedReadOnly
- Declare get/set property wrapper (see code below)
public class MyStackPanel : StackPanel
{
public static readonly DependencyProperty MinDateProperty;
static MyStackPanel()
{
MinDateProperty = DependencyProperty.Register("MinDate",
typeof(DateTime),
typeof(MyStackPanel),
new FrameworkPropertyMetadata(DateTime.MinValue,
FrameworkPropertyMetadataOptions.Inherits));
}
public DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
}
Below is the register method:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, System.Windows.ValidateValueCallback validateValueCallback)
The last parameter is the delegate of ValidateValue method. For PropertyMetadata parameter, we use FrameWorkPropertyMetadata , is the type used for dependency property metadata, rather than the base metadata
types PropertyMetadata or UIPropertyMetadata
. This is true both for existing dependency properties and for most custom dependency property scenarios.
public FrameworkPropertyMetadata(object defaultValue, FrameworkPropertyMetadataOptions flags, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback) : base(defaultValue, propertyChangedCallback, coerceValueCallback, bool isAnimationProhibited, UpdateSourceTrigger defaultUpdateSourceTrigger)
- Default values
- Provide one of the
FrameworkPropertyMetadataOptions
values, such as AffectsMeasure/AffectsArrange/AffectsRender/Inherits etc - Property changed callback delegates
- Coersion values callback delegates ,can operate value
- Make a property un-animatable
- Provide one of the
UpdateSourceTrigger
, such as PropertyChanged/LostFocus/Explicit etc
o Section2 :How the Dependency Property registered and use
DependencyProperty Class has a static hashtable and list to store the registered dependencyProperty,
Class DependencyProperty
{………………
private static Hashtable PropertyFromName = new Hashtable();
internal static ItemStructList<DependencyProperty> RegisteredPropertyList = new ItemStructList<DependencyProperty>(0x300);
…………………..}
Method Register
{………
DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
………………..
PropertyFromName[key] = dp;
RegisteredPropertyList.Add(dp);
…………
if (typeMetadata != null)
{
this.OverrideMetadata(ownerType, typeMetadata);
}
……….}
The key in PropertyFromName is “name.HashCode^ownerType.HashCode”.
The Value of this dictionary is the instance of DependencyPropery.
Then if client have new FrameWorkMetaData, registration will add the metadata into a instance map, which will be explained specifically below in AddOwner method.
Take notice that the PropertyFromName hashtable and RegisteredPropertyList are static ,so all DPs can access these global hashtable and list, and let's see what members are instance members in DependencyProperty class:
Class DependencyProperty
{
private PropertyMetadata _defaultMetadata;
internal InsertionSortMap _metadataMap;
private string _name;
private Type _ownerType;
private Flags _packedData;
private Type _propertyType;
private DependencyPropertyKey _readOnlyKey;
private System.Windows.ValidateValueCallback _validateValueCallback;
…………………
private DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, System.Windows.ValidateValueCallback validateValueCallback)
{
Flags uniqueGlobalIndex;
this._metadataMap = new InsertionSortMap();
this._name = name;
this._propertyType = propertyType;
this._ownerType = ownerType;
this._defaultMetadata = defaultMetadata;
this._validateValueCallback = validateValueCallback;
}
…………………….
}
Most of the instance members are initiated in DP’s private constructor. When register, the filed “_defaultMetadata” used to store FrameWorkMetaData, then what “_metadataMap” used for?It used to store other controls’ metaData.
Now, we have registered MinDateProperty, then if other control need to use this property ,it will use AddOwner method like below:
public DependencyProperty AddOwner(Type ownerType, PropertyMetadata typeMetadata)
{
if (typeMetadata != null)
{
this.OverrideMetadata(ownerType, typeMetadata);
}
lock (Synchronized)
{
PropertyFromName[key] = this;
}
}
If register a new same name property, it will have nothing to do with MyStackPanel.MinDateProperty even if use inherited flag and is children of MyStackPanel
public partial class UserControlLabel : Label
{
public static readonly DependencyProperty MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(UserControlLabel),
new FrameworkPropertyMetadata(DateTime.MinValue,FrameworkPropertyMetadataOptions.Inherits));
}
AddOwner method will call OverrideMetadata method to Supplies alternate metadata for this dependency property when it is present on instances of a specified type, overriding the metadata that was provided in the initial property registration(when registeration ,if metadata is not null, also will call this method to put metadata in map):
this._metadataMap[dType.Id] = typeMetadata;
We can see this map, its key is current Dependency Object type’s Id ,and value is metadata. Take our program as example:
Instance member :_metadataMap in MyStackPanel.MindateProperty:
UserControlLabel.typeId |
UserControlLabel ‘s MinDateProperty. metaData |
MyStackPanel.typeId |
MyStackPannel’s MinDateProperty metaData |
|
|
|
|
( Note: if in AddOwner or register method, propertyMetaData == null, for register method, it will create a default propertyMetadata instance, for AddOwner method,it will don’t call OverrideMetadata method,_metaMap will no be changed, so how UserControlLabel find meta data for MinDatePropertymetaData?
In MinDateProperty.GetMetaData() method , it first find meta data through UserControlLabel’s typeId,if can’t find, try UserControlLabel’s base type, if can’t either, return the MinDateProperty’s default property metaData.)所以在写onPropertyChanged方法时候,要判断 source as StackPanel != null, 因为一个metadata 可能被其他source用到,这时候source就不是stackPanel了。
then add <propertyname ^ ownerType, property instance> to Hashtable PropertyFromName ,now the hashtable contains two elments, key is different ,but value is same:
Static member:PropertyFromName hashtable in MinDateProperty:
MinDate^StackPanel |
StackPanel’s MinDateProperty instance member |
MinDate^UserControl |
StackPanel’s MinDateProperty instance member |
|
|
|
|
This PropertyFromName hashtable is mainly use by the xaml->code process which can be found by analyzing the DependencyProperty.FromName() method using reflector’s “use by” function..
namespace System.Windows.Markup
internal class BamlMapTable
{
internal DependencyProperty GetDependencyProperty(BamlAttributeInfoRecord bamlAttributeInfoRecord)
{
if ((bamlAttributeInfoRecord.DP == null) && (bamlAttributeInfoRecord.PropInfo == null))
{
this.GetAttributeOwnerType(bamlAttributeInfoRecord);
if (bamlAttributeInfoRecord.OwnerType != null)
{
bamlAttributeInfoRecord.DP = DependencyProperty.FromName(bamlAttributeInfoRecord.Name, bamlAttributeInfoRecord.OwnerType);
}
}
return bamlAttributeInfoRecord.DP;
}
}
SomeTimes if we add other class’s DP as a member in my class, we can directly use overrideMetadata method to override metadata:
public void OverrideMetadata(Type forType, PropertyMetadata typeMetadata)
Class Page:Control
Static Page
{
UIElement.FocusableProperty.OverrideMetadata(typeof(Page), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));
}
}
The difference between use AddOwner method and overrideMetadata method is the last one don’t need to add <propertyname ^ ownerType, property instance> to Hashtable PropertyFromName.
Use OverrideMetaData method 意味着这个control并不想把这个Property作为自己的property,在xaml里可以直接设置了,也就是不必写.net property的包装,因为这个根本不是它的属性,一般用于attached property或者基类的property 来override metadata.
In DependencyProperty Class, also have method:
public static DependencyPropertyKey RegisterReadOnly(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, System.Windows.ValidateValueCallback validateValueCallback)
This method is used to register a read-only dependency property ,it also will new a DP instance ,fill hashtable and metamap, at last return DependencyPropertyKey class:
public sealed class DependencyPropertyKey
{
// Fields
private DependencyProperty _dp;
// Methods
internal DependencyPropertyKey (DependencyProperty dp);
public void OverrideMetadata(Type forType, PropertyMeta typeMetadata);
internal void SetDependencyProperty (DependencyProperty dp);
// Properties
public DependencyProperty DependencyProperty{ get; }
}
o Section3 :How to get and Set Property Value
All WPF class which use dependency property should inherited DependencyObject Class, in DependencyObject , it has methods SetValue(DependencyProperty dp, bool value) to set Value for one dependency Property and public object GetValue(DependencyProperty dp)to get Value for one dependency Property. (In program, code will use property wrapper to set and get value, but in xaml, will directly use SetValue() and GetValue() method).
In DependencyObject Class, it uses array
private EffectiveValueEntry[] _effectiveValues
to store the dependencyProperty’s value. The EffectiveValueEntry is not a simple value structure, it includes much information about the pipeline to get value, you can see appendix picture specifically.
In SetValue(DependencyProperty dp, bool value), it firstly get property metadata for this control from _metadataMap which I have said in front section. How to get it? Haven’t you see the map? Through the XXXClass.typeId ( PropertyMetadata metadata = this.SetupPropertyChange(dp))
SomeTimes, we don’t know the instance of Dependency property, we can also get it from PropertyFromName Hashtable, use method below which is in DependencyProperty class.
[FriendAccessAllowed]
internal static DependencyProperty FromName(string name, Type ownerType).
Now,Let’s save the value into _effectiveValues array. But before it, there are complicated things to do, because WPF contains many powerful mechanisms that independently attempt to set the value of dependency properties. Of course, as their name indicates, dependency properties were designed to depend on these providers in a consistent and orderly manner.
The picture below illustrates the five-step process(we call it pipeline) that WPF runs each dependency property through in order to calculate its final value. This process happens automatically thanks to the built-in change notification in dependency properties.
In SetValue method, it firstly will get corresponding dependencyProperty EffectiveValueEntry(if already have, get it, if not already have, new it). _effectiveValues is an array, how do I know which element is my want? _effectiveValues array use DependencyProperty’s hashcode as index, this hashcode is called DependencyProperty.GlobalIndex , it is generated in dependecyProperty ‘s constructor:
lock (Synchronized)
{
uniqueGlobalIndex = (Flags) GetUniqueGlobalIndex(ownerType, name);
RegisteredPropertyList.Add(this);
}
if (propertyType.IsValueType)
{
uniqueGlobalIndex |= Flags.IsValueType;
}
if (propertyType == typeof(object))
{
uniqueGlobalIndex |= Flags.IsObjectType;
}
if (typeof(Freezable).IsAssignableFrom(propertyType))
{
uniqueGlobalIndex |= Flags.IsFreezableType;
}
if (propertyType == typeof(string))
{
uniqueGlobalIndex |= Flags.IsStringType;
}
this._packedData = uniqueGlobalIndex;
GlobalIndex =(int) this._packedData) & 0xffff;
}
Next go to the pipeline,
Step1:“determine Base Value”:
The following list reveals the eight providers that can set the value of most dependency properties, in order from highest to lowest precedence:
1. Local value
2. Style triggers
3. Template triggers
4. Style setters
5. Theme style triggers
6. Theme style setters
7. Property value inheritance
8. Default value
Here I take inheritance for example; you will see how the inheritance is implemented. The code is:
if (!flag4 && metadata.IsInherited)
{
DependencyObject inheritanceParent = this.InheritanceParent;
if (inheritanceParent != null)
{
EntryIndex entry = inheritanceParent.LookupEntry(dp.GlobalIndex);
if (entry.Found)
{
flag4 = true;
newEntry=inheritanceParent._effectiveValues[entry.Index].GetFlattenedEntry(RequestFlags.FullyResolved);
newEntry.BaseValueSourceInternal = BaseValueSourceInternal.Inherited;
}
}
}
Step2:Expression: If the value from step one is an expression (an object deriving from System.Windows.Expression), then WPF performs a special evaluation step to convert the expression into a concrete result.
Step3: Animation: If one or more animations are running, they have the power to alter the current property value (using the value after step 2 as input) or completely replace it.
Step4: if in metadata, have set coerceValueCallback delegate ,will call this delegate to operatet the value.The code like:
if ((metadata.CoerceValueCallback != null)
{
object obj6 = metadata.CoerceValueCallback(this, coersionBaseValue);
}
Step5: When register property, we set ValidateValueCallback delegate, will call delegate to validate this property value.
(Most of the above 5 process is in method below, even can see it as an event trigger method:
Internal UpdateResult UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, ref EffectiveValueEntry newEntry, bool coerceWithDeferredReference, OperationType operationType))
)
After the five steps, save the EffectiveValueEntry which has the value to _effectiveValues[DependencyProperty.GlobalIndex] .
Don’t forget we can add propertyChangedCallback delegate to metadata when regester the DP, at last in setValue() , if value changed, it will call this delegate :
if ((isAValueChange)
{
this.NotifyPropertyChange(new DependencyPropertyChangedEventArgs(dp, metadata, isAValueChange, oldEntry, newEntry, operationType));
}
internal void NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
{
this.OnPropertyChanged(args);
………………………
}
Protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if ((e.IsAValueChange || e.IsASubPropertyChange) || (e.OperationType == OperationType.ChangeMutableDefaultValue))
{
PropertyMetadata metadata = e.Metadata;
if ((metadata != null) && (metadata.PropertyChangedCallback != null))
{
metadata.PropertyChangedCallback(this, e);
}
}
}
Use GetValue() method to get DepencyProperty value from _effectiveValues array using DependencyProperty.GlobalIndex, if have no value, GetValue method will try to get value from default, or if have set inherited flag in metaData,it will try to get value from parent .
In DependencyObject ,there is one useful method ClearValue(DependencyProperty), it can clear the local value(because it has highest precedence), and call UpdateEffectiveValue method to run the pipeline to get value again, and this method can call PropertyChanged delegate too.
o Section4 :Attached Dependency Property
Attached properties are just another strain of DPs. Using Attached Properties we are able to use DPs from classes that are outside of the current class.
1. Register Attached DP: use RegisterAttached method
public class MyStackPanel : StackPanel
{
public static readonly DependencyProperty IntDataProperty = DependencyProperty.RegisterAttached("IntData",
typeof(int),
typeof(MyStackPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.Inherits, onIntdataChange, onIntdataCoreceValue),onIntdataValidate);
}
The ownerType can be not DependencyObject, so RegisterAttached will not call overrideMetaData. Because _metadataMap use DependencyObjectType.typeId as key.
2. Provide static GetPropertyName and SetPropertyName methods as accessors for the attached property.
public class MyStackPanel : StackPanel
{
public static void SetIntData(UIElement element, int value)
{
//actually DependencyObject.SetValue()
element.SetValue(IntDataProperty, value);
}
public static int GetIntData(UIElement element)
{
//actually DependencyObject.GetValue()
return (int)element.GetValue(IntDataProperty);
}
}
3. Use attached property.
<Label x:Name="myLabel" FontWeight="Bold" FontSize="20" Foreground="White" Panel.ZIndex="5" ab:MyStackPanel.IntData="4">
The corresponding coding about attached property is :
MyStackPanel.SetIntData(myLabel,4);
4. Effect. If in MyStackPanel , I have set onPropertyChange delegate ,in the method ,I can operate the label control which have changed the property. In this code below , the mylabel ‘s background will be set to pink.
private static void onIntdataChange(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Control)
{
Control sourceControl = o as Control;
sourceControl.Background = Brushes.Pink;
}
}
Attached Property’s function:
1. Can add functions for other class and not changing their code. Like the upper step 4
2. Save data to _EffectiveValue[] , get it when need to use it:
<Button Canvas.Left=”18” Canvas.Top=”18”Background=”Orange”>Left=18,
What’s the difference between DependencyProperty.RegisterAttached() andDependencyProperty.Register() ? Just at the metadata, RegisterAttached method will not call OverrideMetadata method, this method will do two things which have stated in section2 , one is combine the base type’s metadata with new metadata ,the other is add metadata to instance member _metadataMap.Why the attache property don’t do these two things ,I have not understood it.
Although it, RegisterAttached method still will “new” a DependencyProperty instance ,add it into static member: RegisteredPropertyList and PropertyFromName hash table like Register method do
.
We must know this “IntDate” Dependency property and its value are belong to “MyLabel” instance , not belong to “MyStackPanel” instance any more. Then we get to know why we should add static GetPropertyName and SetPropertyName methods. It need use “MyLabel”’s SetValue() method to store the “IntDate” DP and its value into “MyLabel”’s _effectiveValues array.
Then “MyLabel” will get metadata from IntDataProperty , and trigger the PropertyChangeCallBack and coerceValueCallback , because pass the “MyLabel” instance to these call back, so can operate “MyLabel” instance.
MyLabel’s _effectiveValues[]
|
|
Index = IntDataProperty.GlobalIndex Value=4 |
|
|
|
Appendix 1:
Appendix 2:
Appendix 3:
In register and AddOwner method , they all will use the OverrideMetadata method:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, System.Windows.ValidateValueCallback validateValueCallback)
{
PropertyMetadata defaultMetadata = null;
if ((typeMetadata != null) && typeMetadata.DefaultValueWasSet())
{
defaultMetadata = new PropertyMetadata(typeMetadata.DefaultValue);
}
DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
if (typeMetadata != null)
{
property.OverrideMetadata(ownerType, typeMetadata);
}
return property;
}
OverrideMetaData method:
public void OverrideMetadata(Type forType, PropertyMetadata typeMetadata)
{
DependencyObjectType type;
PropertyMetadata metadata;
this.SetupOverrideMetadata(forType, typeMetadata, out type, out metadata);
this.ProcessOverrideMetadata(forType, typeMetadata, type, metadata);
}
I said in section 2, its main job is adding new metadata to _metadataMap structure. But already have confusing issue which I don’t understand.
private void SetupOverrideMetadata(Type forType, PropertyMetadata typeMetadata, out DependencyObjectType dType, out PropertyMetadata baseMetadata)
{
// get a wrapper struct DependencyObjectType which wrape the owner type
dType = DependencyObjectType.FromSystemType(forType);
//get owner type’s directly base type’s metadata
baseMetadata = this.GetMetadata(dType.BaseType);
}
private void ProcessOverrideMetadata(Type forType, PropertyMetadata typeMetadata, DependencyObjectType dType, PropertyMetadata baseMetadata)
{
lock (Synchronized)
{
// set map I have said in section2
this._metadataMap[dType.Id] = typeMetadata;
}
typeMetadata.InvokeMerge(baseMetadata, this);
typeMetadata.Seal(this, forType);
}
The confusing issue is “typeMetadata.InvokeMerge” method, it seems combine the base type’s metadata with current metadata:
protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
{
if (baseMetadata.PropertyChangedCallback != null)
{
Delegate[] invocationList = baseMetadata.PropertyChangedCallback.GetInvocationList();
if (invocationList.Length > 0)
{
System.Windows.PropertyChangedCallback a = (System.Windows.PropertyChangedCallback) invocationList[0];
for (int i = 1; i < invocationList.Length; i++)
{
a = (System.Windows.PropertyChangedCallback) Delegate.Combine(a, (System.Windows.PropertyChangedCallback) invocationList[i]);
}
a = (System.Windows.PropertyChangedCallback) Delegate.Combine(a, this._propertyChangedCallback);
this._propertyChangedCallback = a;
}
}
if (this._coerceValueCallback == null)
{
this._coerceValueCallback = baseMetadata.CoerceValueCallback;
}
}
In code ,it seems to combine this DP’s propertyChangeCallback with baseMetaData’s propertyChangeCallback, and if this._coerceValueCallback== null ,use baseMetadata.CoerceValueCallback.
But I have made an experiment:
public class MyStackPanel : StackPanel
{
public static readonly DependencyProperty MinDateProperty;
static MyStackPanel()
{
MinDateProperty = DependencyProperty.Register("MinDate",
typeof(DateTime),
typeof(MyStackPanel),
new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits,onMindateChange,onMindateCoreceValue),onMindateValidate);
}
public DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
…………………
}
public class MyStackPanel2 : MyStackPanel
{
public static readonly DependencyProperty MinDateProperty =DependencyProperty.Register("MinDate",
typeof(DateTime),
typeof(MyStackPanel2), new FrameworkPropertyMetadata(DateTime.MinValue ,
FrameworkPropertyMetadataOptions.Inherits,onMindateChange2), onIntdataValidate);
public new DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
private static void onMindateChange2(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
}
}
It’s a pity,when set value for MyStackPanel2.Mindate,it will trigger onMindateChange2 method, its parent MyStackPanel.onMindateChange will not be triggered , so do the onMindateCoreceValue.
So ,I don’t know the merge in overrideMetadata is used for what?