Introduction
Application objects frequently have a many-to-many relationship to one another.
This occurs, for example, in representing orders for products.
Each order requests, and thereby relates to, possibly several products.
For inventory control purposes, it is frequently desirable to have products
relate to the orders requesting them.
Each product is requested by, and thereby relates to, possibly several orders.
Many-to-many relationships are frequently represented in databases as a linking
table.
In such a table, each table element references an instance of each of the objects
in the relationship.
The table representing the relationship between orders and products would contain
an element for each order line item.
The element would refer to the order and to the item ordered.
In .NET, each side of the relationship can be represented as a set of collections.
Each order, for example, could have a collection of products ordered.
Similarly, each product could have a collection of orders which request it.
However, updating one of these collections—adding a product to an order, for
example—then requires updating another of these collections—the
collection of orders requesting the product.
This requires each object participating in the relationship to be aware it
participates in a relationship.
Each object must maintain other objects’ views of the relationship as well as
its own.
This is cumbersome and error prone.
This article describes a generic class that avoids this complication.
The companion ZIP file contains the source for the class, and the source for an
NUnit test for the class.
The class, Association <TLeft, TRight>
maintains a list
of object pairs.
The class can provide a strongly typed ICollection<T>
view for
each object potentially in the relationship.
Each ICollection<T>
view dynamically changes the underlying
relationship as the collection is manipulated.
Each ICollection <T>
view also dynamically changes content to
reflect changes to the relationship made by other objects.
For ease of data binding, each ICollection <T>
view also
implements the INotifyCollectionChanged
interface.
This permits event subscribers to receive an event when the content changes.
Using the code
Creating the relationship
A relationship is created with a declaration such as:
Association <TLeft, TRight> association = new Association <TLeft, TRight> ();
This creates an Association <TLeft, TRight>
object which
implements a relationship between objects of type TLeft
and objects
of type TRight
.
Relationships between types are typically singletons.
For example, there would be a single instance of a relationship between orders and
products.
Individual order objects and product objects would each have their own view of the
relationship.
The Association <TLeft, TRight>
class does not enforce the
singleton pattern.
This allows types to have multiple relationships with one another.
For example, orders and products might have an “order contains”
relationship and an “items backordered” relationship.
Creating the views
Objects of type TLeft
can obtain their view of the relationship
with a declaration such as:
IAssociationCollection <TRight> _myTRights = association.LeftView
(this);
This creates an object implementing the ICollection <TRight>
interface and the INotifyCollectionChanged
interface.
(The object _TRights
could also be declared with type
ICollection <TRight>
.
This would, however, make it difficult to subscribe to the CollectionChanged
event.)
The ICollection<TRight>
at any point contains references
to each TRight
in the relationship with the specific
TLeft
.
Similarly, an object of type TRight
can obtain its view of the
relationship with the declaration:
IAssociationCollection <TLeft> _myTLefts = association.RightView (this);
Using the views
The System.Collections.Generic.ICollection <T>
interface
defines the members:
void Add (T item);
void Clear ();
bool Contains (T item);
void CopyTo (T [] array, int startIndex);
int Count { get; }
bool IsReadOnly { get; }
bool Remove (T item);
The object returned by LeftView
and RightView
implement
these by manipulating the underlying
Association <TLeft,
TRight>
object.
The Add
method adds a single item to the collection on which it
is invoked.
It ensures a (TLeft
, TRight
) pair corresponding
to the item for which the IAssociationCollection <T>
was
created and to the item referenced by the Add
method is in the
underlying Association <TLeft, TRight>
object.
If the pair was already in the underlying
Association <TLeft,
TRight>
object, the pair is not duplicated and no events are
published.
If the pair was not already in the underlying
Association <TLeft,
TRight>
object, the pair is added and
CollectionChanged
events are published by all
IAssociationCollection <T>
objects
associated with either of the items in the added pair.
Similarly, the Remove
method removes a single item from the
collection on which it is invoked.
It ensures a (TLeft
, TRight
) pair corresponding
to the item for which the IAssociationCollection <T>
object was
created and to the item referenced by the Add
method is not in the
underlying Association <TLeft, TRight>
object.
If the pair was already in the underlying
Association <TLeft,
TRight>
object, the pair is removed and
CollectionChanged
events are published by all
IAssociationCollection <T>
objects
associated with either of the items in the removed pair.
If the pair was not already in the underlying
Association <TLeft,
TRight>
object, nothing is removed and no events are published.
Finally the Clear
method removes all items from the collection on
which it is invoked.
It ensures no (TLeft
, TRight
) pair corresponding
to the item for which the IAssociationCollection <T>
object was
created is in the underlying Association <TLeft, TRight>
object.
If any pairs were removed from the
Association <Left Item,
TRight>
object, a single
CollectionChanged
event is
published (listing all formerly associated objects) for the object for which the
IAssociationCollection <T>
was created, and a separate
CollectionChanged
event is published (listing the single object for
which the
IAssociationCollection <T>
was created) for each
object formerly associated with the object for which the
IAssociationCollection <T>
was created.
In summary, adding an item to an IAssociationCollection <T>
object causes it to appear in other IAssociationCollection <T>
objects as required.
Removing an item from an IAssociationCollection <T>
object
causes it to disappear from other IAssociationCollection <T>
objects as required.
Finally, IAssociationCollection <T>
publish
CollectionChanged
events as required to describe the changes they
undergo.
The CollectionChanged
event handler has the signature:
void handler (object publisher,
System.Collections.Specialized.NotifyCollectionChangedEventArgs eventArgs);
The NotifyCollectionChangedEventArgs
type defines the members:
NotifyCollectionChangedAction Action { get; }
IList NewItems { get; }
int NewStartingIndex { get; }
IList OldItems { get; }
int OldStartingIndex { get; }
The CollectionChanged
events published by the
IAssociationCollection
<T>
objects do not use the
NewStartingIndex
and
OldStartingIndex
members, as collections are unordered.
The only
NotifyCollectionChangedAction
values used for the
Action
member are
Add
and
Remove
.
The
NewItems
list is non-
null
only if
the
Action
value is
Add
.
The
OldItems
list is non-
null
only if
the
Action
value is
Remove
.
Threading concerns
The IAssociationCollection <T>
objects themselves are
thread-safe.
However, the enumerators obtained through the GetEnumerator()
method are not thread-safe.
All events are published on the thread invoking the Add
, Clear
, or
Remove
method causing the event to be
published.
Serialization concerns
The Association <TLeft, TRight>
type is serializable.
If the types TLeft
and TRight
are serializable,
then an Association <Left Item, TRight>
object is
serializable.
The IAssociationCollection <T>
objects are not
serializable.
If you intend your object using IAssociationCollection <T>
fields to be serializable, it must use the [NonSerialized]
attribute
on those fields, or implement custom serialization through the
System.Runtime.Serialization.ISerializable
interface.
The collections implemented by IAssociationCollection <T>
are
not independent entities.
They are instead a view of data held elsewhere.
Each IAssociationCollection <T>
object references its parent
Association <TLeft, TRight>
object, which in turn
references all TLeft
and TRight
objects currently
participating in the relationship.
Were IAssociationCollection <T>
objects serializable, attempts
to serialize a single TLeft
or TRight
object would
instead serialize possibly all TLeft
and TRight
objects in the program.
Typically, a Association <TLeft, TRight>
object would be
serialized only along with all TLeft
and TRight
objects in the program.
Points of interest
Why a central repository?
As mentioned in the Introduction, relationships are typically modeled in .NET with
collections.
These collections are required to update one another as each changes.
This is certainly feasible, and relatively easily implemented.
Why, then, have a central repository of relationship information at all?
There are two factors that argue for a central repository of relationship
information.
The first is consistency: a single definitive source cannot get out of
synchronization with itself.
If relationship information is spread out through a series of collections owned
by individual objects, the information can be lost (through, e.g., garbage
collection of individual objects which are collectively referenced only by one
another) or become contradictory (through, e.g., A claiming it is related to B but
B claiming it is not related to A).
A second factor lies in recording the relationship on external storage.
If the set of objects to which a given object is related is a view of some other
object, then relationship information need not be recorded along with the given
object.
Instead, all relationship information can be recorded at once, and refer to the
individual objects which are already recorded.
It is much simpler to store rows in two database tables, and then create the
linking table, if the relationship is centralized.
Why not IList views?
Collections are unordered, and lists are ordered.
Typically an application presents a set of items associated with a given item
as a list, in some particular order.
Providing an IList <T>
view, rather than an
ICollection
<T>
view, would make this common task easier.
The two-way nature of the views provided by the
Association <TLeft,
TRight>
class make this problematic.
Not only would each
TLeft
instance impose an order on the
TRight
instances in its
IList
, each
TRight
instance would also impose an order on the
TLeft
instances in its
IList
.
Each sorting request would need to apply a permutation to the base list of items
which simultaneously sorted the view being sorted, and avoided changing both
the order of other views and the validity of any enumerators they had created.
While this is quite likely doable, the complexity of the solution would outweigh
the complexity of creating and sorting
IList
objects from
ICollection
objects when sorting was required.
Enumerating virtual collections
One issue of interest to arise in the construction of the
Association
<TLeft, TRight>
class had to do with enumerators.
Each
IAssociationCollection <T>
object providing a view of
the collection associated with an item implements the
ICollection
<T>
interface. This interface in turn inherits from the
IEnumerable <T>
interface. Therefore, the
IAssociationCollection <T>
object must be able to provide an
IEnumerator <T>
object.
This object would need to enumerate the objects appearing in
(
TLeft
,
TRight
) pairs with the object for which
the
IAssociationCollection <T>
object was prepared.
An IEnumerator
for the underlying storage object cannot be used for
the IEnumerator
for a view.
This is because the underlying storage object can be manipulated by ICollection
objects entirely unrelated to the view.
IEnumerator
objects become invalid when the collection to which they
refer is modified.
Attempting to use an IEnumerator
object for the underlying storage
object as the base for the IEnumerator
object for a view would result
in the IEnumerator
object for a view becoming invalid when some other
ICollection
was modified.
This is clearly undesirable (and breaks the IEnumerator
contract).
The approach taken in the Association <TLeft, TRight>
class is to use a List
object as the underlying storage object, and
have views’ IEnumerator
objects maintain their current position in
that List
with an index, rather than an IEnumerator
object.
Also, (TLeft
, TRight
) pairs are never removed from
the List
: if items are to be removed from one another’s
ICollection
views, instead the (TLeft
,
TRight
) pair is marked “deleted.”
The IEnumerator
objects skip all (TLeft
, TRight
)
pairs marked “deleted.”
Pairs marked “deleted” are reused if the items are added to one another’s
ICollection
views.
“Deleted” pairs are not serialized, so serializing and deserializing the
Association
<TLeft, TRight>
object effectively removes
the “deleted” pairs from the
Association <TLeft, TRight>
object.
Publishing CollectionChanged events
The purpose of the Association <TLeft, TRight>
object is
to provide a set of collections that reflect one another’s changes.
So, for example, using Add
on one collection is intended to cause
CollectionChanged
events to be published by both the
collection on which Add
was executed, and the collection associated
with the item added.
However, the collection on which Add
is executed has no knowledge of
other collections.
This makes modifying these other collections, and publishing events on them,
somewhat difficult.
The solution used in the Association <TLeft, TRight>
class is to have all IAssociationCollection<T>
objects
subscribe to a CollectionChanged
event published by the
Association
<TLeft, TRight>
itself.
As the
Association <TLeft, TRight>
object does not itself
implement an
ICollection
interface, this event is not public.
It is available only to the
IAssociationCollection <T>
objects.
When the
Association <TLeft, TRight>
object publishes
a
CollectionChanged
event, each
IAssociationCollection
<T>
object examines the set of (
TLeft
,
TRight
) pairs changed, and determines if the object with which it
is associated is in any of these pairs.
If so, it extracts the relevant object references (that is, the other element
of the pairs of which it is an element) and publishes its own
CollectionChanged
event listing those object references.
History
- 30 August 2012: Original version of the article.
- 31 August 2012: Reorganized article and included design discussions.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.