6
\$\begingroup\$

This is an experiment to investigate how to wire up binding and inheritance in a composable, custom control complex.

Custom Control Structure

There is a custom parent (TestParent) with multiple custom child controls plugged into a ObservableCollection, DP on the parent. The Parent derives from Label and the children from FrameworkElement and the Label Content is updated by a Refresh method on the parent, based on property change notifications (just for experimental purposes).

Composability

The ObservableCollection DP on the parent control provides a site for attaching the child elements. This is supported by some glue logic to wire up the Logical relationships, between parent and child, that are required to support binding and implicit inheritance. The ObservableCollection needs to be initialised in the parent, instance constructor in order to provide a unique collection for each instance.

Property Inheritance

The parent has an Attached Property (AttachedString) that is inherited by the child elements. On the parent and on one of the child elements, this property is bound to a ComboBox. As per normal inheritance behaviour, when the value of the AP on the parent is changed, the unbound children will dynamically inherit the new value. The child class also has a property that does not inherit from the parent.

![![enter image description here

Test Page

The View includes of some ComboBox elements to provide changeable sources to test binding to the custom control complex at parent and child levels. The aim is to check that binding will work at both levels and that an attached property on the parent can be implicitly inherited onto its children.

<Window x:Class="APTest.Spec.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cb="clr-namespace:CollectionBinding;assembly=DPCollection3"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel Margin="10,10,10,10">
        <StackPanel.Resources>
            <x:Array x:Key="Items" Type="system:String">
                <system:String>Name</system:String>
                <system:String >Command</system:String>
                <system:String>CommandParameter</system:String>
            </x:Array>

        </StackPanel.Resources>

        <StackPanel  Margin="0,0,0,70">
            <DockPanel DockPanel.Dock="Top">
                <Label DockPanel.Dock="Left" MinWidth="110">Parent String</Label>
                <ComboBox x:Name="ParentString" SelectedIndex="1"
                          ItemsSource="{StaticResource Items}">
                </ComboBox>
            </DockPanel>
            <DockPanel DockPanel.Dock="Bottom">
                <Label DockPanel.Dock="Left" MinWidth="110">Child String</Label>
                <ComboBox x:Name="ChildString"
                          ItemsSource="{StaticResource Items}">
                </ComboBox>
            </DockPanel>
            <DockPanel DockPanel.Dock="Bottom">
                <Label DockPanel.Dock="Left" MinWidth="110">Unattached String</Label>
                <ComboBox x:Name="UnattachedChildString"
                          ItemsSource="{StaticResource Items}" SelectedIndex="1">
                </ComboBox>
            </DockPanel>
        </StackPanel>

        <!--Custom Control-->
        <cb:TestParent x:Name="Test" Margin="110,0,0,0"
                   AttachedString="{Binding ElementName=ParentString, 
                                    Path=SelectedValue}" Background="#FFEBEBEB" >
            <cb:TestParent.MyItems>
                <cb:TestChild UnAttachedString="{Binding ElementName=UnattachedChildString, 
                                Path=SelectedValue}" />
                <cb:TestChild AttachedString=
                           "{Binding ElementName=ChildString, 
                                Path=SelectedValue,
                                    PresentationTraceSources.TraceLevel=High}" />
                <cb:TestChild />
            </cb:TestParent.MyItems>
        </cb:TestParent>

    </StackPanel>
</Window>

enter image description here

Maintaining Inheritance and Binding

The key to assuring proper binding and inheritance behaviour is to maintain the integrity of the WPF Logical Tree. If the child elements are added without attending to this, then the binding and inheritance context for them will be unknown, because they have no Logical relationship to the other Framework elements. In other words they will not be connected to the Logical Tree.

The Logical Tree

My initial understanding was that the binding and inheritance would "just work" if I used a FreezableCollection to receive the child elements, but this does not seem to be the case so I used a simpler ObservableCollection.
To make it work I had to do three things:

  1. Use a FrameworkElement (or derivative) as the child (collection member) element
  2. Manually add (and remove) the child elements to the Logical Tree
  3. Override the parent LogicalChildren Enumerator to return the Enumerator for the ObservableCollection

