Fixing Relative WPF Bindings On Custom Properties

I recently spent a lot of time troubleshooting a problem where my relative WPF bindings were not behaving as expected for custom dependency properties that I had defined. This post explores the problem and the solution.

Problem

I was working on a custom WPF control recently and spent several hours trying to figure out why the bindings on my custom dependency properties weren't functioning correctly. The control was initially displayed in a Collapsed visibility state but then later became visible after user interaction. Within that control, I had some content that I wanted to display, but I wanted the content's color to be inherited from a parent control.

This problem only occured when the custom control was initially rendered with a Collapsed visibility. If the control started out as Visible or even Hidden, the bindings worked as expected.

The following is a derived, and greatly simplified example to illustrate the problem I was having. Please keep in mind that this coding style does not reflect best practices for production quality code. In this example, we have a Window that appears with a button in the middle of the screen that says "Click me." We have a custom FooteredContentControl that should appear when the user clicks the button. The custom control should have text that says "Body" in the main Content property, and then we want to see an icon of a foot in the Footer property. Oh, and we want them both to inherit the hot pink color from the window.

The xaml below defines the MainWindow layout.

<Window
    x:Class="WpfApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfApp1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    Foreground="HotPink"
    mc:Ignorable="d">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="FooteredContentControl.xaml" />
            </ResourceDictionary.MergedDictionaries>
            <PathGeometry x:Key="PathGeometryFoot" Figures="M16 2A2 2 0 1 1 14 4A2 2 0 0 1 16 2M12.04 3A1.5 1.5 0 1 1 10.54 4.5A1.5 1.5 0 0 1 12.04 3M9.09 4.5A1 1 0 1 1 8.09 5.5A1 1 0 0 1 9.09 4.5M7.04 6A1 1 0 1 1 6.04 7A1 1 0 0 1 7.04 6M14.53 12A2.5 2.5 0 0 0 17 9.24A2.6 2.6 0 0 0 14.39 7H11.91A6 6 0 0 0 6.12 11.4A2 2 0 0 0 6.23 12.8A6.8 6.8 0 0 1 6.91 15.76A6.89 6.89 0 0 1 6.22 18.55A1.92 1.92 0 0 0 6.3 20.31A3.62 3.62 0 0 0 10.19 21.91A3.5 3.5 0 0 0 12.36 16.63A2.82 2.82 0 0 1 11.91 15S11.68 12 14.53 12Z" />
        </ResourceDictionary>
    </Window.Resources>
    <DockPanel>
        <local:FooteredContentControl
            x:Name="Control"
            DockPanel.Dock="Top"
            Visibility="Collapsed">
            <local:FooteredContentControl.Footer>
                <Path
                    Width="20"
                    Height="20"
                    Data="{StaticResource PathGeometryFoot}"
                    Fill="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=Foreground}"
                    Stretch="Uniform" />
            </local:FooteredContentControl.Footer>
            <TextBlock Text="Body" />
        </local:FooteredContentControl>
        <Button
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Click="VisibilityToggle_Click"
            Content="Click Me" />
    </DockPanel>
</Window>
MainWindow.xaml

Here's the code-behind that drives MainWindow class behavior.

using System.Windows;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void VisibilityToggle_Click(object sender, RoutedEventArgs e)
        {
            Control.Visibility = Control.Visibility == Visibility.Visible ?
                Visibility.Collapsed : Visibility.Visible;
        }
    }
}
MainWindow.xaml.cs

The FooteredContentControl is the custom control that we're having problems with. It is defined below.

using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public class FooteredContentControl : ContentControl
    {
        public static readonly DependencyProperty FooterProperty =
            DependencyProperty.Register("Footer", typeof(object), typeof(FooteredContentControl),
                new FrameworkPropertyMetadata(null));

        static FooteredContentControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(FooteredContentControl),
                new FrameworkPropertyMetadata(typeof(FooteredContentControl)));
        }

        public object Footer
        {
            get { return GetValue(FooterProperty); }
            set { SetValue(FooterProperty, value); }
        }
    }
}
FooteredContentControl.cs

Here's the style for the FooteredContentControl that defines the visual layout.

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp1">
    <Style x:Key="{x:Type local:FooteredContentControl}" TargetType="{x:Type local:FooteredContentControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:FooteredContentControl}">
                    <StackPanel>
                        <ContentPresenter />
                        <ContentPresenter ContentSource="Footer" />
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
FooteredContentControl.xaml

Everything else is standard, out-of-the-box WPF project files.

In the example above, we click the button and only the word "Body" shows up but we don't see any feet. After snooping around with debugging tools, you can see that the Path with the feet is actually there, but it's just not visible because the Binding in the path's Fill is not being evaluated correctly. Thus, the Fill which defines the color is set to null, rendering the feet effectively transparent. To solve this problem, the issue with the binding needs to be addressed.

Solution

The solution was to make the value of my custom dependency property part of the logical tree. To accomplish this, a property changed callback was added to the FooterProperty and that callback method then invokes RemoveLogicalChild and AddLogicalChild to ensure that any footer content is added to the logical tree. This allows the relative binding to work as expected.

using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public class FooteredContentControl : ContentControl
    {
        public static readonly DependencyProperty FooterProperty =
            DependencyProperty.Register("Footer", typeof(object), typeof(FooteredContentControl),
                new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnFooterPropertyChanged)));

        static FooteredContentControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(FooteredContentControl),
                new FrameworkPropertyMetadata(typeof(FooteredContentControl)));
        }

        public object Footer
        {
            get { return GetValue(FooterProperty); }
            set { SetValue(FooterProperty, value); }
        }

        /// <summary>
        /// This called by the dependency property when its value has changed.
        /// </summary>
        /// <remarks>
        /// This is what was added to fix the bug
        /// that prevented the Footer content from showing
        /// when this control first appears in Collapsed
        /// visibility.
        /// </remarks>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnFooterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = (FooteredContentControl)d;

            // The following methods are provided by the FrameworkElement base class.

            // Removes any old content from the logical tree.
            control.RemoveLogicalChild(e.OldValue);
            // Adds new content to the logical tree.
            control.AddLogicalChild(e.NewValue);
        }
    }
}

The demo code will now show the feet icon when the user clicks the button.