0

I am busy creating a user control that has some basic charting/graph functions. In essence I want to have an "Items" dependency property to which the user of the control can bind. The control will then display all the items and updates made to the source.

What I have done so far was to create an "Items" DP in my user control, code behind.

public static readonly DependencyProperty ItemsProperty =
    DependencyProperty.Register("Items",
    typeof(ObservableCollection<Polyline>),
    typeof(RPGraph),
    new FrameworkPropertyMetadata(
    new ObservableCollection<Polyline>(),
    new PropertyChangedCallback(OnItemsChanged)));

public ObservableCollection<Polyline> Items 
{
    get { return (ObservableCollection<Polyline>)GetValue(ItemsProperty); }
    set { SetValue(ItemsProperty, value);  }
}

public static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}

My first stumbling block was that "OnItemsChanged" didn't get called when my collection changed. After a couple of hours I found a stackoverflow post explaining why (ObservableCollection dependency property does not update when item in collection is deleted). Following this advice solved one part of my problem. Now I could Add new items (Polylines) to the ObservableCollection list. But what if I added an extra point or modified a point in the Polyline. Armed with the knowledge of the previous problem I found the Points.Changed event. I then subscribed to it and placed the update code in there.

This finally works, but man there must be a better or more elegant way of achieving this (as stated at the top), which I think all boils down to not using ObservableCollection? Any advice?

Below is the working OnItemChanged method (excuse the draft code, I just wanted to get it working :-) :

    public static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {     
        var thisControl = d as RPGraph;

        foreach (Polyline poly in thisControl.Items)
            thisControl.manager.Items.Add(poly.Points.ToArray());

        if (e.OldValue != null)
        {
            var coll = (INotifyCollectionChanged)e.OldValue;

            // Unsubscribe from CollectionChanged on the old collection
            coll.CollectionChanged -= Items_CollectionChanged;
        }

        if (e.NewValue != null)
        {
            var coll = (ObservableCollection<Polyline>)e.NewValue;

            // Subscribe to CollectionChanged on the new collection
            coll.CollectionChanged += (o, t) => {
                ObservableCollection<Polyline> items = o as ObservableCollection<Polyline>;

                thisControl.manager.Items.Add(items[t.NewStartingIndex].Points.ToArray());

                foreach (Polyline poly in items)
                {
                    poly.Points.Changed += (n, m) => {

                        for (int i = 0; i < thisControl.manager.Items.Count; i++)
                            thisControl.manager.Items[i] = thisControl.Items[i].Points.ToArray();

                        thisControl.manager.DrawGraph(thisControl.graphView);
                    };
                }
                thisControl.manager.DrawGraph(thisControl.graphView); 
            };
        }
        thisControl.manager.DrawGraph(thisControl.graphView);  
    }
Community
  • 1
  • 1
Avalan
  • 159
  • 1
  • 10
  • AFAIK there is no other way of doing this. You have to use an event handler at each level of the hierarchy that you want to monitor. For example, monitor the list to detect insertions and deletions, then monitor each element in the list to detect when a property is changed, etc etc. As you have noted this leads to complex long-winded code. – Steven Rands Jan 27 '15 at 16:46

1 Answers1

0

You are completely right, an ObservableCollection does not notify when any of its items changes its property value.

You could extend the functionality of ObservableCollection adding notifications for these cases.

It may look like this:

public sealed class ObservableNotifiableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public event ItemPropertyChangedEventHandler ItemPropertyChanged;
    public event EventHandler CollectionCleared;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
    {
        base.OnCollectionChanged(args);

        if (args.NewItems != null)
        {
            foreach (INotifyPropertyChanged item in args.NewItems)
            {
                item.PropertyChanged += this.OnItemPropertyChanged;
            }
        }

        if (args.OldItems != null)
        {
            foreach (INotifyPropertyChanged item in args.OldItems)
            {
                item.PropertyChanged -= this.OnItemPropertyChanged;
            }
        }
    }

    protected override void ClearItems()
    {
        foreach (INotifyPropertyChanged item in this.Items)
        {
            item.PropertyChanged -= this.OnItemPropertyChanged;
        }

        base.ClearItems();
        this.OnCollectionCleared();
    }

    private void OnCollectionCleared()
    {
        EventHandler eventHandler = this.CollectionCleared;
        if (eventHandler != null)
        {
            eventHandler(this, EventArgs.Empty);
        }
    }

    private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        ItemPropertyChangedEventHandler eventHandler = this.ItemPropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new ItemPropertyChangedEventArgs(sender, args.PropertyName));
        }
    }
}

Then you can subscribe to the ItemPropertyChanged event and do your stuff.

dymanoid
  • 14,771
  • 4
  • 36
  • 64
  • This looks like a much better approach and provides a reusable solution. After searching more in line with this solution I actually noticed that there are some answers to my question already, like this one http://stackoverflow.com/questions/269073/observablecollection-that-also-monitors-changes-on-the-elements-in-collection which explains why you need to override the ClearItems etc. Thanks, it opened up I new world. Regarding the above code, this will only work for object which implements INotifyPropertChanged right? – Avalan Jan 27 '15 at 20:06
  • 1
    @Avalan, yes, exactly. This is why you see `where T : INotifyPropertyChanged`. You just need the `PropertyChanged` event. Alternatively, you could update this code and use duck typing instead of expicitly requiring the interface. – dymanoid Jan 27 '15 at 20:20
  • Nice! I just learned something new, "Duck Typing" (with a good explanation here: http://ericlippert.com/2014/01/02/what-is-duck-typing ) Thanks, I am now well on my way implementing a neat piece of code! – Avalan Jan 28 '15 at 07:00