Nicholas Miller
Nicholas Miller

Reputation: 4410

How to re-render a custom control when its sub-items and their properties change?

The original question was not knowing why the PropertyChangedCallback didn't fire, and it was caused by some code typos (too localized for SO). However, I've modified the question to address how to re-render a custom control when it has sub-items that are added/removed, or if the sub-items have properties that are changed. Please visit my answer to see in depth how to trigger the re-rendering.


The title may not be the best description of the problem, however, the problem itself isn't very clear in anyway whatsoever. I thought posting this question may be of some use because the PropertyChangedCallback is firing quite unpredictably.

I have the following structure for my custom control that I'm authoring:

So basically, my control has a DependencyProperty which stores an ObservableCollection<MyItem>. This DependencyProperty has a PropertyChangedCallback which is used to detect when the collection is set/unset. This callback function is used to subscribe to the CollectionChanged events so that I can cause a re-render of my control when a MyItem is added/removed from the collection.

My control also should re-render when the collection items have a property that changes. Therefore, MyItem also implements INotifyPropertyChanged, and I subscribe the PropertyChangedEventHandler when MyItem objects are added/removed to the collection.

So far, so good...

The MyItem class defines a few more DependencyProperies, each of which has the same PropertyChangedCallback function. To my suprise however, this callback function is not triggered when I modify one of the MyItem properties in XAML.

What I'm hoping to learn is why this is happening and why the PropertyChangedCallback is not firing. Also, I would like to know what scenarios can cause the callback to fire.

My goal is to have a control that re-renders when:
a) Its properties are changed
b) Its children are added/removed
c) Its children's properties are changed
d) Its children's children are added/removed
e) Its children's children's properties are changed.


Code Samples

Here is how I register the MyControl.Items property. This DependencyProperty is successful at firing the property changed event.

public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
    "Items", 
    typeof(ObservableCollection<MyItem>), 
    typeof(MyControl), 
    new FrameworkPropertyMetadata(
        null, //Default to null.  Instance-scope value is set in constructor.
        FrameworkPropertyMetadataOptions.AffectsRender, 
        OnItemsPropertyChanged));

This is how I respond to setting/unsetting of the MyItems collection:

private static void OnItemsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{   //This callback is called when ObservableCollection<MyItem> is set/unset.
    MyControl ctrl = (MyControl)obj;

    INotifyCollectionChanged oldList = args.OldValue as INotifyCollectionChanged;
    INotifyCollectionChanged newList = args.NewValue as INotifyCollectionChanged;

    //If the old list implements the INotifyCollectionChanged interface, then unsubscribe to CollectionChanged events.
    if (oldList != null)
        oldList.CollectionChanged -= ctrl .OnItemsCollectionChanged;
    //If the new list implements the INotifyCollectionChanged interface, then subscribe to CollectionChanged events.
    if (newList != null)
        newList.CollectionChanged += ctrl .OnItemsCollectionChanged;
}

The following function is called when an item is added or removed from the ObservableCollection<MyItem>

private void OnItemsCollectionChanged(object source, NotifyCollectionChangedEventArgs args)
{   //Invaliate the visual, causing it to re-layout and re-render.
    InvalidateVisual();

    //The contents of the Items collection was modified.
    //Subscribe/Unsubcribe to the PropertyChanged event as necessary.
    switch (args.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (MyItem mi in args.NewItems)
                mi.PropertyChanged += OnMyItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (MyItem mi in args.OldItems)
                mi.PropertyChanged -= OnMyItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Replace:
            foreach (MyItem  mi in args.NewItems)
                mi.PropertyChanged += OnMyItemPropertyChanged;
            foreach (MyItem mi in args.OldItems)
                mi.PropertyChanged -= OnMyItemPropertyChanged;
                break;
        case NotifyCollectionChangedAction.Reset:
            foreach (MyItem mi in (source as IEnumerable<MyItem >))
                mi.PropertyChanged += OnMyItemPropertyChanged;
            break;
    }
}

Now, I need to be able to react to when the MyItem has a property that changes. Therefore, I established a callback function for when the MyItem has a PropertyChanged event:

private void OnMyItemPropertyChanged(object source, PropertyChangedEventArgs args)
{   //One of the MyItems had a property that was changed, 
    //invalidate the visual and re-render.
    InvalidateVisual();
}

The previous function is never called, because the MyItem's DependencyProperties never fire the property changed event. Below illustrates how I've set up the DependencyProperties that I tried modifying in XAML:

public static readonly DependencyProperty MyIntProperty = DependencyProperty.Register(
    "MyInt", 
    typeof(int), 
    typeof(MyItem), 
    new PropertyMetadata(0, DependencyPropertyChanged));

public static readonly DependencyProperty MyDoubleProperty = DependencyProperty.Register(
    "MyDouble", 
    typeof(double), 
    typeof(MyItem), 
    new PropertyMetadata(0d, DependencyPropertyChanged));

public static readonly DependencyProperty MyStringProperty = DependencyProperty.Register(
    "MyString", 
    typeof(string), 
    typeof(MyItem), 
    new PropertyMetadata("", DependencyPropertyChanged));

The following function is the callback for those DependencyProperties. If fired, it should raise the INotifyPropertyChanged.PropertyChangedEventHandler:

private static void DependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    MyItem item= (MyItem )obj;
    item.RaisePropertyChanged(args.Property.Name);
}

protected void RaisePropertyChanged(string name)
{   //Notify listeners (such as the parent control) when a property changes.
    if(PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(name));;
}

Well... I have a similar set of events/handlers which are used for the MyItem.SubItems DependencyProperty, but at this point that isn't of any use.

