.
In Depth Banner
Skip Navigation LinksWelcome > In Depth articles > Sparse array databinding > The sparse collection

A sparsely populated collection suitable for databinding.

Obviously, when we speak of collection systems that can be bound to user interface controls we imply that the collection system must obey certain .net rules.  There are many types of collection in .net and the most basic of them provides an enumeration interface.  Then there are various types of list collection and finally the all singing all dancing binding list.  I thought that just for fun it would be a good idea to create a collection system that provided all of these interfaces just to be on the safe side.

2D Sparsely populated arrays declare themselves as rectangular even if they are not full of items.  So, I thought it would be a good idea to create an array that had the ability to declare the number of rows and columns as well as some system that enabled me to declare when rows or columns have been added as well as the keys that allow me to select a particular row and column combination.

SparseCollection code:

The SparseItem<T> object

Because items in the collection are associated with a row and column address I created a placeholder that can keep the position within the collection and the object being stored. The following class is the placeholder wrapper class for the items stored in the collection.

    /// <summary>

    /// This generic class is a placeholder for the items ultimately stored in the collection

    /// </summary>

    /// <typeparam name="T">The type for which the collection is specialised</typeparam>

    public class SparseItem<T> : INotifyPropertyChanged

    {

 

        public bool IsDefault { get; set; }

 

        public SparseItem()

        {

            IsDefault = true;

        }

 

        public SparseItem(object rowKey, object columnKey, T value)

        {

            _rowKey = rowKey;

            _columnKey = columnKey;

            _value = value;

        }

 

        public SparseItem(SparseItemKeyTuple tuple, T value)

        {

            _rowKey = tuple.RowKey;

            _columnKey = tuple.ColumnKey;

            _value = value;

        }

 

        private object _columnKey;

 

        public object ColumnKey

        {

            get { return _columnKey; }

            set

            {

                if (_columnKey != value)

                {

                    _columnKey = value;

                    OnNotify("ColumnKey");

                }

            }

        }

 

        private object _rowKey;

 

        public object RowKey

        {

            get { return _rowKey; }

            set

            {

                if (_rowKey != value)

                {

                    _rowKey = value;

                    OnNotify("RowKey");

                }

            }

        }

 

        private T _value;

 

        public T Value

        {

            get { return _value; }

            set

            {

                if (typeof(T).IsValueType)

                {

                    _value = value;

                }

                else

                {

                    if ((_value != null) && (null != _value as INotifyPropertyChanged))

                    {

                        ((INotifyPropertyChanged)_value).PropertyChanged -= ValueChanged;

                    }

                    _value = value;

                    if (null != value as INotifyPropertyChanged)

                        ((INotifyPropertyChanged)value).PropertyChanged += ValueChanged;

                    IsDefault = false;

                }

                OnNotify("Value");

           }

        }

 

        private void ValueChanged(object sender, PropertyChangedEventArgs e)

        {

            OnNotify("Value");

        }

 

        #region INotifyPropertyChanged Members

 

        protected void OnNotify(string s)

        {

            if (PropertyChanged != null)

                PropertyChanged(this, new PropertyChangedEventArgs(s));

        }

 

        public event PropertyChangedEventHandler PropertyChanged;

 

        #endregion

    }

 

This object performs a very important task of detecting whether the object being stored implements INotifyPropertyChanged and if so, subscribes to the change notification event for the purpose of forwarding any change notification to the collection.

You will notice that during creation the item requires a SparseItemKeyTuple object that carries the row and column associations for the item. This class is shown below.

    /// <summary>

    /// This class is used to associate an item with a position in the sparse array

    /// </summary>

    public struct SparseItemKeyTuple

    {

 

        public SparseItemKeyTuple(object columnKey, object rowKey)

        {

            RowKey = rowKey;

            ColumnKey = columnKey;

        }

 

        public object RowKey;

        public object ColumnKey;

    }

 

The collection object

Before launching into the actual collection note that I have created an interface that any collection type could implement to enable sparse array behaviour. This interface is shown here:

    public interface ISparseCollection

    {

        event EventHandler RowAdded;

        event EventHandler ColumnAdded;

 

        List<object> RowKeys { get; }

        List<object> ColumnKeys { get; }

 

        object this[SparseItemKeyTuple tuple]{get; set;}

    }

 

