0

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.
wondra
  • 3,271
  • 3
  • 29
  • 48
  • Also take a look at this: https://stackoverflow.com/a/15023687/1136211 – Clemens Jul 15 '19 at 09:41
  • That doesn't matter. The point is that you have to attach a CollectionChanged event handler in the PropertyChangedCallback. – Clemens Jul 15 '19 at 09:52
  • This has nothing to do with dependency property vs attached property. The `CollectionChanged` callback (`DependencyPropertyChangedCallback`) is only invoked when the dependency property gets a new collection assigned. Inside the `ColectionChanged` callback you have to subscribe to the `MyCollection.CollectionChanged` event too to get notified when the actual collection (and not only the property) has changed. – BionicCode Jul 15 '19 at 09:55
  • @Clemens sorry, my mistake but the answer still does not apply, please see edit - if you set the value in XAML, it would *re-use* default value rather than instantiating new `ObservaleCollection` and because of that the `PropertyChangedCallback` will never be called in first place (and answer is *is* called once), it I will never give me the chance to register `NotifyCollectionChanged`. Alternatively, if you still do not trust me the answer does apply, please do try the provided example with answer aplied (point 3. of what I tried). – wondra Jul 15 '19 at 09:58
  • Afaik there is no way to safely provide a non-null default value for a collection-type attached property. You would have to do an explicit collection instance creation in XAML: `... elements ... ` – Clemens Jul 15 '19 at 10:01
  • @Clemens in case if I do not try to provide default value, how do I avoid the problem `null` causing `XamlObjectWriterException` (i.e. "what I tried point 1.")? – wondra Jul 15 '19 at 10:03
  • You would have to make an explicit collection instance creation in XAML: `... elements ... ` – Clemens Jul 15 '19 at 10:04
  • If you provide a collection in XAML like this, why is it an ObservableCollection? Do you ever add or remove elements later? – Clemens Jul 15 '19 at 10:08
  • @Clemens while not absolutely necessary, it would be nice to be defined of items changing. I would prefer to avoid to the bloat (and headache of correctly derving collections) of `class MyCollectionCollection : ObservableCollection` but using `ObservableCollection` directly does not seem to be possible in XAML. – wondra Jul 15 '19 at 10:12
  • Have you seen this? https://stackoverflow.com/q/2695847/1136211. It won't compile in my system however... – Clemens Jul 15 '19 at 10:13
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/196460/discussion-between-wondra-and-clemens). – wondra Jul 15 '19 at 10:14

1 Answers1

1

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)
        {
            // ...
        }
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268