Saturday, October 04, 2008

Customizing Logical Children

WPF’s logical tree is used in providing all kinds of functionality, such as property inheritance and resource resolution.

Yesterday a colleague asked me how to customize the logical children of a control. Basically, he had a control that hosted a collection of child items that aren’t actually visuals. In the XAML, he wanted to be able to bind properties of the children. Kind of like how you can bind to the Height property of a RowDefinition in a WPF Grid control. The RowDefinition isn’t a Visual, but it can participate in binding by virtue of being part of the logical tree.

My initial thought was to override the FrameworkElement.LogicalChildren property, but he had already tried that without success. Altering the logical tree is not something I’ve ever had need to do before. Therefore, I decided to try this out for myself and am recording my findings here.

To test this all out, I created a user control called ArtistsControl that has an Artists property containing a collection of Artist objects. Each Artist object just has a name. The ArtistsControl contains a ListBox that shows each Artist in the collection.

I wanted to address the following use cases:

  1. Non-bound Artist declaration
  2. Artist bound to a property on the owning Window
  3. Artist bound to a property on the current DataContext
  4. Artist whose name comes from a statically-resolved resource defined in the owning Window
  5. Artist whose name comes from a dynamically-resolved resource defined in the owning Window
  6. Artist bound to a property on the owning Window, but done in the code-behind setting the source of the binding directly

Aside: The first issue I came up against had nothing to do with the initial problem at all. I wanted to define a user control called ArtistsControl that had a collection property called Artists that could be populated very easily from XAML:

<local:ArtistsControl>
    <local:Artist>Powderfinger</local:Artist>
</local:ArtistsControl>

However, I kept running into obscure compile errors such as:

Cannot set content property 'Artists' on element 'ArtistsControl'. 'Artists' has incorrect access level or its assembly does not allow access.

Long story short, the problem was that my dependency property was of type IList<Artist>. In order for the collection to play nice with XAML, it needs to be a non-generic type (can’t even be a closed generic type). To resolve this I just created my own ArtistCollection type which inherits from ObservableCollection<Artist>. After that, the above XAML snippet worked fine.

#1, #4 and #6 all work fine “out-of-the-box”. That makes sense because they don’t rely on the logical tree in any way.

As I mentioned earlier, the first thing I tried at this point is to override the LogicalTree property in ArtistsControl:

protected override IEnumerator LogicalChildren
{
    get
    {
        if (Artists == null)
        {
            yield break;
        }

        foreach (var artist in Artists)
        {
            yield return artist;
        }
    }
}

This has the effect of fixing . . . nothing!

However, I did notice this error in the output window:

Cannot find governing FrameworkElement or FrameworkContentElement for target element.

This led me to change the base class of Artist from DependencyObject to FrameworkElement. This also did nothing to solve the other use cases. But it did change the error in the output window to:

Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=Tag; DataItem=null; target element is 'Artist' (Name=''); target property is 'ArtistName' (type 'String')

Hmmm. I have supposedly made the Artist objects part of the logical tree, so why can they not resolve anything further up the tree?

I had an epiphany around this point when I realized:

Just because the parent acknowledges the child, that doesn’t mean the child acknowledges the parent.

In other words, just because the Artist objects are part of the ArtistControl’s LogicalChildren collection, that doesn’t mean the Artist objects have their Parent property set to the ArtistControl. The logical tree is a two-way connection: the child is connected to the parent, and the parent is connected to the child.

So how do we connect the two? FrameworkElement.Parent is a get-only property, and WPF uses internal methods to alter its value. Fortunately FrameworkElement defines a couple of methods to help us here: AddLogicalChild() and RemoveLogicalChild(). All we need to do is call these methods to add and remove Artist objects to and from the logical tree.

Overriding the LogicalChildren property allows property inheritance to work. The attached sample will still work without it, but any inherited properties will fail to propagate within your custom logical tree unless you also override LogicalChildren.

When all is said and done, all use cases are satisfied and the following XAML works fine:

<local:ArtistsControl ...>
    <local:Artist>The Smashing Pumpkins</local:Artist>
    <local:Artist ArtistName="{Binding Tag, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
    <local:Artist ArtistName="{Binding Tag}"/>
    <local:Artist ArtistName="{StaticResource StaticArtistResource}"/>
    <local:Artist ArtistName="{DynamicResource DynamicArtistResource}"/>
</local:ArtistsControl>