Demonstrating BindingList Serialization Bug

Save/Share Google Yahoo! Digg It Reddit del.icio.us
My Zimbio

Okay, so I blogged about how C# BindingList<T> serialization is broken and how to fix it. The problem centers around filling a BindingList subtype with items that implement INotifyPropertyChanged. This enables your list to fire ListChanged events indicating ItemChanged whenever a property of a list item changes. Unfortunately, serializing and deserializing the list causes the ItemChanged notification to stop firing unless you take steps to properly rebuild the list. This happens because C# event handlers aren’t serializable, and .NET fails to rewire the listeners.

A colleague of mine asked for a quick example to demonstrate the problem, so I whipped up this test case.

First, let’s do some groundwork. Here’s a 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())

{

// serialize the specified object to a memory stream

BinaryFormatter formatter = new BinaryFormatter();

formatter.Serialize(outputStream, obj);

// reconstruct an object instance from the serialized data

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 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, like Michael Weinhardt’s great solution for generic BindingList sorting, 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

{

// a flag we’ll set to indicate an event fired

static bool itemChangedEventReceived;

// event handler that looks for ItemChanged

static void acctList_ListChanged(object sender, ListChangedEventArgs e)

{

if (e.ListChangedType == ListChangedType.ItemChanged)

{

itemChangedEventReceived = true;

}

}

static void Main(string[] args)

{

// create a list item and a MyBindingList<T>

Account acct = new Account();

MyBindingList<Account> acctList = new MyBindingList<Account>();

// add the Account to the BindingList

// this will cause the BindingList

// to start listening to PropertyChanged events

acctList.Add(acct);

// hook up an event listener to the BindingList

acctList.ListChanged += acctList_ListChanged;

// make a change to the Account and see if the list notifies of the change

itemChangedEventReceived = false;

acct.Balance = 1;

if (itemChangedEventReceived)

{

Console.WriteLine(“ListChanged/ItemChanged event received”);

}

else

{

Console.WriteLine(“ListChanged/ItemChanged event NOT received”);

}

// so far, so good – the BindingList fires events like we expect

// serialize and deserialize the BindingList

MyBindingList<Account> deserAcctList;

deserAcctList = SerializeUtility.SerializeAndDeserialize(acctList);

// lookup the deserialized Account in the deserialized BindingList

Account deserAcct = deserAcctList[0];

// hook up an event listener to the deserialized BindingList

deserAcctList.ListChanged += acctList_ListChanged;

// make a change to the deserialized Account

itemChangedEventReceived = false;

deserAcct.Balance = 2;

if (itemChangedEventReceived)

{

Console.WriteLine(“ListChanged/ItemChanged event received”);

}

else

{

Console.WriteLine(“ListChanged/ItemChanged event NOT received”);

}

// uh, oh! The BindingList didn’t fire an event!

Console.ReadLine();

}

}

}

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.

As I described in Fixing BindingList Deserialixation, we fix it by adding a custom method to invoke after deserialization:

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;

// call SetItem again on each item to re-establish event hookups

foreach (T item in items)

{

// explicitly call the base version in case SetItem is overridden

base.SetItem(index++, item);

}

}

}

}

If you make this modification and run the test program, your output will be:

ListChanged/ItemChanged event received
ListChanged/ItemChanged event received

The bug is fixed. Note, however, that all of this is only necessary if you intend to extend BindingList. When BindingList is used as your concrete type, the bug doesn’t appear. [update: some readers report the problem still exists with concrete BindingLists - could it be explained by different patch levels on the framework?] But, since extending BindingList is often a smart move anyway, I recommend creating a custom class on your projects with this deserialization fix to use in lieu of direct instantiation of BindingList.

Save/Share Google Yahoo! Add to Technorati Favorites Digg It Reddit
del.icio.us My Zimbio

3 Responses to “Demonstrating BindingList Serialization Bug”

  1. Charlie Says:

    Thanks, this really helped me.

  2. Anthony Says:

    Very Clever !!
    And beautifully explaned I might add. Also I never realized there was a OnDeserialized Attribute V Useful :)

  3. marlon Says:

    I got a exception when serialize a object implements INotifyPropertyChanged like Account class. This’s really helped me.

    I bind Account.Balance to TextBox.Text.

    this.textBox.DataBindings.Clear();
    Binding binding = new Binding(“Text”, account, “Balance”);
    this.textBox.DataBindings.Add(binding);

    I do it again after deserialize account but textBox.Text will not changed when set account.Balance because of “PropertyChanged” is null. What can I do to fix this problem?

Leave a Reply