Reputation: 4410
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.
I have the following structure for my custom control that I'm authoring:
MyControl : FrameworkElement
MyControl.Items
MyItem : FrameworkContentElement
MyItem.SubItems
MySubItem : FrameworkContentElement
MySubItem
MyItem
MyItem
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.
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
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:
ObservableCollection<MyItem>
is set/unset.ObservableCollection<MyItem>
has an item added/removed, or if the collection is reset.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