This ensured that the required infrastructure was in place but two more steps were required to establish inheritance:

  1. Registering the inheritted property on the parent as an Attached Property (AP)
  2. Add a DP on the child class and instead of Register-ing it, set it to reference the parent type's AP using the AddOwner method of the AP on the parent type. This allows the child to modify the AP metadata so that it can subscribe to the PropertyChanged event and also set a local default value. The default value can also be linked to the AP default via it's metadata.

Change Notification

Because the Collection does not notify on changes within members (I was hoping that the FreezableCollection would take care of this), I had to implement INotifyPropertyChanged on the child class and connect it to the property changed callbacks of it's DPs. The resulting PropertyChangedevents had to be subscribed to in the parent class, when each child was added.

The DependencyPropertyChangedEventArgs class has usefull state (such as OldValue and NewValue) that is not supported by InotifyPropertyChanged. In order to harmonise the property changed notifications, PropertyChangedEventArgs (from the InotifyPropertyChanged interface) was subclassed to add the additional state.

There was also a notification race condition, due to inheritance, that had to be quenched. Two notification paths are started when the parent AP is changed: notification from the parent and notification from the inheriting children; and both paths ended with the Refresh method which needed to modify the parent content property. The first path was trying to do this while the other was accessing the Logical Tree and this caused an error: changes to state are not allowed while a tree walk is ongoing.

My first attempt to managed this was by using the DependencyPropertyHelper, in the parent, to detect if the change in value was inherited or not and to ignore them if they were. The current version got rid of all of that logic and instead, used the parent instance Dispatcher to queue the updates, running them asynchronously, so that they don't clash. This also eliminated the problem but without using reflection.

Code

using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;

namespace CollectionBinding
{
    public class TestParent : Label
    {
        #region refresh
        private static void Refresh (TestParent instance)
        {
            instance.Content = string.Format("parent value:\t{0}", instance.AttachedString);
            foreach (var myItem in instance.MyItems)
            {
                instance.Content += string.Format("\n  child value:\t{0}\t{1}",
                                myItem.AttachedString, myItem.UnAttachedString);
            }
            instance.Content += string.Format("\n Attached DP References are{0} equal",
                TestParent.AttachedStringProperty == TestChild.AttachedStringProperty
                ? "" : " NOT");

        }

        public static void RefreshAsync(TestParent tp)
        {
            tp.Dispatcher.InvokeAsync(() => Refresh(tp));
        }

        private void OnPropertyChanged (object oldvalue, object newvalue)
        {
            if (oldvalue != null && oldvalue.Equals(newvalue)) return;
            RefreshAsync(this);
        }

        #endregion

        #region Inheritable AP string AttachedString

        public static readonly
            DependencyProperty AttachedStringProperty =
                DependencyProperty.RegisterAttached(
                    "AttachedString", typeof(string),
                    typeof(TestParent),
                    new FrameworkPropertyMetadata("Not Set in Parent",
                        FrameworkPropertyMetadataOptions.Inherits, OnStringChanged));

        public static void SetAttachedString(DependencyObject target, string value)
        {
            target.SetValue(AttachedStringProperty, value);
        }

        public static string GetAttachedString(DependencyObject target)
        {
            return (string) target.GetValue(AttachedStringProperty);
        }

        private static void OnStringChanged(DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var instance = d as TestParent;
            if (instance == null) return;
            instance.OnPropertyChanged(e.OldValue, e.NewValue);
        }

        public string AttachedString
        {
            get { return GetAttachedString(this); }
            set { SetAttachedString(this, (string) value); }
        }

        #endregion

        #region DP ObservableCollection<TestChild> MyItems

        public static readonly DependencyProperty MyItemsProperty =
            DependencyProperty.Register(
                "MyItems", typeof(ObservableCollection<TestChild>),
                typeof(TestParent),
                new PropertyMetadata(default(ObservableCollection<TestChild>)));

        public ObservableCollection<TestChild> MyItems
        {
            get { return (ObservableCollection<TestChild>) GetValue(MyItemsProperty); }
            set { SetValue(MyItemsProperty, value); }
        }

        private void MyItem_Changed(object s, PropertyChangedEventArgs args )
        {
            var e = args as MyPropertyChangedEventArgs;
            if (e == null)
                OnPropertyChanged(new object(), new object());
            else
                OnPropertyChanged(e.OldValue, e.NewValue);
        }

