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


2 Comments:
definitely a little easier to understand. thanks for the info.
Nice work! I often sort of 'throw up' ideas without the care you have put into making this far more enjoyable. I am about to post a pretty extensive item on the 'DomainDataSource' control I recently finished which I find really cool for getting the view layer(s) started. It does pretty much what you would expect (works with collections, single instances, across all ORMs mentioned below.. etc)
It's great to know another person dedicated to making ORM ubiqutous and not something people consider is a 'nice to have'. Don't you wish that on average .NET 'involved' developers were 1/2 as aware as your average Java developer was say 2-3 years ago? Ahh.. But that is changing ast I am glad to see.
In my opinion, for most systems, not using SOME ORM (which really means NHibernate, ActiveRecord as a nice expediting technology WITH NHibernate, or Microsoft Linq to Entities) any project is throwing an insane amount of their budget down the toilet (and people using other abstraction layers, you are only shifting the costs into maintenance, and not helping anyone really).
But hey, I have no real opinion (grin)..
Damon Carr
Post a Comment
<< Home