Detecting Bindings that should be OneTime

In WPF, a Binding’s source can be any .NET object; the target of the Binding will be updated when the specified property on that source changes. This works best when the source property is a DependencyProperty, or when the source object implements INotifyPropertyChanged; these objects have built-in support for property value changed notifications. In other cases, the ComponentModel infrastructure (as exposed by the PropertyDescriptor class) stores the source object in a global table in order to track clients who wish to be notified when a property value changes.

Binding to a regular property of a regular .NET object (that doesn’t implement INotifyPropertyChanged) has two drawbacks:

  1. It may be needlessly inefficient. If, for example, the source object is not implementing INotifyPropertyChanged because it’s immutable, creating and attaching value changed handlers is unnecessary overhead.
  2. It can cause a memory leak.

Both these problems can be eliminated by setting the Mode of the Binding to OneTime, but in a large application, determining all the bindings that could be OneTime is not an easy task. Some spelunking (with .NET Memory Profiler and .NET Reflector) showed that the (internal) ReflectTypeDescriptionProvider class has a static Hashtable containing all objects that have had value changed handlers added. A common reason for objects to end up in that Hashtable is their participation in a WPF binding, so enumerating this Hashtable at runtime can help track down bindings that may need to be changed. (And if an object is never removed from this hashtable, that may be a sign of a memory leak.)

This method uses reflection to dump the contents of the ReflectTypeDescriptionProvider._propertyCache hashtable for diagnostic purposes (the definition of the ReflectPropertyDescriptorInfo class is given later):

private static ReadOnlyCollection<ReflectPropertyDescriptorInfo> GetReflectPropertyDescriptorInfo()
{
    List<ReflectPropertyDescriptorInfo> listInfo = new List<ReflectPropertyDescriptorInfo>();
 
    // get the ReflectTypeDescriptionProvider._propertyCache field

    Type typeRtdp = typeof(PropertyDescriptor).Module.
        GetType("System.ComponentModel.ReflectTypeDescriptionProvider");
    FieldInfo propertyCacheFieldInfo = typeRtdp.GetField("_propertyCache",
        BindingFlags.Static | BindingFlags.NonPublic);
    Hashtable propertyCache = (Hashtable) propertyCacheFieldInfo.GetValue(null);
 
    if (propertyCache != null)
    {
        // try to make a copy of the hashtable as quickly as possible (this object can be accessed by other threads)

        DictionaryEntry[] entries = new DictionaryEntry[propertyCache.Count];
        propertyCache.CopyTo(entries, 0);
 
        FieldInfo valueChangedHandlersFieldInfo = typeof(PropertyDescriptor).GetField("valueChangedHandlers",
            BindingFlags.Instance | BindingFlags.NonPublic);
 
        // count the "value changed" handlers for each type

        foreach (DictionaryEntry entry in entries)
        {
            PropertyDescriptor[] pds = (PropertyDescriptor[]) entry.Value;
            if (pds != null)
            {
                foreach (PropertyDescriptor pd in pds)
                {
                    Hashtable valueChangedHandlers = (Hashtable) valueChangedHandlersFieldInfo.GetValue(pd);
                    if (valueChangedHandlers != null && valueChangedHandlers.Count != 0)
                        listInfo.Add(new ReflectPropertyDescriptorInfo(entry.Key.ToString(), pd.Name,
                            valueChangedHandlers.Count));
                }
            }
        }
    }
 
    listInfo.Sort();
    return listInfo.AsReadOnly();
}

The following code implements a window that displays all the properties that were found. It can be used by adding it to a WPF application and creating a special diagnostic button or keystroke that opens the window. You can open two windows and compare the lists side-by-side, or use the Refresh button to regenerate the list (after interacting with your application’s UI) to see if any properties have been added or removed.

ReflectPropertyDescriptorWindow.xaml:

