Attached properties are a great way to extend capabilities of existing elements without the need to derive or otherwise tinker with those elements. Suppose we create an attached property that is a collection of objects of some particular kind. When that property changes, those objects are read, and that special functionality is applied. Here’s a hypothetical example of such a scheme:
- public static class SomeHelper {
- public static DemoCollection GetData(DependencyObject obj) {
- return (DemoCollection)obj.GetValue(DataProperty);
- }
-
- public static void SetData(DependencyObject obj, DemoCollection value) {
- obj.SetValue(DataProperty, value);
- }
-
- public static readonly DependencyProperty DataProperty =
- DependencyProperty.RegisterAttached("Data", typeof(DemoCollection), typeof(SomeHelper), new UIPropertyMetadata(null, OnDataChanged));
-
- static void OnDataChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {
- var coll = (DemoCollection)e.NewValue;
- // do something with collection
- foreach(var d in coll) {
- Trace.WriteLine(d.Text);
- }
- }
-
- }
-
- public class Demo : DependencyObject {
- public string Text {
- get { return (string)GetValue(TextProperty); }
- set { SetValue(TextProperty, value); }
- }
-
- public static readonly DependencyProperty TextProperty =
- DependencyProperty.Register("Text", typeof(string), typeof(Demo), new UIPropertyMetadata(string.Empty));
-
- }
-
- public class DemoCollection : List<Demo> {
- }
To summarize: The SomeHelper class defines the Data property which is of type DemoCollection, which is a List of Demo objects. The reason DemoCollection is defined and the code doesn’t use List<Demo> directly, is because WPF’s XAML does not support generics at this time. For each Demo object within the collection, its property Text is printed using Trace.WriteLine. In a real application, this is where the special code would be.
Let’s use that in a simple scenario:
- <Grid>
- <local:SomeHelper.Data>
- <local:DemoCollection>
- <local:Demo Text="Hello" />
- </local:DemoCollection>
- </local:SomeHelper.Data>
- </Grid>
The “local” XML prefix points to the namespace and assembly where the above types are defined. In this case, we should see “Hello” traced out to the Visual Studio debugger when running with the debugger. No surprise there.
Now suppose the value of the Text property is derived using data binding like so:
- <local:Demo Text="{Binding MyName}" />
This indicates that Text should be bound to the MyName property of the closest DataContext up the visual tree. Let’s set up the DataContext to be the current Window and MyName would be exposed through this window:
- public partial class MainWindow : Window {
- public MainWindow() {
- InitializeComponent();
-
- DataContext = this;
- }
-
- public string MyName {
- get { return "Cactus"; }
- }
- }
When this is run, the expected output is “Cactus”. Surprisingly, the output is an empty string. The Visual Studio debugger outputs the following in the output window as a data binding error:
- System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=MyName; DataItem=null; target element is 'Demo' (HashCode=48380239); target property is 'Text' (type 'String')
It seems the DataContext didn’t “stick”: The message claims that DataItem=null, meaning there is no source for the binding, and so it fails.
If we try something even simpler, such as binding to a named element, such as the window itself with a well known property like this:
- <local:Demo Text="{Binding ActualWidth, ElementName=win}" />
(the window is named “win”). Again nothing appears, and the debugger output claims:
- System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=ActualWidth; DataItem=null; target element is 'Demo' (HashCode=63379435); target property is 'Text' (type 'String')
Same result. What’s going on? Can’t we use any binding in this case?
The problem is a bit subtle. A DataContext or any element for that matter cannot be found because there is no “WPF" parent” around. The Demo’s instance parent is the DemoCollection object, which knows nothing about WPF and so has no way of knowing who is its “parent” in the visual tree.
Are we doomed? Fortunately, there is a way to fix this. We need to work with an object that is more “WPF aware” than a simple List<>. The next best thing is FreezableCollection<T>. This can hold any object derived from DependencyObject, but Freezable is preferred, and in this case, required; Freezable is an interesting class in itself, but this is beyond the scope of this post and it doesn’t matter in this case. We don’t need the special aspects of Freezable, we just need its awareness of being part of the WPF world.
We need to make two changes. The first, in the collection:
- public class DemoCollection : FreezableCollection<Demo> {
- }
The other is making sure or Demo class is indeed a Freezable:
- public class Demo : Freezable {
- public string Text {
- get { return (string)GetValue(TextProperty); }
- set { SetValue(TextProperty, value); }
- }
-
- public static readonly DependencyProperty TextProperty =
- DependencyProperty.Register("Text", typeof(string), typeof(Demo), new UIPropertyMetadata(string.Empty));
-
-
- protected override Freezable CreateInstanceCore() {
- throw new NotImplementedException();
- }
- }
This forces us to implement the abstract CreateInstanceCore from Freezable, but that doesn’t matter in this case, because it will not be created by the Freezable cloning mechanism. In any case, implementing it is typically done by calling the new operator with the default constructor of the current type.
That’s it. Now binding works as expected in all scenarios.