Cascaded PropertyChangedEvents crashes with DataGridView


Bug or not?

we wanted to show different properties of objects of a BindingList in a DataGridView. Some of the properties had to change their values depending on the values of other properties. Therefore we introduced a new BindingList with an event handler 'ChildObjectChanged', that is raised when one of the items in this BindingList changes.

Also we wanted to change other values, not included in the BindingList, depending on changes in this DataGridView. Therefore we implemented a new class, that listens to the 'ChildObjectChanged' event and raises its own event to inform any listener to update its depending data.

When binding this construction to a DataGridView, a 'NullReferenceException occurred'; the stack showed this occured in DataGridViewCell.SetValue(..).

When binding other controls no errors occured, the software worked as planned.


Gerd Sauermann, Arend Sailer

[939 byte] By [GerdSauermann] at [2008-2-14]
# 1
Here is the code fragment of our BindingList and the business objects. We bound the business objects via a BindingSource to the DataGridView.



using System;

using System.Collections.Generic;

using System.Text;

using System.ComponentModel;

using System.Reflection;

using System.Collections;

using System.Diagnostics;

using System.Windows.Forms;

namespace TestPropertyChanged

{

public class MyBindingList<T> : BindingList<T>

{

public event EventHandler ChildObjectChanged;

void Subscribe(object item)

{

INotifyPropertyChanged newObject = item as INotifyPropertyChanged;

if (newObject != null)

{

newObject.PropertyChanged += new PropertyChangedEventHandler(newObject_PropertyChanged);

}

}

protected override void InsertItem(int index, T item)

{

base.InsertItem(index, item);

Subscribe(item);

}

protected override object AddNewCore()

{

object newObject = base.AddNewCore();

Subscribe(newObject);

return newObject;

}

void newObject_PropertyChanged(object sender, PropertyChangedEventArgs e)

{

if (ChildObjectChanged != null) ChildObjectChanged(this, EventArgs.Empty);

}

}

public class RawMaterial : INotifyPropertyChanged

{

public event PropertyChangedEventHandler PropertyChanged;

private double? quantity;

public double? Quantity

{

get { return quantity; }

set

{

quantity = value;

if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Quantity"));

}

}

private double? pricePerUnit;

public double? PricePerUnit

{

get { return pricePerUnit; }

set

{

pricePerUnit = value;

if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("PricePerUnit"));

}

}

public double? Price

{

get { return pricePerUnit * quantity; }

}

}

public class Calculation : INotifyPropertyChanged

{

public event PropertyChangedEventHandler PropertyChanged;

private MyBindingList<RawMaterial> rawMaterials = new MyBindingList<RawMaterial>();

public MyBindingList<RawMaterial> RawMaterials

{

get { return rawMaterials; }

set { rawMaterials = value; }

}

public Calculation()

{

rawMaterials.ChildObjectChanged += new EventHandler(rawMaterials_ChildObjectChanged);

}

void rawMaterials_ChildObjectChanged(object sender, EventArgs e)

{

if (RawMaterials.Count == 0)

{

rawMaterialCosts = null;

}

else

{

rawMaterialCosts = 0;

foreach (RawMaterial material in RawMaterials)

{

rawMaterialCosts += material.Price;

}

}

if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("RawMaterialCosts"));

}

private double? rawMaterialCosts = null;

public double? RawMaterialCosts

{

get { return rawMaterialCosts; }

}

}

}


GerdSauermann at 2007-9-8 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 2

Our list bound controls listen to IBindingList.ListChanged events. A better solution would be to hook the child INotifyPropertyChanged events and turn them into IBindingList.ListChanged events of type ItemChanged (and BindingList<T> automatically does this post Beta 2). The sample below demonstrates this - it's loosely based on what we do post Beta 2 in BindingList<T>.

Joe