If you are able to add any insight into how the PropertyChangedCallback works, I would greatly appreciate it. Thanks for reading this rather lengthy posts.

Upvotes: 3

Views: 2870

Answers (1)

Nicholas Miller
Nicholas Miller

Reputation: 4410

I apologize, but the original question is too localized (small typos, and copy-paste errors). However, to make this page useful, I have prepared a full explanation for how to create a custom control which has sub items or sub-sub items inside of it. This page also explains how to configure each item so that property changes and collection changes cause a re-render on the original control.

First, the custom control must contain a DependencyProperty (and CLR-backed property) for a collection of items. This collection should implement INotifyCollectionChanged, which makes ObservableCollection a good choice. This collection should be parameterized to hold the sub items for the control. Using the names from the original post, this requires code that looks like the following:

[ContentProperty("Items")] //This allows the "Items" property to be implicitly used in XAML.
public class MyControl : Control
{
    public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
        "Items", 
        typeof(ObservableCollection<MyItem>), 
        typeof(MyControl),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnItemsChangedProperty));

    //CLR-property.
    [Category("MyControl")]
    public ObservableCollection<MyItem> Items
    {
        get { return (ObservableCollection<MyItem>)GetValue(ItemsProperty); }
        set { SetValue(ItemsProperty, value); }
    }

    public MyControl() : base()
    {   //Set a new collection per control, but don't destroy binding.
        SetCurrentValue(ItemsProperty, new ObservableCollection<MyItem>());
    }

    protected override void OnRender(DrawingContext dc)
    {
        //Draw stuff here.
    }

    //More methods defined later...
}

At this point, re-rendering is triggered when the ObservabledCollection<MyItem> is set and unset. This collection is set automatically when the control is instantiated, which causes the first re-render.

Next, the collection should be monitored to detect when items are added and removed. To do this, we must use the PropertyChangedCallback function that is provided to the DependencyProperty. This function simply subscribes/unsubscribes to CollectionChanged events depending on if the collection of items is set or unset:

private static void OnItemsChangedProperty(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    MyControl ctrl = (MyControl)obj;

    INotifyCollectionChanged oldList = args.OldValue as INotifyCollectionChanged;
    INotifyCollectionChanged newList = args.NewValue as INotifyCollectionChanged;

    //If the old list implements the INotifyCollectionChanged interface, then unsubscribe to CollectionChanged events.
    if (oldList != null)
        oldList.CollectionChanged -= ctrl.OnItemsCollectionChanged;
    //If the new list implements the INotifyCollectionChanged interface, then subscribe to CollectionChanged events.
    if (newList != null)
        newList.CollectionChanged += ctrl.OnItemsCollectionChanged;
}

And below is the callback function that handles when items are added/removed. A re-render is triggered in here as well:

private void OnItemsCollectionChanged(object source, NotifyCollectionChangedEventArgs args)
{
    InvalidateVisual(); //Re-render MyControl

    switch (args.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (MyItem item in args.NewItems)
                item.PropertyChanged += OnItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (MyItem item in args.OldItems)
                item.PropertyChanged -= OnItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Replace:
            foreach (MyItem item in args.NewItems)
                item.PropertyChanged += OnItemPropertyChanged;
            foreach (MyItem item in args.OldItems)
                item.PropertyChanged -= OnItemPropertyChanged;
                break;
        case NotifyCollectionChangedAction.Reset:
            foreach (MyItem item in (source as IEnumerable<MyItem>))
                item.PropertyChanged += OnItemPropertyChanged;
            break;
    }
}

As you can see in the above function, the PropertyChanged event, which is defined in MyItem is subscribed/unsubscribed to as necessary so that the custom control can be notified when a property inside the MyItem class is changed. This allows the control to re-render when it's sub-items have a property changed.

Here is the handler for when the sub-items' properties change:

private void OnItemPropertyChanged(object source, PropertyChangedEventArgs args)
{
    InvalidateVisual(); //Just re-render.
}

At this point the custom control will re-render as a result of the following situations:

  • The ObservableCollection<MyItem> is set/unset.
  • The ObservableCollection<MyItem> has an item added/removed, or if the collection is reset.
  • The ObservableCollection<MyItem> has an item that has a property that is changed.

The final step to completing this explanation is to implement the INotifyPropertyChanged interface within the MyItem class. The PropertyChanged event is simply invoked when any of the DependencyProperties is changed. See the code below:

public class MyItem : FrameworkContentElement, INotifyPropertyChanged
{
    //INotifyPropertyChanged members:
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }

    //DependencyProperties
    public static readonly DependencyProperty MyIntProperty = DependencyProperty.Register(
        "MyInt", 
        typeof(int), 
        typeof(MyItem), 
        new PropertyMetadata(0, DependencyPropertyChanged));
    public static readonly DependencyProperty MyStringProperty = DependencyProperty.Register(
        "MyString", 
        typeof(string), 
        typeof(MyItem), 
        new PropertyMetadata("", DependencyPropertyChanged));

    //Callback that invokes the INotifyPropertyChanged.PropertyChangedEventHandler
    private static void DependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        MyItem item = (MyItem)obj;
        item.RaisePropertyChanged(args.Property.Name);
    }
}

If desired, this pattern can be repeated to allow sub-sub items to cause re-rendering of the custom control. Simply establish a DependencyProperty in the first sub-item that contains another ObservableCollection, just like the MyControl class. However, instead of causing a re-render directly when the sub-sub items have a PropertyChanged event, call the RaisePropertyChanged method to pass the notification back to the parent-most control.

I hope this helps any control authors manage the re-rendering of their controls! :)

Upvotes: 5

Related Questions