The collection object, SparseCollection<T> is a fairly lengthy class. Not because of very complex functionality but because it implements the full gamut of the IBindingList interface. This is one of those interfaces that, in my opinion, incorrectly derives from several other interfaces and so is an utter pain to implement as one is forced to code for all its base interfaces too. Anyway, design and architecture gripes aside, here’s the code:

    public class SparseCollection<T> : IBindingList, ISparseCollection

    {

        //Some friendly exception strings

        const string SparseItemArgument = "SparseCollection can only contain SparseItem objects";

        const string SparseArrayError = "Array must be of type SparseItem<T>";

 

        //Events to enable notification of row or column additions

        public event EventHandler RowAdded;

        public event EventHandler ColumnAdded;

 

        //Housekeeping

        int _internalRowCount, _internalColumnCount;

 

        //An internal list used to maintain the objects in a non-sparse fashion

        private BindingList<SparseItem<T>> _items = new BindingList<SparseItem<T>>();

 

        /// <summary>

        /// Called before an item is added so that the row or column increase detection will work

        /// </summary>

        void BeginAdd()

        {

            _internalRowCount = RowKeys.Count();

            _internalColumnCount = ColumnKeys.Count();

        }

 

        /// <summary>

        /// Called at the end of adding an item to determine

        /// whether we need to fire a row/column add event

        /// </summary>

        void EndAdd()

        {

            if (_internalRowCount != RowKeys.Count())

                OnRowAdded();

            if (_internalColumnCount != ColumnKeys.Count())

                OnColumnAdded();

        }

 

        /// <summary>

        /// Notifies any listeners of new columns

        /// </summary>

        private void OnColumnAdded()

        {

            if (ColumnAdded != null)

                ColumnAdded(this, EventArgs.Empty);

        }

 

        /// <summary>

        /// Notifies any listeners of new rows

        /// </summary>

        private void OnRowAdded()

        {

            if (RowAdded != null)

                RowAdded(this, EventArgs.Empty);

        }

 

        /// <summary>

        /// Property containing the dynamically maintained list of rowkeys.

        /// </summary>

        /// <remarks>

        /// You should NEVER store this list

        /// </remarks>

        public List<object> RowKeys

        {

            get

            {

                List<object> keys = new List<object>();

                var q = from i in _items

                        orderby i.RowKey

                        select i.RowKey;

                foreach (object o in q.Distinct())

                    keys.Add(o);

                return keys;               

            }

        }

 

 

        /// <summary>

        /// Property containing the dynamically maintained list of columnkeys.

        /// </summary>

        /// <remarks>

        /// You should NEVER store this list

        /// </remarks>

        public List<object> ColumnKeys

        {

            get

            {

                List<object> keys = new List<object>();

                var q = from i in _items

                        orderby i.ColumnKey

                        select i.ColumnKey;

                foreach (object o in q.Distinct())

                    keys.Add(o);

                return keys;               

            }

        }

 

        /// <summary>

        /// A nested class representing a row of objects of type T

        /// </summary>

        /// <typeparam name="T"></typeparam>

        public class SparseRow<T>

        {

 

            SparseCollection<T> _parent;

            object _rowKey;

 

            public SparseRow(SparseCollection<T> parent, object rowKey)

            {

                _parent = parent;

                _rowKey = rowKey;

            }

 

            IEnumerable<SparseItem<T>> RowEntries

            {

                get

                {

                    var t = from n in _parent._items

                            where n.RowKey == _rowKey

                            select n;

 

                    return t.AsEnumerable();

                }

            }

        }

 

        //The default constructor for the collection

        public SparseCollection()

        {

            _items.ListChanged += new ListChangedEventHandler(_items_ListChanged);

        }

 

        //A handler that forwards list item changed events

        void _items_ListChanged(object sender, ListChangedEventArgs e)

        {

            RaiseListChanged(e);

        }

 

        /// <summary>

        /// Adding a row to the collection

        /// </summary>

        /// <param name="rowKey">The key representing the row</param>

        /// <param name="columnKey">The key representing the column</param>

        /// <param name="value">The item to add</param>

        public void Add(object rowKey, object columnKey, T value)

        {

 

            BeginAdd();

 

            SparseItem<T> ri = (from i in _items

                                where i.ColumnKey == columnKey && i.RowKey == rowKey

                                orderby i.ColumnKey

                                select i).FirstOrDefault();

 

            if (ri == null)

            {

                ri = new SparseItem<T>() { RowKey = rowKey, ColumnKey = columnKey, Value = value };

                _items.Add(ri);

            }

 

            EndAdd();

        }

 

        /// <summary>

        /// Adding a single item

        /// </summary>

        /// <param name="item"></param>

        public void Add(SparseItem<T> item)

        {

            BeginAdd();

            _items.Add(item);

            EndAdd();

        }

 

        /// <summary>

        /// Get an array of objects in a row

        /// </summary>

        /// <param name="rowkey"></param>

        /// <returns></returns>

        public SparseItem<T>[] Row(object rowkey)

        {

            return (from k in _items

                    where k.RowKey.Equals(rowkey)

                    orderby k.ColumnKey

                    select k).ToArray();

        }

 

        #region IBindingList Members

        //NOTE that non implemented members of the IBindingList interface will throw exceptions.

        //You could of course change these if you so desire

 

        public void AddIndex(PropertyDescriptor property)

        {

            throw new NotImplementedException();

        }

 

        public object AddNew()

        {

            throw new NotImplementedException();

        }

 

        public bool AllowEdit

        {

            get { return false; }

        }

 

        public bool AllowNew

        {

            get { return false; }

        }

 

        public bool AllowRemove

        {

            get { return false; }

        }

 

        public void ApplySort(PropertyDescriptor property, ListSortDirection direction)

        {

            throw new NotImplementedException();

        }

 

        public int Find(PropertyDescriptor property, object key)

        {

            throw new NotImplementedException();

        }

 

        public bool IsSorted

        {

            get { return false; }

        }

 

        public event ListChangedEventHandler ListChanged;

 

        protected void RaiseListChanged(ListChangedEventArgs e)

        {

            if (ListChanged != null)

                ListChanged(this, e);

        }

 

        public void RemoveIndex(PropertyDescriptor property)

        {

            throw new NotImplementedException();

        }

 

        public void RemoveSort()

        {

            throw new NotImplementedException();

        }

 

        public ListSortDirection SortDirection

        {

            get { throw new NotImplementedException(); }

        }

 

        public PropertyDescriptor SortProperty

        {

            get { throw new NotImplementedException(); }

        }

 

        public bool SupportsChangeNotification

        {

            get { return true; }

        }

 

        public bool SupportsSearching

        {

            get { return false; }

        }

 

        public bool SupportsSorting

        {

            get { return false; }

        }

 

        #endregion

 

        #region IList Members

 

        public int Add(object value)

        {

            BeginAdd();

            SparseItem<T> ri = value as SparseItem<T>;

            if (ri != null)

            {

                _items.Add(ri);

                EndAdd();

                return _items.IndexOf(ri);

            }

            else

                throw new ArgumentException(SparseItemArgument);

        }

 

        public void Clear()

        {

            _items.Clear();

        }

 

        public bool Contains(object value)

        {

            return _items.Contains(value as SparseItem<T>);

        }

 

        public int IndexOf(object value)

        {

            return _items.IndexOf(value as SparseItem<T>);

        }

 

        public void Insert(int index, object value)

        {

            BeginAdd();

            SparseItem<T> ri = value as SparseItem<T>;

            if (ri != null)

            {

                _items.Insert(index, ri);

                EndAdd();

            }

            else

                throw new ArgumentException(SparseItemArgument);

        }

 

        public bool IsFixedSize

        {

            get { return false; }

        }

 

        public bool IsReadOnly

        {

            get { return false; }

        }

 

        public void Remove(object value)

        {

            _items.Remove(value as SparseItem<T>);

        }

 

        public void RemoveAt(int index)

        {

            _items.RemoveAt(index);

        }

 

        /// <summary>

        /// Gets or sets an item in the array given a specific key-tuple address

        /// </summary>

        /// <param name="tuple"></param>

        /// <returns></returns>

        public object this[SparseItemKeyTuple tuple]

        {

            get

            {

                var q=from i in _items

                        where i.RowKey.Equals(tuple.RowKey) && i.ColumnKey.Equals(tuple.ColumnKey)

                        select i;

                if (q.Count() > 0)

                    return q.FirstOrDefault();

                return null;

            }

            set

            {

                var q = from i in _items

                        where i.RowKey.Equals(tuple.RowKey) && i.ColumnKey.Equals(tuple.ColumnKey)

                        select i;

                if (q.Count() > 0)

                    q.FirstOrDefault().Value = (T)value;

                else

                    Add(new SparseItem<T>(tuple, (T)value));

            }

        }

 

        public object this[int index]

        {

            get

            {

                return _items[index];

            }

            set

            {

                _items[index] = value as SparseItem<T>;

            }

        }

 

        #endregion

 

        #region ICollection Members

        //NOTE that as before, not all members of this interface are fully implemented.

 

        public void CopyTo(Array array, int index)

        {

            SparseItem<T>[] ria = array as SparseItem<T>[];

            if (ria != null)

                _items.CopyTo(ria, index);

            else

                throw new ArgumentException(SparseArrayError);

        }

 

        public int Count

        {

            get { return _items.Count; }

        }

 

        public bool IsSynchronized

        {

            get { return false; }

        }

 

        public object SyncRoot

        {

            get { throw new NotImplementedException(); }

        }

 

        #endregion

 

        #region IEnumerable Members

 

        public IEnumerator GetEnumerator()

        {

            return _items.GetEnumerator();

        }

 

        #endregion

    }

 

Summary

The sparse array collection shown in this article is the first of two main sections of a complete databinding story. Here you’ve seen how to implement a novel and binding-compliant collection system that will enable you to create economic sparsely populated arrays of any reference or value type object.

In the next article you will see how to create a simple WPF control that binds to this object to create a grid that is capable of displaying data from this collection.

Copyright © Bob Powell 2000-.  All rights reserved.