In desktop applications using the model-view-presenter pattern, it's best to use a session for each presenter. This approach can also be adapted to the model-view-view model pattern. More information on these patterns is available at http://en.wikipedia.org/wiki/Model-view-presenter.
In this recipe, I'll show you how to implement this session-per-presenter pattern with dependency injection.
You'll need the named scope extension to Ninject available at http://github.com/remogloor/ninject.extensions.namedscope.
Download the source code in ZIP format and extract it. Open the Ninject.Extensions.NamedScope.sln
solution in Visual Studio and build the solution. Copy Ninject.dll
and Ninject.Extensions.NamedScope.dll
from the builddebug
folder to our Cookbook solution's Lib
folder.
If you're not familiar with the dependency injection concept, a free video tutorial is available from TekPub at http://tekpub.com/view/concepts/1.
SessionPerPresenter
.Eg.Core
project from Chapter 1, NHibernate.dll
, NHibernate.ByteCode.Castle.dll
, Ninject.dll
, and Ninject.Extensions.NamedScope.dll
.App.config
file and set up the NHibernate and log4net configuration sections. Refer to the Configuring NHibernate and Configuring NHibernate logging recipes in Chapter 2.Data
.Data
folder, create an IDao<TEntity>
interface with the following code:public interface IDao<TEntity> : IDisposable where TEntity : class { IEnumerable<TEntity> GetAll(); }
public class Dao<TEntity> : IDao<TEntity> where TEntity : class { private readonly ISessionProvider _sessionProvider; public Dao(ISessionProvider sessionProvider) { _sessionProvider = sessionProvider; } public void Dispose() { _sessionProvider.Dispose(); } public IEnumerable<TEntity> GetAll() { var session = _sessionProvider.GetCurrentSession(); IEnumerable<TEntity> results; using (var tx = session.BeginTransaction()) { results = session.QueryOver<TEntity>() .List<TEntity>(); tx.Commit(); } return results; } }
Data
folder, create an ISessionProvider
interface with the following code:public interface ISessionProvider : IDisposable { ISession GetCurrentSession(); void DisposeCurrentSession(); }
public class SessionProvider : ISessionProvider { private readonly ISessionFactory _sessionFactory; private ISession _currentSession; public SessionProvider(ISessionFactory sessionFactory) { Console.WriteLine("Building session provider"); _sessionFactory = sessionFactory; } public ISession GetCurrentSession() { if (null == _currentSession) _currentSession = _sessionFactory.OpenSession(); return _currentSession; } public void DisposeCurrentSession() { _currentSession.Dispose(); _currentSession = null; } public void Dispose() { if (_currentSession != null) _currentSession.Dispose(); _currentSession = null; } }
NinjectBindings
with the following code:public class NinjectBindings : NinjectModule { public override void Load() { const string presenterScope = "PresenterScope"; var asm = GetType().Assembly; var presenters = from t in asm.GetTypes() where typeof (IPresenter).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract select t; foreach (var presenterType in presenters) Kernel.Bind(presenterType) .ToSelf() .DefinesNamedScope(presenterScope); Kernel.Bind<ISessionProvider>() .To<SessionProvider>() .InNamedScope(presenterScope); Kernel.Bind(typeof(IDao<>)) .To(typeof(Dao<>)); } }
ProductListView
class with the following code:public class ProductListView { private readonly string _description; private readonly IEnumerable<Product> _products; public ProductListView( string description, IEnumerable<Product> products) { _description = description; _products = products; } public void Show() { Console.WriteLine(_description); foreach (var p in _products) Console.WriteLine(" * {0}", p.Name); } }
IPresenter
interface inherited from IDisposable
. This interface can be left empty.MediaPresenter
class with the following code:public class MediaPresenter : IPresenter { private readonly IDao<Movie> _movieDao; private readonly IDao<Book> _bookDao; public MediaPresenter(IDao<Movie> movieDao, IDao<Book> bookDao) { _movieDao = movieDao; _bookDao = bookDao; } public ProductListView ShowBooks() { return new ProductListView("All Books", _bookDao.GetAll().OfType<Product>()); } public ProductListView ShowMovies() { return new ProductListView("All Movies", _movieDao.GetAll().OfType<Product>()); } public void Dispose() { _movieDao.Dispose(); _bookDao.Dispose(); } }
ProductPresenter
class with the following code:public class ProductPresenter : IPresenter { private readonly IDao<Product> _productDao; public ProductPresenter(IDao<Product> productDao) { _productDao = productDao; } public ProductListView ShowAllProducts() { return new ProductListView("All Products", _productDao.GetAll()); } public virtual void Dispose() { _productDao.Dispose(); } }
Program.cs
, in the Main
method, add the following code:var nhConfig = new Configuration().Configure(); var sessionFactory = nhConfig.BuildSessionFactory(); var kernel = new StandardKernel(); kernel.Load(new Data.NinjectBindings()); kernel.Bind<ISessionFactory>() .ToConstant(sessionFactory); var media1 = kernel.Get<MediaPresenter>(); var media2 = kernel.Get<MediaPresenter>(); media1.ShowBooks().Show(); media2.ShowMovies().Show(); media1.Dispose(); media2.Dispose(); using (var product = kernel.Get<ProductPresenter>()) { product.ShowAllProducts().Show(); } Console.WriteLine("Press any key"); Console.ReadKey();
NHCookbook
database.There are several interesting items in this recipe to discuss. First, we've set up a slightly complex object graph. For each instance of MediaPresenter
, our graph appears as shown in the next image:
In the previous image, one instance of session provider is shared by both data access objects. This is accomplished with the configuration of Ninject, our dependency injection framework
In our NinjectBindings
, we match up our service interfaces to their matching implementations. We bind the open generic IDao<>
interface to Dao<>
, so that requests for IDao<Book>
are resolved to Dao<Book>
, IDao<Movie>
to Dao<Movie>
, and so on.
One session per presenter is accomplished with the use of DefinesNamedScope
and InNamedScope
. We find all of the IPresenter
implementations in the assembly. Each presenter is bound and defines the PresenterScope
. When we bind ISessionProvider
to SessionProviderImpl
, we use InNamedScope("PresenterScope")
to indicate that we will have only one session provider per presenter.
A simple call to Kernel.Get<MediaPresenter>()
will return a new presenter instance all wired up and ready to use. It will have two data access objects sharing a common session provider. To close the session and release any lingering database connections, be sure to call
Dispose()
when you're finished with the presenter.
A typical Save
method on a Dao may look something like this:
var session = _sessionProvider.GetCurrentSession(); try { session.SaveOrUpdate(entity); } catch (StaleObjectStateException) { _sessionProvider.DisposeCurrentSession(); throw; }
Notice how we are immediately throwing away the session in the catch block. When NHibernate throws an exception from inside a session call, the session's state is undefined. The only remaining operation you can safely perform on that session is Dispose()
. This allows us to recover gracefully from any exceptions, as the exploded session is already thrown away, so a fresh session can take its place.
You should also take care with entities still associated with this failed session. It's usually a good idea to attach them to the new session, as any operation, including lazy loading, against the failed session will cause further exceptions. The session.Merge recipe mentioned later in this chapter discusses a method for accomplishing this.
Because the boundaries are not as well-defined as in a web application, there are two very common anti-patterns for handling NHibernate sessions in desktop applications. The first, a singleton session, has the following problems:
The second, a micro-session, where a session is opened to perform a single operation and then quickly closed loses all of the benefits of the unit of work, most notably the session cache. Entities will be constantly re-fetched from the database.