Adding to a Source Collection from a Collection Changed Event Handler

Suppose that you are building an application where you are monitoring changes to a source collection. You define a filter result set using CLINQ. In response to a change in the filter set, you eventually want to add a new row to the source collection. The problem is that you cannot modify the contents of a collection during the CollectionChanged event handler for very good, obvious reasons.

Here's a hypothetical and, very real, scenario. You have a source collection that represents the contents of a shopping cart. You create a CLINQ query that contains the contents of the shopping cart where the SKU matches certain specific "magic SKUs".
A Magic SKU is often a signal indicator that something else needs to be done to a shopping cart.
This is often how electronic coupons, discounts, gift certificates, and other credits are redeemed.

Here is some code to build such a cart and such a monitoring query:
_sourceCart = new ContinuousCollection<OrderItem>();

_sourceCart.CollectionChanged += 
    new NotifyCollectionChangedEventHandler(_sourceCart_CollectionChanged);

_sourceCart.Add(
    new OrderItem()
    {
        Quantity = 1,
        SKU = "101392031",
        Price = 19.99,
        Description = "Cool DVD"
    });

var modCart = from orderItem in _sourceCart
              where orderItem.SKU == "999999999"
              select orderItem;

modCart.CollectionChanged += 
    new NotifyCollectionChangedEventHandler(modCart_CollectionChanged);

_sourceCart.Add(
    new OrderItem()
    {
        Quantity = 1,
        SKU = "999999999",
        Price = 19.99,
        Description = "Awesome DVD" 
    });


In this code, you see a two item cart and then a monitor query designed to contain just the items with the SKU of all 9s. In a real-world scenario, the monitor query might be far more complex, looking for the presence of 2 DVDs in a given price range or 1 DVD plus 1 DVD player etc. Such monitor queries are also used to scan for potential up-sell and cross-sell opportunities for in-cart advertising and suggesting.

Here is the code that listens to changes in the monitor query and responds by adding a discount to the cart: (note, this code WILL throw an exception!)
  if (e.Action == NotifyCollectionChangedAction.Add)
            {               
              // customer added the magic DVD to the cart
              // give them a $20 discount!
             OrderItem discount = new OrderItem()
                    {
                        Quantity = 1,
                        SKU = "-1",
                        Price = -20.00,
                        Description = "Buy the cool and awesome DVDs and get one free!"
                    };

              _sourceCart.Add(discount);

            }


To get around the limitation of running into the re-entry barrier that is part of ObservableCollection, all you really need to do is use a background thread to perform the addition. Notification of the background-added item will actually be posted to the right Dispatcher, so the GUI will still know about it when bound:
if (e.Action == NotifyCollectionChangedAction.Add)
{               
        // customer added the magic DVD to the cart
        // give them a $20 discount!
    Thread t = new Thread(
        new ThreadStart(delegate()
    {
        OrderItem discount = new OrderItem()
        {
            Quantity = 1,
            SKU = "-1",
            Price = -20.00,
            Description = "Buy the cool and awesome DVDs and get one free!"
        };
        
        _sourceCart.Add(discount);

    }));
    t.Start();
}

Last edited Feb 14, 2008 at 3:15 PM by brettkotch, version 4

Comments

IDisposable Oct 10, 2008 at 4:02 AM 
Spinning up a thread for this seems awful expensive, especially given that the thread will immediately block waiting for the list lock to be released. Could you perhaps create a FIFO queue of deferred-execution of Action<T> delegates against the node/list that gets fired when the code event handling is complete and it is known-safe for the modification to occur.