Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Fixing BindingList<T> Deserialization

0.00/5 (No votes)
22 Jul 2007 1  
BindingList<T> is flawed with regard to serialization. A flaw in the SDK prevents PropertyChanged events from INotifyPropertyChanged instances from getting rewired on deserialization. Here's a solution to fix the problem.

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())
            {
                // 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 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
    {
        // 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!

        }
    }
}

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;

            // 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);
            }
        }
    }
}

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here