class GenericBindingList<T> : BindingList<T>, IRaiseItemChangedEvents
{
private bool _raisesItemChangedEvents=false;
private PropertyDescriptorCollection _shape=null;

public GenericBindingList() : base()
{
if (typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(T)))
{
_raisesItemChangedEvents = true;

_shape = TypeDescriptor.GetProperties(typeof(T));

if (null == _shape)
{
_raisesItemChangedEvents = false;
}
}
}

private void HookPropertyChanged(T item)
{
INotifyPropertyChanged inpc = (item as INotifyPropertyChanged);

if (null != inpc)
{
inpc.PropertyChanged += new PropertyChangedEventHandler(Child_PropertyChanged);
}
}

private void UnhookPropertyChanged(T item)
{
INotifyPropertyChanged inpc = (item as INotifyPropertyChanged);

if (null != inpc)
{
inpc.PropertyChanged -= new PropertyChangedEventHandler(Child_PropertyChanged);
}
}

void Child_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (this.RaiseListChangedEvents)
{
if (string.IsNullOrEmpty(e.PropertyName))
{
ResetBindings();
}
else if (typeof(T).IsAssignableFrom(sender.GetType()))
{
T item = (T)sender;

int pos = this.IndexOf(item);

PropertyDescriptor pd = _shape.Find(e.PropertyName, true);

ListChangedEventArgs args = new ListChangedEventArgs(ListChangedType.ItemChanged, pos, pd);

OnListChanged(args);
}
}
}

#region BindingList<T> overrides

protected override void InsertItem(int index, T item)
{
if (_raisesItemChangedEvents)
{
HookPropertyChanged(item);
}

base.InsertItem(index, item);
}

protected override void RemoveItem(int index)
{
if (_raisesItemChangedEvents)
{
UnhookPropertyChanged(this[index]);
}

base.RemoveItem(index);
}

protected override void SetItem(int index, T item)
{
if (_raisesItemChangedEvents)
{
UnhookPropertyChanged(this[index]);
HookPropertyChanged(item);
}

base.SetItem(index, item);
}

protected override void ClearItems()
{
foreach (T item in this.Items)
{
UnhookPropertyChanged(item);
}

base.ClearItems();
}
#endregion

#region IRaiseItemChangedEvents Members

bool IRaiseItemChangedEvents.RaisesItemChangedEvents
{
get
{
return this._raisesItemChangedEvents;
}
}

#endregion
}

JoeStegman at 2007-9-8 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 3
And to relate this back to your first post - your BindingList implementation should fire ListChanged events of type ItemChanged when any child property changes (even a related one). Your other types would then only need to listen to ListChanged events on your BindingList.

Also note that you need to hook/unhook events in several spots as shown in my sample above.

Joe

JoeStegman at 2007-9-8 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 4
Thank you for your code example refering the BindingList problem.

Our main problem is the behaviour of the DataGridView. Is the exception, that only occurs if a PropertyChanged event is fired within a Set operation of a DataGridViewCell, a known bug?

Gerd and Arend

GerdSauermann at 2007-9-8 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 5
The DataGridView doesn't listen to PropertyChanged events - it only listens to ListChanged events. If you're binding through a BindingSource, the BindingSource will translate some PropertyChaned events to ListChanged. From you've provided, I can't tell what's happening (there are no issues related to what you're describing) however I can see where your solution will lead to problems (such as recursive property changed events getting fired, memory leaks, events getting fired on disposed objects). In order to determine the exact issue, I'd need a small repro.

Also, if you're going to sub-class BindingList<T>, I'd recommend you control child change notification through ListChanged events. When Windows Forms controls are bound to lists, they don't listen to PropertyChanged events. The best way to handle child PropertyChanged events in a sub-classed BindingList<T> is to turn them into ListChanged events (which we will automatically do in BindingList<T> post B2). The sample I provided does this and I think you'll find this fits more naturally into the Windows Forms data binding model.

Joe

JoeStegman at 2007-9-8 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...