        void MyItems_Changed(object d, NotifyCollectionChangedEventArgs e)
        {
            var addedItems = e.NewItems as IList;
            var deletedItems = e.OldItems as IList;

            if (addedItems != null)
            {
                foreach (var addedItem in addedItems)
                {
                    this.AddLogicalChild((TestChild) addedItem);
                    ((TestChild)addedItem).PropertyChanged += MyItem_Changed;
                }
            }
            if (deletedItems != null)
            {
                foreach (var deletedItem in deletedItems)
                {
                    this.RemoveLogicalChild((TestChild) deletedItem);
                    ((TestChild)deletedItem).PropertyChanged -= MyItem_Changed;
                }
            }
        }

        #endregion

        // Connect to Logical Tree
        protected override IEnumerator LogicalChildren
        {
            get { return MyItems.GetEnumerator(); }
        }

        public TestParent()
        {
            MyItems = new ObservableCollection<TestChild>();
            MyItems.CollectionChanged += MyItems_Changed;
        }

        static TestParent()
        {
            // Use standard label template
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TestParent),
                new FrameworkPropertyMetadata(typeof(Label)));
        }
    }

    public class TestChild : FrameworkElement, INotifyPropertyChanged
    {
        #region DP string UnAttachedString

        public static readonly DependencyProperty UnAttachedStringProperty = 
            DependencyProperty.Register(
                "UnAttachedString", typeof(string), 
                typeof(TestChild),
                new PropertyMetadata(default(string), OnStringChanged));

        public string UnAttachedString
        {
            get { return (string) GetValue(UnAttachedStringProperty); }
            set { SetValue(UnAttachedStringProperty, value); }
        }

        #endregion

        #region Inherited AP string AttachedString

        public static readonly DependencyProperty AttachedStringProperty =
            TestParent.AttachedStringProperty.AddOwner(typeof(TestChild),
                new FrameworkPropertyMetadata(
                    (object) TestParent.AttachedStringProperty.DefaultMetadata.DefaultValue,
                        OnPropertyChanged));

        private static void OnPropertyChanged(DependencyObject d,
                                            DependencyPropertyChangedEventArgs e)
        {
            ((TestChild) d).OnPropertyChanged(e.OldValue, e.NewValue, e.Property.Name);
        }

        public string AttachedString
        {
            get { return (string) GetValue(AttachedStringProperty); }
            set { SetValue(AttachedStringProperty, (string) value); }
        }

        #endregion

        #region INotifyPropertyChanged implimentation
        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged(object oldValue, object newValue,
            [CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this,
                    new MyPropertyChangedEventArgs(propertyName, oldValue, newValue));
        }

        #endregion
    }

    public class MyPropertyChangedEventArgs : PropertyChangedEventArgs
    {
        public MyPropertyChangedEventArgs(string propertyName, object oldValue, 
            object newValue) : base(propertyName)
        {
            OldValue = oldValue;
            NewValue = newValue;
        }

        public object OldValue;
        public object NewValue;
    }
}

Handling DP Callbacks

Because DPs are registered as static members, the callback also has to be static. This means that a technique is needed to carry the callback into the instance domain. The EventArgs derived classes support this by including a reference to the instance. The static callback can use this to access the instance domain. Based on my reading of the WPF source code, the standard way to implement this is to have a static and dynamic callback, the later invoked by the former, of the same name, to transform between domains. The standard signature for the instance callback (again based on the WPF source code) is Callback(object oldvalue, object newvalue).

Review