<Window x:Class="OneTimeBinding.ReflectPropertyDescriptorWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:src="clr-namespace:OneTimeBinding"
    Title=".NET Properties used in Binding Paths" Height="450" Width="450" WindowStartupLocation="CenterScreen"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
 
    <Window.Resources>
        <ResourceDictionary>
            <DataTemplate DataType="{x:Type src:ReflectPropertyDescriptorInfo}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding TypeName, Mode=OneTime}"/>
                    <TextBlock>.</TextBlock>
                    <TextBlock FontWeight="Bold" Text="{Binding PropertyName, Mode=OneTime}"/>
                    <TextBlock Text="{Binding DisplayHandlerCount, Mode=OneTime}"/>
                </StackPanel>
            </DataTemplate>
        </ResourceDictionary>
    </Window.Resources>
 
    <DockPanel>
        <Button DockPanel.Dock="Top" Margin="4"
            Click="RefreshButton_Click">_Refresh</Button>
        <ScrollViewer Margin="4">
            <ItemsControl ItemsSource="{Binding ReflectProperties}"/>
        </ScrollViewer>
    </DockPanel>
</Window>

ReflectPropertyDescriptorWindow.xaml.cs:

public partial class ReflectPropertyDescriptorWindow : Window
{
    public ReflectPropertyDescriptorWindow()
    {
        InitializeComponent();
        ReflectProperties = GetReflectPropertyDescriptorInfo();
    }
 
    public static readonly DependencyProperty ReflectPropertiesProperty =
        DependencyProperty.Register("ReflectProperties", typeof(ReadOnlyCollection<ReflectPropertyDescriptorInfo>),
        typeof(ReflectPropertyDescriptorWindow), new PropertyMetadata());
 
    public ReadOnlyCollection<ReflectPropertyDescriptorInfo> ReflectProperties
    {
        get { return (ReadOnlyCollection<ReflectPropertyDescriptorInfo>) GetValue(ReflectPropertiesProperty); }
        set { SetValue(ReflectPropertiesProperty, value); }
    }
 
    private void RefreshButton_Click(object sender, RoutedEventArgs e)
    {
        ReflectProperties = GetReflectPropertyDescriptorInfo();
    }
 
    private static ReadOnlyCollection<ReflectPropertyDescriptorInfo> GetReflectPropertyDescriptorInfo()
    {
        // as shown above

    }
}

And finally, the definition of the immutable ReflectPropertyDescriptorInfo object, which is used as the source of a OneTime binding in the UI:

public sealed class ReflectPropertyDescriptorInfo : IEquatable<ReflectPropertyDescriptorInfo>,
    IComparable<ReflectPropertyDescriptorInfo>
{
    public ReflectPropertyDescriptorInfo(string typeName, string propertyName, int handlerCount)
    {
        m_typeName = typeName;
        m_propertyName = propertyName;
        m_handlerCount = handlerCount;
    }
 
    public string TypeName
    {
        get { return m_typeName; }
    }
 
    public string PropertyName
    {
        get { return m_propertyName; }
    }
 
    public int HandlerCount
    {
        get { return m_handlerCount; }
    }
 
    public string DisplayHandlerCount
    {
        get { return m_handlerCount == 1 ? "" : string.Format(CultureInfo.InvariantCulture,
            " ({0:n0} handlers)", m_handlerCount); }
    }
 
    public int CompareTo(ReflectPropertyDescriptorInfo other)
    {
        if (object.ReferenceEquals(other, null))
            return 1;
 
        int compareResult = m_typeName.CompareTo(other.m_typeName);
        if (compareResult == 0)
            compareResult = m_propertyName.CompareTo(other.m_propertyName);
        if (compareResult == 0)
            compareResult = m_handlerCount.CompareTo(other.m_handlerCount);
        return compareResult;
    }
 
    // Implementations of Equals, GetHashCode, operators, etc. elided for brevity

 
    readonly string m_typeName;
    readonly string m_propertyName;
    readonly int m_handlerCount;
}

Posted by Bradley Grainger on October 22, 2008