Tuesday, February 12, 2008

How to disconnect UI and Data in WPF (CachedObservableCollection) and some updates regarding ThreadSafeObservableCollection

Sometimes, we want to "freeze" current displayed collection, while the original collection keep being updates. How to do it? There are some ways to get such functionality. One is to use CollectionView DeferRefresh method and release (dispose) it after you finished. However, it can not provide us with full functionality, due to fact, that the lock applies on original collection, thus we wont update it until the lock will be released.

image 

Another approach is to keep disconnected underlying collection, that synchronized with the original collection while it reaches "cache limit". After reaching it, we'll disconnect the original collection, keeping to be updated and reconnect (or, even refill) the underlying collection when possible.

image

Let's see the example of such functionality. We have the underlying collection, that rapidly changing in background (another thread)

class MyData : ThreadSafeObservableCollection<string>
   {
       public MyData()
       {
           ThreadPool.QueueUserWorkItem(delegate
           {
               int i = 0;
               while (true)
               {
                   base.Add(string.Format("Test {0}", i++));
                   Thread.Sleep(100);
                   if (base.Count > 10)
                       base.RemoveAt(0);
               }
           });

Also, we have another collection, that initialized by original collection and number of items should be cached.

dta = Resources["data"] as MyData;
cache = new CachedObservableCollection<string>(dta,5);

Then, we just create CollectionView to be able to sort/filter displayed data and bind it into ListBox

CollectionView view = new CollectionView(cache);
Binding b = new Binding();
b.Source = view;
lst.SetBinding(ListBox.ItemsSourceProperty, b);

So, what CachedObservableCollection<T> actually doing? First of all, it holds the reference to our original data source. It also, subscribes to CollectionChanged event of the original source in order to check whether it reach the cache limit.

public CachedObservableCollection(ObservableCollection<T> collection, int maxItems):base()
        {
            m_cached = collection;
            m_cached.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(m_cached_CollectionChanged);
            MaxItems = maxItems;
        }

It it is, we just disconnect the source and clean up unnecessary items.

void m_cached_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                if (base.Count < MaxItems)
                {
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        base.Add((T)e.NewItems[i]);
                    }
                }
                else
                {
                    for (int i = base.Count - 1; i >= MaxItems; i--)
                    {
                        base.RemoveItem(i);
                    }
                }
            }
        }

We should treat RemoveItem in order to be sure, that items, removed from cache will be removed from underlying data source as well (if they still there)

protected override void RemoveItem(int index)
        {
            T obj = base[index];
            if(m_cached.Contains(obj))
                m_cached.Remove(obj);
            base.RemoveItem(index);
        }

Actually, we finished. The only thing we should do it to interface MaxItem property and assure it to rise OnPropertyChanged event. ObservableCollection is not DependencyObject, thus we cannot use DependencyProperty there. INotifyPropertyChanged is still implemented.

int m_maxItems;
        public int MaxItems
        {
            get { return m_maxItems; }
            set { m_maxItems = value;
            base.OnPropertyChanged(new PropertyChangedEventArgs("MaxItems"));
            }
        }

Now, we can bind this property to Slider and manage maximum number of cached items by using binding.

Binding b1 = new Binding();
b1.Source = cache;
b1.Path = new PropertyPath("MaxItems");
sld.SetBinding(Slider.ValueProperty, b1);

Well done. However, I have another problem here. I need to enumerate ThreadSafeObservableColelction. Let's see the code of MyData

ThreadPool.QueueUserWorkItem(delegate
            {
                while (true)
                {
                    foreach (string str in this)
                    {
                        Log.Add(string.Format("Inspecting {0}", str));
                        if (Log.Count > 10)
                            Log.RemoveAt(0);
                        Thread.Sleep(100);                       
                    }

                }
            });

If I'll run it this way, I'll get well known exception: Collection was modified; enumeration operation may not execute. This happens, because of rapidly changes in the collection, while enumerating it. We can use old well-known lock(SyncObj) method, however, we have no SyncObject in ObservableCollection<T>, thus we should provide thread safe read only disconnected array, that relays on original data in certain time period. We can continue write the collection, but we should be able to read, thus we'll use ReaderLock to fix current collection state and create disconnected copy of items in this moment.

public T[] ToSyncArray()
        {
            _lock.AcquireReaderLock(-1);
            T[] _sync = new T[this.Count];
            this.CopyTo(_sync, 0);
            _lock.ReleaseReaderLock();
            return _sync;
        }

Now, we finally done. We can read and write the collection, also we can have disconnected copy of it in order to provide consistent UI, while changing the actual data.

Source code for this article.

No comments: