Welcome > 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.
|