I'm aware that I'm re-inventing a ListControl here but I wanted to try to understand how the framework element plumbing works. There were a few things that felt strange, like having to filter out the inherited changes (although this was eliminated by invoking the Refresh method asynchronously) and implementing INotifyPropertyChanged on dependency properties for example and I'm not sure about the whole child collection approach. Anyway, all feed-back and critique is most welcome.

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$
  1. You can drop equality checks in OnPropertyChanged since PropertyChanged callback only triggers when dependency property actually changes.
  2. Your properties can use a better naming. AttachedString doesn't tell me anything. I can already see that the property returns string and I can see that it is an attached property. What I do not know is its purpose. Use property name to tell me that.
  3. Your class wont work if I set MyItems from code. This is counter-intuitive. Either make the setter private or make it work.

  4. You are leaking event handlers. Which might not be an issue for your current use case, but it can quickly become one, once stuff gets serious. Either use weak events or unsubscribe manually.

  5. Label is a really poor choice for a base class. There are two main approaches when it comes to controls, that host multiple children:

    • Panel approach. Panels are responsible for arranging Visuals in available space.

    • ItemsControl approach. ItemsControl is responsible for assigning templates to arbitrary items and then hosting those templates in specified Panel.

    Note, that in neither of those approaches parent is responsible for actually rendering children. You are having so much trouble updating your parent precisely because you've neglected this principle.

The problem is that I do not know how many wheels you want to reinvent. The most straightforward way to implement the behavior you want is to declare an attached property:

static class Attached
{
    public static readonly DependencyProperty StringProperty = DependencyProperty.RegisterAttached(
        "String", typeof (string), typeof (Attached), new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.Inherits));

    public static void SetString(DependencyObject element, string value)
    {
        element.SetValue(StringProperty, value);
    }

    public static string GetString(DependencyObject element)
    {
        return (string) element.GetValue(StringProperty);
    }
}

...and that's it. The rest can be implemented using regular controls and bindings. For example:

<StackPanel Orientation="Vertical" 
            Attached.String="{Binding SelectedValue, ElementName=ParentString}">
    <StackPanel.Resources>
        <Style TargetType="Label">
            <Setter Property="Content" 
                    Value="{Binding (Attached.String), RelativeSource={RelativeSource Self}}"/>
        </Style>
    </StackPanel.Resources>
    <Label Content="{Binding SelectedValue, ElementName=UnattachedChildString}"/>
    <Label Attached.String="{Binding SelectedValue, ElementName=ChildString}"/>
    <Label />
</StackPanel>

From here you can take it into different directions depending on how deep you want to go.

  • You can define a special Label template, that would automatically change depending on the value of your attached property.
  • You can extend Label class and write a custom control (for child elements), which would update label's content depending on the values of some other custom properties, including attached ones.
  • You can extend Control class and write a custom template for it.
  • You can extend FrameworkElement class, and write your own rendering logic on low level using DrawingContext.
  • Etc.

I don't think you are going to need a special TestPanel for any of those options, simple StackPanel (or any other panel), should work just fine. But it is hard to tell for sure since your code is rather hypothetical-ish. I do not know the actual use case or an actual problem you are trying to solve (if any).

\$\endgroup\$
6
  • \$\begingroup\$ Ok thanks. Leaking events where? I unsubscribe manually already... \$\endgroup\$
    – Cool Blue
    Commented Dec 22, 2016 at 12:30
  • \$\begingroup\$ @CoolBlue, you only unsubscribe from PropertyChanged and only when item is manually removed from the collection. Unsubscribtion won't trigger if veiw is unloaded, if MyItems property changes, or if collection is reset. And there is also MyItems.CollectionChanged, which you never unsubscribe from. \$\endgroup\$
    – Nikita B
    Commented Dec 22, 2016 at 12:57
  • \$\begingroup\$ By the way, the reason I had the equality check was in case I got double notification from an inherited change. Is that not possible? \$\endgroup\$
    – Cool Blue
    Commented Dec 22, 2016 at 13:13
  • \$\begingroup\$ @CoolBlue, I am not sure. I guess it's possible. But you shouldn't really have a PropertyChanged event on your childs in the first place. \$\endgroup\$
    – Nikita B
    Commented Dec 22, 2016 at 14:06
  • 1
    \$\begingroup\$ @CoolBlue, I mean, you don't need notifications for the case you presented. As you can see in my example above, the same thing can be done using regular StackPanel, which obviously does not subscribe to any events, yet it works. If you really need to subscribe to notifications, I think the proper way to do it is to fire some bubbling routed event and then handle it differently depending on where it originated. If you need a more specific advice, you should post a question where you describe and solve an actual problem, instead of a hypothetical example. \$\endgroup\$
    – Nikita B
    Commented Dec 22, 2016 at 14:50

Not the answer you're looking for? Browse other questions tagged or ask your own question.