Here I continue talking about re-implementing and improving WPF concepts outside of WPF and in a way that is not necessarily connected to GUI development. The first article in the series was discussing implementation of property and collection bindings outside of WPF and is available at Binding without WPF or at Codeproject: Binding without WPF. The final goal of these series is to implement most of the concepts that WPF introduced outside of WPF without dependency on any MS visual libraries and, perhaps, even in different languages, like JavaScript, Java and Objective-C.
This article discusses re-implementing WPF Attached Properties.
As a reminder – WPF Attached Properties serve the same purpose as the usual properties: they allow to get a property value from an object. They are, however, implemented very differently from the usual properties: instead of being part of the object, they are defined outside of it. If an Attached Property was set on an object, it can be retrieved from some memory store outside of the object with the object serving as a key. If an Attached Property was never defined on an object, the Attached Property’s default value will be returned. This value is defined per Attached Property and will be the same for any object.
WPF’s Attached Properties provide a number of very useful features:
- Attached Properties were used in WPF to reduce the storage required for all the numerious
WPF properties. Indeed, if an object uses default Attached Property value, it does not
require any additional storage. This type of storing property values
is called sparse storage.
- WPF Attached Properties virtually allow adding new data to an object
without modifying the object’s type.
- WPF Attached Properties allow setting callbacks that fire when a property
changes on an object.
- In WPF only Attached (and Dependency) Properties can be a binding’s target.
(Dependency Properties are very similar to the AttachedProperties but can
only be defined within the type to which they can be attached).
- Attached Properties propagate down the visual tree.
- WPF’s built-in animation framework can only animate Attached and Dependency Properties.
Here we show how to build a framework providing capabilities very similar to the WPF Attached Properties. In order to differentiate between the WPF Attached Properties and this framework’s properties I call them AProperties or AProps. Here are the AProps capabilties that match those of the Attached Properties:
- AProps provide sparse storage.
- AProps allow to add external data to the objects without changing
the objects’ type or class.
- AProps allow to add a callback to be fired when a value changes on an object.
On top of the features above that are also available for the WPF attached properties it will also provide the following nice features:
- AProps can be attached to a C# entity of any type, not only to those descended from
DependencyObject
.
- Unlike WPF Attached Properties, the mechanism of operating with AProps is strongly typed.
- In WPF one can specify an Attached Propertie’s callback when it is created or registered. This callback fires after an object’s property changes and it is the same for any object that has the Attached Property. AProps allow to specify also a callback to be fired before a property is changed on an object. If this callback returns
false
, the property changed is cancelled. Moreover, the framework allows adding callback to the individual objects. This callbacks are fired on when the AProperty is changed on the object to which the callback was added. Other objects are not affected by the callback.
- Unlike Attached Properties, AProps do not have to be defined as static variables (even though they can be defined static).
Now I would like to list the functionality that the Attached Properties have, but AProps (at this point yet) do not.
- The binding framework from the previous article is not made to bind to or from AProps. Even though this functionality is coming soon.
- Attached Properties change the property value within UI thread, while the AProps change in the thread of the caller. Perhaps, at some point, I’ll add another degree of freedom to the AProps that would allow to specify property change thread.
- There is no (yet) visual framework built around AProps, so there is no propagation down the visual tree or animation classes that use AProps.
- There is no value coercion mechanism for AProps (which anyways not used very frequently in WPF).
The AProps code together with the test project that shows how to use them is can be downloade from APropsCode.zip.
The main (virtually the only) class for dealing with AProps is AProperty
under NP.AProps
project.
It provides a functionality for creating AProperty
object. AProperty
contains a Dictionary
(map) that maps the objects to their property values, or rather to some entities that contain their corresponding property values. If the object does not exist in the Dictionary
, the default value gets returns as its AProperty
value.
The central public methods of AProperty
class are the following:
public PropertyType GetProperty(ObjectType obj)
Given an object returns its AProperty
value.
public void SetProperty(ObjectType obj, PropertyType newPropertyValue)
Sets AProperty
value on the passed object.
-
public void AddOnPropertyChangedHandler
(
ObjectType obj,
OnPropertyChangedDelegate propChangedHandler
)
Adds object’s individual property change handler (other objects won’t be affected by it).
-
public void RemoveOnPropertyChangedHandler
(
ObjectType obj,
OnPropertyChangedDelegate propChangedHandler
)
Removes object’s individual property change handler.
public void ClearAProperty(ObjectType obj)
Clears AProperty
value from the object (essentially removes the object from the AProperty
‘s Dictionary
.
The constructor of AProperty
class has the following signature:
public AProperty
(
PropertyType defaultValue = default(PropertyType),
Func beforePropertyChangedFn = null,
OnPropertyChangedDelegate onPropertyChangedFn = null
)
As you can see, it allows to pass the default value of the AProperty
and two delegates: beforePropertyChangedFn
and onPropertyChangedFn
. The first of the delegates executes before AProperty
changes on some object. If it returns false
the property change is cancelled. The second delegate executes after the property change. Unlike individual object property change handlers these delegates execute for any object whose corresponding AProperty
changes. The provided API does not allow modifying these delegate once they were set; otherwise all the objects that had their corresponding AProperty
set might be affected. The OnPropertyChangedFn
delegate is similar to OnPropertyChanged delegate that can be passed to the Attached Property’s metadata as the second argument.
The code that shows how to use AProperty
API is under APropertyTest
project. MyTestClass
is a class with one property Name
of type string
. We want to add some integer index to MyTestClass
object by using AProperty
functionality.
Here is how we create indexAProperty
object:
AProperty<MyTestClass, int> indexAProperty = new AProperty
(
-1, null, (obj, oldVal, newVal) => {
Console.WriteLine("This is a generic (not individual) property change event handler, oldValue: {0}, newValue: {1}", oldVal, newVal);
}
);
We set the default value for indexAProperty
to -1
, the generic delegate to fire before the property change is not set (null
), and the generic post-property-change delegate prints a message with old and new value.
After that, we create a list of MyTestClass
object and populate it with 3 objects. The indexAProperty
for those 3 objects is set from is set to 1
, 2
and 3
correspondingly. The object with index 2
is assigned an individual property change event handler:
List myTestObjList = new List();
for (int i = 1; i < 4; i++)
{
MyTestClass myTestObj = new MyTestClass { Name = "Obj " + i };
if (i == 2)
{
indexAProperty.AddOnPropertyChangedHandler
(
myTestObj,
(obj, oldVal, newVal) => {
Console.WriteLine("This is individual property change event handler, oldValue: {0}, newValue: {1}", oldVal, newVal);
}
);
}
myTestObjList.Add(myTestObj);
indexAProperty.SetProperty(myTestObj, i);
}
Finally we iterate through the list of objects an print the indexAProperty
value for each of the objects:
foreach (MyTestClass myTestObj in myTestObjList)
{
int objNumber = indexAProperty.GetProperty(myTestObj);
Console.WriteLine(objNumber);
}