Search code examples
c#wpfxamlattached-properties

WPF Attached Property unable to specify Changed Callback for collection


EDIT: In order to clear up all confusions with instanct-closing as duplicates. Please see point (3.) explaining why the accepted answer does not apply. In short, the linked answer is fine as long as you are not using XAML to set the value because XAML will never call PropertyChangedCallback because it re-uses the default instance.


Question:
Considering a simple WPF's Attached Property of ObservableCollection<T> type with XAML-defined value:

// public static class MyCollectionExetension.cs
public static ObservableCollection<int> GetMyCollection(DependencyObject obj)
{
    return (ObservableCollection<int>)obj.GetValue(MyCollectionProperty);
}

public static void SetMyCollection(DependencyObject obj, ObservableCollection<int> value)
{
    obj.SetValue(MyCollectionProperty, value);
}

public static readonly DependencyProperty MyCollectionProperty =
    DependencyProperty.RegisterAttached("MyCollection", typeof(ObservableCollection<int>), 
    typeof(MyCollectionExetension), new PropertyMetadata(null);

public static void DoThisWhenMyCollectionChanged(DependencyObejct assignee, IEnumerable<int> newValues) {
   // how can I invoke this?
}

//UserControl.xaml
<Grid xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <b:DataGridExtensions.MyCollection >
        <sys:Int32>1</sys:Int32>
        <sys:Int32>2</sys:Int32>
    </b:DataGridExtensions.MyCollection>
</Grid>

How can I hook collection changed events with access to the both the DependencyObject it is attached to and the new items? MyCollection must be definable in XAML.
It seems simple at first, but none of following worked for me:

  1. Set callback new UIPropertyMetadata(null, CollectionChanged) causes crash:

XamlObjectWriterException: 'Collection property 'System.Windows.Controls.Grid'.'MyCollection' is null.'

  1. OK, let's provide default value in order to avoid the crash above: new UIPropertyMetadata(new ObservableCollection<int>(), CollectionChanged) That, however prevent CollectionChanged from ever firing due to XAML not instantiating new collection but rather adding items to existing collection.

  2. Fixing the above and hook CollectionChanged while providing default value new UIPropertyMetadata(ProvideWithRegisteredCollectionChanged(), CollectionChanged)does not work neither because there is no way to pass the DependencyProperty to ProvideWithRegisteredCollectionChanged() method due to being in static context.

  3. Coalescing in MyCollection either in GetMyCollection() getter or CoerceValueCallback does not prevent the crash from point 1. above since it does not seem to be called before property is first accessed.

Solution

  • You can't correctly assign a non-null default value for a collection-type attached property. Hence you have to create an instance in XAML.

    Since declaring an ObservableCollection directly in XAML seems not easily possible, declare an appropriate derived type:

    public class MyCollection : ObservableCollection<int>
    {
    }
    

    and create an instance in XAML like this:

    <Grid>
        <b:MyCollectionExtension.MyCollection>
            <b:MyCollection>
                <sys:Int32>1</sys:Int32>
                <sys:Int32>2</sys:Int32>
            </b:MyCollection>
        </b:MyCollectionExtension.MyCollection>
    </Grid>
    

    The attached property declaration should look like shown below, including the code that attaches and detaches a CollectionChanged event handler.

    public static class MyCollectionExtension
    {
        public static MyCollection GetMyCollection(DependencyObject obj)
        {
            return (MyCollection)obj.GetValue(MyCollectionProperty);
        }
    
        public static void SetMyCollection(DependencyObject obj, MyCollection value)
        {
            obj.SetValue(MyCollectionProperty, value);
        }
    
        public static readonly DependencyProperty MyCollectionProperty =
            DependencyProperty.RegisterAttached(
                "MyCollection",
                typeof(MyCollection),
                typeof(MyCollectionExtension),
                new PropertyMetadata(MyCollectionPropertyChanged));
    
        public static void MyCollectionPropertyChanged(
            DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var oldCollection = e.OldValue as MyCollection;
            var newCollection = e.NewValue as MyCollection;
    
            if (oldCollection != null)
            {
                oldCollection.CollectionChanged -= MyCollectionChanged;
            }
            if (newCollection != null)
            {
                newCollection.CollectionChanged += MyCollectionChanged;
            }
        }
    
        public static void MyCollectionChanged(
            object o, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                // ...
            }
        }
    }