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.
Here's the code-behind that drives MainWindow
class behavior.
The FooteredContentControl
is the custom control that we're having problems with. It is defined below.
Here's the style for the FooteredContentControl
that defines the visual layout.
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.