Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#
Article

Fixing BindingList<T> Deserialization

Rate me:
Please Sign up or sign in to vote.
4.88/5 (11 votes)
22 Jul 20075 min read 64.4K   30   10
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.

C#
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:

C#
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><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:

C#
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:

C#
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:

C#
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><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


Written By
United States United States
Mike Gavaghan opines on C# and .Net in his blog Talk Nerdy To Me[^]. He is a Microsoft Certified Professional Developer working as a C#/.Net software consultant based in Dallas, Texas.

Since 1992, he has supported clients in diverse businesses including financial services, travel, airlines, and telecom. He has consulted at both mammoth enterprises and small startups (and sees merits and problems in both).

You may also view his profile on LinkedIn[^].

Comments and Discussions

 
GeneralThanks a lot Pin
Hardy Wang3-Feb-09 14:25
Hardy Wang3-Feb-09 14:25 
GeneralAnother Note Pin
Mike DiRenzo18-Oct-07 7:22
Mike DiRenzo18-Oct-07 7:22 
GeneralRe: Another Note Pin
Mike Gavaghan20-Oct-07 11:43
Mike Gavaghan20-Oct-07 11:43 
GeneralThanks for the article Pin
Mike DiRenzo18-Oct-07 6:59
Mike DiRenzo18-Oct-07 6:59 
QuestionIt doesn't call OnDeserialized for me Pin
RichardHowells11-Sep-07 13:16
RichardHowells11-Sep-07 13:16 
I thought it was manna from heaven when I discovered this article. Sadly it's not working for my case.

When I use this technique with WCF (using WCF as a replacement for remoting), I'm finding that the OnDeserialised method never gets called. If I use the same types with the BinaryFormatter then the method does get called.

I am a novice here and I am not sure what to do next. Can I force it to use the BinaryFormatter to serialize/deserialize the objects? Would that help? How do I do it?

Thanks.

- Richard
AnswerRe: It doesn't call OnDeserialized for me Pin
Mike Gavaghan11-Sep-07 14:17
Mike Gavaghan11-Sep-07 14:17 
GeneralRe: It doesn't call OnDeserialized for me Pin
RichardHowells12-Sep-07 0:11
RichardHowells12-Sep-07 0:11 
GeneralThanks Pin
Josh Smith23-Jul-07 8:36
Josh Smith23-Jul-07 8:36 
GeneralNice Article Pin
TJoe22-Jul-07 9:47
TJoe22-Jul-07 9:47 
GeneralRe: Nice Article Pin
Mike Gavaghan22-Jul-07 10:03
Mike Gavaghan22-Jul-07 10:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.