views:

137

answers:

5

Hello all,

here is my problem: I'm building a desktop application, with the following tools:

  • Caliburn
  • Ninject
  • NHibernate

All my view models and repositories are instanciated with Ninject. My repositories all need an ISession in their constructor.

I'd like to follow ayende's advice concerning the ViewModels: each ViewModel opens a new session.

Is it possible to configure Ninject to open a new session when a ViewModel is created, and use this session inside the repositories used by this view model?

I had a look to the InScope function of Ninject, as well as the ICurrentSessionContext interface in NHibernate, but I don't know how to model all of that to get what I want...

Did someone make something like that before?

Thanks in advance

Mike

A: 

I solved a similar scenario leveraging the ViewModel lifecycle: I created an ISessionAware interface (with a SetSession method) to be implemented by repositories, then I initialized the repositories through ISessionAware in the OnInitialize method of the ViewModel (which is enforced by Caliburn when the VM is managed by a ScreenConductor).

Using reflection to inspect the properties holding the repositories, I could put all the infrastructure on a BaseDataVM class.

Using a scope in the container would be more elegant, I think, but I don't know Ninject.

Marco Amendola
A: 

I have a very similar project (except I'm not using Caliburn) and have been trying to figure out how to do this as well. I did come up with one method that works well for constructor injection using Ninject's InScope() method.

I have a static class called IoC that wraps access to Ninject's kernel. Since the dependencies are all injected into the constructor, the context is only relevant when the object is being created. So it doesn't matter what is supplied for context, but a Guid feels like the safe choice. Program.OpenSession() is a static method to open a new ISession.

public static class Ioc
{
    private static readonly IKernel _kernel;

    static IoC()
    {
        _kernel = new StandardKernel();
        _kernel.Load(new ContextModule());
    }

    private static object _context;

    public static T ResolveInContext<T>(object context)
    {
        _context = context;
        var result = _kernel.Get<T>();
        _context = null;
        return result;
    }

    private class ContextModule : NinjectModule
    {
        public override void Load()
        {
            Bind<ISession>().ToMethod(x => Program.OpenSession()).InScope(x => _context);
            Bind<frmCompanyViewer>().ToSelf().InScope(x => _context);
        }
    }
}

Usage is:

var frm = IoC.ResolveInContext<frmCompanyViewer>(Guid.NewGuid());

The form's constructor signature is:

public frmCompanyViewer(ISession session, ICompanyRepository companyRepository)

I verified that with InScope on the bindings, the same ISession that is used to construct frmCompanyViewer is also used to construct companyRepository. If I remove InScope then two ISessions are used.

Edited to add: This would also work, see comments. This should be made thread safe for a real application. I changed the method name to ConstructInContext to clarify that the context only applies during object construction.

    public static T ConstructInContext<T>()
    {
        _context = Guid.NewGuid();
        var result = _kernel.Get<T>();
        _context = null;
        return result;
    }
Jamie Ide
And does this work if I don't instantiate the repository with constructor injection, but do something like this IN the constructor?this.repository = IoC.ResolveInContext<IRepository>(form.Guid);
Mike
If you mean have a static Guid property on your form, then yes it should work. The context only applies while the Ninject constructs the object, so even if you passed the same Guid as a static property on the form, each form would get its own ISession. Now that I think about it, you don't need to pass it anything since the context only applies during construction. ResolveInContext could just set the context. It's obviously not thread safe however.
Jamie Ide
A: 

We have this with AOP, in unhaddins. Is called "Conversation per Business Transaction".

search in google

José F. Romaniello
Why the downvote?
apollodude217
A: 

Well, I've found a solution thanks to the ninject group.

The solution here is to use the function InScope when I bind ISession, and browse in the IContext variable to inspect the services. If one service in the request hierarchy is assignable to the base class of my view models, I use the context as scope.

So the first time an ISession will be injected in the constructor of my ViewModel, a new scope is used. And all subsequent calls to ISession inside the constructor of the ViewModel will be resolved with the same scope. And then only one session is created for my ViewModel.

Here is the code:

Bind<ISession>().ToMethod(ctx =>
    {
        var session = ctx.Kernel.Get<INHibernateSessionFactoryBuilder>()
            .GetSessionFactory()
            .OpenSession();

        session.FlushMode = FlushMode.Commit;

        return session;
    })
    .InScope(ctx =>
    {
        var request = ctx.Request;

        if (request.Service is IScreen)
            return request;

        while ((request = request.ParentRequest) != null)
            if (typeof(IScreen).IsAssignableFrom(request.Service))
                return request;

        return new object();
    });

And the constructor of the viewmodel must contains all the injected dependencies which rely on the ISession:

[Inject]
public PlayersManagementViewModel(ISession session, IPlayersRepository playersRepository)
{
}

Hope that helps

Mike
What is IScreen?
Jamie Ide
IScreen is the base interface of view models in Caliburn
Mike