Generic Custom NHibernate Collections - A Second Swing
I talked about custom collections for WPF and NHibernate back here, but I wanted to mention that I made an alternative solution that has less lines of code and it is apparently easier for other people to understand.
A quick recap: We want to harness the powerful databinding features of WPF. To optimize the two-way binding functionality, our objects need to implement INotifyPropertyChanged. No problem there, but our collections need to implement INotifyCollectionChanged, which is problematic, because our collections are commonly IList(T)s.
Why does NHibernate use an IList? When declaring a transient (new) object, we always write something such as the following:
A "transient" collection is a new or unsaved collection that was created in your code, and the concrete implementation of "IList(T)" is a "List(T)". A persistent (saved) object is not built by your code, it is built by NHibernate. It is still an "IList(T)", but the concrete implementation is a PersistentGenericBag(T).
The PersistentGenericBag class has no default constructor, it requires an ISession as a construction parameter to support the "Lazy-Loading" magic. Since PersistentGenericBag has no default constructor, it wasn't designed for us to use in transient collections. Besides, why would we want to use an NHibernate-implementation-specific type inside of our domain objects? That would couple our domain objects too tightly with NHibernate specific implementation, in my opinion.
What to do? We need to make a new interface (I defined mine to implement INotifyCollectionChanged for my uses, but this could implement anything you need for your purposes):
We need to define a new "Transient" collection type for our interface:
We need to define a new "Persistent" collection type for our interface:
Finally, we need an implementation of IUserCollectionType to tie this all together and use it in the mapping files. Notice how I treat this as a factory class:
How to use this? In your mapping file, something such as:
In the code:
And you should be in business.
I like this code, because the NHibernate-specific stuff is only accessible from the NHibernate-specific factory. The user code never references a PersistentDomainCollection, which makes for a clean cut. Again thanks to Billy McCafferty and Damon Carr, since my solutions are "cannibalizations" of their more original works. Any thoughts?
A quick recap: We want to harness the powerful databinding features of WPF. To optimize the two-way binding functionality, our objects need to implement INotifyPropertyChanged. No problem there, but our collections need to implement INotifyCollectionChanged, which is problematic, because our collections are commonly IList(T)s.
Why does NHibernate use an IList? When declaring a transient (new) object, we always write something such as the following:
private IList InnerType m_InnerItems = new List<InnerType>();
A "transient" collection is a new or unsaved collection that was created in your code, and the concrete implementation of "IList(T)" is a "List(T)". A persistent (saved) object is not built by your code, it is built by NHibernate. It is still an "IList(T)", but the concrete implementation is a PersistentGenericBag(T).
The PersistentGenericBag class has no default constructor, it requires an ISession as a construction parameter to support the "Lazy-Loading" magic. Since PersistentGenericBag has no default constructor, it wasn't designed for us to use in transient collections. Besides, why would we want to use an NHibernate-implementation-specific type inside of our domain objects? That would couple our domain objects too tightly with NHibernate specific implementation, in my opinion.
What to do? We need to make a new interface (I defined mine to implement INotifyCollectionChanged for my uses, but this could implement anything you need for your purposes):
using System.Collections.Generic;
using System.Collections.Specialized;
namespace NotifyingCollectionDemo.Library.Collections
{
public interface IDomainCollection<T>:INotifyCollectionChanged, IList<T>
{
}
}
We need to define a new "Transient" collection type for our interface:
using System.Collections.Generic;
using System.Collections.Specialized;
namespace NotifyingCollectionDemo.Library.Collections
{
public class TransientDomainCollection<T>:List<T>, IDomainCollection<T>
{
#region INotifyCollectionChanged Members
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate an item has been
/// added to the end of the collection.
/// </summary>
/// <param name="item">Item added to the collection.</param>
protected void OnItemAdded(T item)
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, this.Count - 1));
}
}
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate the collection
/// has been reset. This is used when the collection has been cleared or
/// entirely replaced.
/// </summary>
protected void OnCollectionReset()
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate an item has
/// been inserted into the collection at the specified index.
/// </summary>
/// <param name="index">Index the item has been inserted at.</param>
/// <param name="item">Item inserted into the collection.</param>
protected void OnItemInserted(int index, T item)
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, index));
}
}
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate an item has
/// been removed from the collection at the specified index.
/// </summary>
/// <param name="item">Item removed from the collection.</param>
/// <param name="index">Index the item has been removed from.</param>
protected void OnItemRemoved(T item, int index)
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, item, index));
}
}
#endregion
/// <summary>
/// we need to re-implement the IList methods to support observability
/// </summary>
/// <param name="item"></param>
#region IList<T> members
public new void Add(T item)
{
base.Add(item);
this.OnItemAdded(item);
}
public new void Clear()
{
base.Clear();
this.OnCollectionReset();
}
public new void Insert(int index, T item)
{
base.Insert(index, item);
this.OnItemInserted(index, item);
}
public new bool Remove(T item)
{
int index = this.IndexOf(item);
bool result = base.Remove(item);
this.OnItemRemoved(item, index);
return result;
}
public new void RemoveAt(int index)
{
T item = this[index];
base.RemoveAt(index);
this.OnItemRemoved(item, index);
}
#endregion
}
}
We need to define a new "Persistent" collection type for our interface:
using System.Collections.Generic;
using System.Collections.Specialized;
using NHibernate.Collection.Generic;
using NHibernate.Engine;
namespace NotifyingCollectionDemo.Library.Collections
{
public class PersistentDomainCollection<T>:PersistentGenericBag<T>, IDomainCollection<T>
{
#region constructors
public PersistentDomainCollection(ISessionImplementor session, IList<T> coll) : base(session, coll)
{
}
public PersistentDomainCollection(ISessionImplementor session) : base(session)
{
}
#endregion
#region INotifyCollectionChanged Members
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate an item has been
/// added to the end of the collection.
/// </summary>
/// <param name="item">Item added to the collection.</param>
protected void OnItemAdded(T item)
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, this.Count - 1));
}
}
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate the collection
/// has been reset. This is used when the collection has been cleared or
/// entirely replaced.
/// </summary>
protected void OnCollectionReset()
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate an item has
/// been inserted into the collection at the specified index.
/// </summary>
/// <param name="index">Index the item has been inserted at.</param>
/// <param name="item">Item inserted into the collection.</param>
protected void OnItemInserted(int index, T item)
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, index));
}
}
/// <summary>
/// Fires the <see cref="CollectionChanged"/> event to indicate an item has
/// been removed from the collection at the specified index.
/// </summary>
/// <param name="item">Item removed from the collection.</param>
/// <param name="index">Index the item has been removed from.</param>
protected void OnItemRemoved(T item, int index)
{
if (this.CollectionChanged != null)
{
this.CollectionChanged(this, new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, item, index));
}
}
#endregion
/// <summary>
/// we need to re-implement the IList methods to support observability
/// </summary>
/// <param name="item"></param>
#region IList<T> members
public new void Add(T item)
{
base.Add(item);
this.OnItemAdded(item);
}
public new void Clear()
{
base.Clear();
this.OnCollectionReset();
}
public new void Insert(int index, T item)
{
base.Insert(index, item);
this.OnItemInserted(index, item);
}
public new bool Remove(T item)
{
int index = this.IndexOf(item);
bool result = base.Remove(item);
this.OnItemRemoved(item, index);
return result;
}
public new void RemoveAt(int index)
{
T item = this[index];
base.RemoveAt(index);
this.OnItemRemoved(item, index);
}
#endregion
}
}
Finally, we need an implementation of IUserCollectionType to tie this all together and use it in the mapping files. Notice how I treat this as a factory class:
using System.Collections;
using System.Collections.Generic;
using NHibernate.Collection;
using NHibernate.Engine;
using NHibernate.Persister.Collection;
using NHibernate.UserTypes;
namespace NotifyingCollectionDemo.Library.Collections
{
public class DomainCollectionFactory<T> :IUserCollectionType
{
#region IUserCollectionType Members
public IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister)
{
return new PersistentDomainCollection<T>(session);
}
public IPersistentCollection Wrap(ISessionImplementor session, object collection)
{
return new PersistentDomainCollection<T>(session,collection as IList<T>);
}
public object Instantiate()
{
return new TransientDomainCollection<T>();
}
public IEnumerable GetElements(object collection)
{
return (IEnumerable) collection;
}
public bool Contains(object collection, object entity)
{
return ((IList) collection).Contains(entity);
}
public object IndexOf(object collection, object entity)
{
return ((IList) collection).IndexOf(entity);
}
public object ReplaceElements(object original, object target, ICollectionPersister persister,
object owner, IDictionary copyCache, ISessionImplementor session)
{
IList result = (IList) target;
result.Clear();
foreach (object o in ((IEnumerable) original))
{
result.Add(o);
}
return result;
}
#endregion
}
}
How to use this? In your mapping file, something such as:
<bag name="Items" inverse="true" cascade="all-delete-orphan" generic="true" lazy="true"
collection-type=
"NotifyingCollectionDemo.Library.Collections.DomainCollectionFactory`1[[NotifyingCollectionDemo.Library.DomainModel.ListItem, NotifyingCollectionDemo.Library]], NotifyingCollectionDemo.Library">
<key column="ListContainerID" />
<one-to-many class="ListItem" />
</bag>
In the code:
private IDomainCollection<ListItem> _items = new TransientDomainCollection<ListItem>();
public IDomainCollection<ListItem> Items
{
get { return this._items; }
set { this._items = value; }
}
And you should be in business.
I like this code, because the NHibernate-specific stuff is only accessible from the NHibernate-specific factory. The user code never references a PersistentDomainCollection, which makes for a clean cut. Again thanks to Billy McCafferty and Damon Carr, since my solutions are "cannibalizations" of their more original works. Any thoughts?
Labels: C#, NHibernate









