Introduction
I've blogged before about the benefit of using C# INotifyPropertyChanged
with BindingList<T>
. BindingList<T>
automatically fires ListChanged
events whenever list items get added, removed, or reordered. But, there's an additional ListChanged
event type: ItemChanged
. This event type is fired whenever a property on any list item changes. However, this event won't fire automatically unless your list items implement the INotifyPropertyChanged
interface.
The mechanism here is the C# BindingList<T>
that detects the INotifyPropertyChanged
interface on list items as they get added. The list wires a delegate to listen to PropertyChanged
events on all of the list items. These events are then translated into the ListChanged
events we're looking for.
Trouble comes when you serialize and deserialize your list. Perhaps you're reading stored objects from a file, or you're receiving them over a remote connection. Because C# event handlers are not serializable, the BindingList
's listener connections are lost during the serialization process for subtypes, and they don't get rewired when the list is deserialized. It's straightforward to do that, but the implementation is missing from the .NET SDK, so we have to write it ourselves.
Interestingly, this problem doesn't show up with concrete instances of BindingList<T>
� only its subtypes. Why would you ever want to extend BindingList<T>
? Perhaps it's to add domain specific features, or perhaps it's to add generic sorting capabilities like the SortableBindingList<T>
class described in this great article by Michael Weinhardt.
Demonstrating the problem
First, let's look at an example that demonstrates how the deserialization problem occurs. We'll start with this little utility method that takes a generic object, serializes it to a MemoryStream
, and then rebuilds a copy of the object by deserializing the output.
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace FixBindingList
{
public static class SerializeUtility
{
public static T SerializeAndDeserialize<T>(T obj)
{
T retval;
using (MemoryStream outputStream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(outputStream, obj);
using (MemoryStream inputStream =
new MemoryStream(outputStream.ToArray()))
{
retval = (T)formatter.Deserialize(inputStream);
}
}
return retval;
}
}
}
We're going to use this method to serialize, then deserialize, an extension to BindingList
to test if it preserves event listeners. This is essentially what would happen if you sent the object over a remote connection or retrieved it from a file.
Next comes the item to add to the list. Let's create a simple bank Account
class and have it implement INotifyPropertyChanged
:
using System;
using System.ComponentModel;
namespace FixBindingList
{
[Serializable]
public class Account : INotifyPropertyChanged
{
private decimal balance;
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
public decimal Balance
{
get
{
return balance;
}
set
{
balance = value;
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs("Balance"));
}
}
}
}
}
When we add Account
instances to our list, the list will wire listeners to the PropertyChanged
event. Two things about the event are noteworthy right now. First, we must declare it NonSerialized
. The event listeners won't get serialized anyway, so deserialization would throw a SerializationException
saying "Cannot get the member 'Child_PropertyChanged
'." We avoid this by telling .NET to not even try to deal with it (hence, setting the stage for later trouble).
The other thing to note is the "field:
" prefix on the attribute. What's that all about? Well, C# event members are really more like property members than field members. You can't actually "serialize" behavior � only state. The compiler will, behind the scenes, create a hidden field for you to maintain a collection of all your listeners. The event member encapsulates the behavior of adding to and removing from that field. So, the "field:
" prefix is saying "this attribute doesn't apply to the event, it's intended for the hidden field created to support the event." Esoteric, indeed!
Up next is a class that extends C# BindingList<T>
. This is a trivial class, for now, that adds no new state or behavior. If you didn't have some other compelling reason to extend this class (such as Michael Weinhardt's solution I mentioned earlier), you wouldn't even need to do this. But, we'll go ahead and create a subtype anyway to elucidate the bug:
using System;
using System.ComponentModel;
namespace FixBindingList
{
[Serializable]
public class MyBindingList<T> : BindingList<T>
{
}
}
Now, we're all set for a test program that demonstrates the problem:
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace FixBindingList
{
public class TestProgram
{
static bool itemChangedEventReceived;
static void acctList_ListChanged
(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.ItemChanged)
{
itemChangedEventReceived = true;
}
}
static void Main(string[] args)
{
Account acct = new Account();
MyBindingList<Account> acctList = new MyBindingList<Account>();
acctList.Add(acct);
acctList.ListChanged += acctList_ListChanged;
itemChangedEventReceived = false;
acct.Balance = 1;
if (itemChangedEventReceived)
{
Console.WriteLine("ListChanged/ItemChanged event received");
}
else
{
Console.WriteLine
("ListChanged/ItemChanged event NOT received");
}
MyBindingList<Account> deserAcctList;
deserAcctList = SerializeUtility.SerializeAndDeserialize(acctList);
Account deserAcct = deserAcctList[0];
deserAcctList.ListChanged += acctList_ListChanged;
itemChangedEventReceived = false;
deserAcct.Balance = 2;
if (itemChangedEventReceived)
{
Console.WriteLine("ListChanged/ItemChanged event received");
}
else
{
Console.WriteLine
("ListChanged/ItemChanged event NOT received");
}
}
}
}
The output of this program is:
ListChanged/ItemChanged event received
ListChanged/ItemChanged event NOT received
Initially, the list fires ItemChanged
events. After a serialize/deserialize step, it doesn't. Hence, we have a bug we must fix.
Fixing the problem
As I wrote in Fixing BindingList Deserialization, there's a straightforward fix to this problem. You simply need to get the BindingList<T>
to rewire the events after deserialization. It's disappointing the SDK doesn't do this for you.
What I've done is I've extended BindingList<T>
and added a method annotated with the OnDeserialized
attribute like this:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;
namespace FixBindingList
{
[Serializable]
public class MyBindingList<T> : BindingList<T>
{
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
List<T> items = new List<T>(Items);
int index = 0;
foreach (T item in items)
{
base.SetItem(index++, item);
}
}
}
}
The StreamingContext
parameter is a requirement of the OnDeserialized
attribute, but everything else is straightforward. After .NET has finished deserializing the list, we iterate over each of the items and invoke SetItem()
. These items, of course, are already on the list at this point. However, it's the implementation of BindingList<t>.SetItem()</t>
that handles the wiring of event listeners to the PropertyChanged
event of any items implementing INotifyPropertyChanged
. Because we're essentially "replacing" list items with references to the same item, there are no other side effects to the list.
One final note is to point out that I explicitly invoke the supertype implementation of SetItem()
using the base
keyword. SetItem()
is a virtual method, and it's possible a more elaborate extension of BindingList<T>
may choose to override it. That's okay, but it might unintentionally introduce unwanted behavior on deserialization. Thus, we avoid that problem by forcing the call to be handled directly by the base class.
If you make this modification and run the test program, your output will be:
ListChanged/ItemChanged event received
ListChanged/ItemChanged event received
Conclusion
The bug is fixed. Note, however, that all of this is only necessary if you intend to extend BindingList<T>
. When BindingList<T>
is used as your concrete type, the bug doesn't appear.
However, extending BindingList<T>
is often smart. BindingList<T>
has a number of shortcomings including lack of sorting and filtering support, plus an ItemRemoved
implementation that fails to identify the removed item. I recommend creating a subclass on your projects to use in lieu of direct instantiation of BindingList<T>
. This will allow you to close the gap on BindingList<T>
's "last mile" problems with a cross-project reusable utility.