Search code examples
c#winformsgenericssimple-injector

Winforms IoC Container - How to handle concrete types with a presenter factory


Background

I'm using Winforms with a MVP pattern to create an application. I'm using SimpleInjector as my IoC container. My presenters inherit from:

public interface IPresenter<TView>
{
    TView View { get; set; }
}

internal class HomePresenter : IPresenter<IHomeView>
{
    public IHomeView View { get; set; }

    ...
}

In order to create my presenters, I have decided to use a presenter factory with the following method:

public static IPresenter<TView> CreateForView<TView>(TView view)
    {
        var presenter = _container.GetInstance<IPresenter<TView>>();
        presenter.View = view;
        return presenter;
    }

And then in each view, the view creates its own presenter by calling the presenter factory:

_homeMainPresenter = (HomePresenter) presenterFactory.CreateForView<IHomeView>(this);
_homeMainPresenter.View = this;

In my Program.cs file, I have:

    static void Main()
    {
        System.Windows.Forms.Application.EnableVisualStyles();
        System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);

        Bootstrap();

        System.Windows.Forms.Application.Run((HomeView)container.GetInstance<IHomeView>());
    }

    private static void Bootstrap()
    {
        // Create the container
        container = new Container();

        // Register types
        container.Register<IHomeView, HomeView>(Lifestyle.Singleton);
        container.Register<IPresenter<IHomeView>, HomePresenter>();
        ...

        // Verify the container
        container.Verify();
    }

Problem

When the presenter factory is called from the HomeView view, the type fed into the factory is a HomeView type, not IHomeView. So, the application throws an exception because the container does not have a HomeView registration (only IHomeView). My presenters all have interfaces for the views references they store as I feel this will be better for testing. How do I avoid this situation?


Solution

  • Having a interface-to-implementation binding for your forms is not useful, since they are root types for your presentation technology. Most presentation technologies can't deal with custom abstractions anyway and this is the reason that you are casting your IHomeView back to HomeView to allow it to be passed on to the Application.Run method.

    Instead of resolving the presenter from within the view, you can do the following instead:

    public interface IHomeView { }
    
    public interface IPresenter<TView> {
        TView View { get; set; }
    }
    
    public class HomeView : Form, IHomeView
    {
        private readonly IPresenter<IHomeView> presenter;
    
        public HomeView(IPresenter<IHomeView> presenter) {
            this.presenter = presenter;
            InitializeComponent();
        }
    }
    

    Here the Form gets injected with an IPresenter<IHomeView> and stores that incoming dependency. The factory is not needed anymore and can be removed from your code.

    And in your program main:

    static void Main()
    {
        System.Windows.Forms.Application.EnableVisualStyles();
        System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
    
        Bootstrap();
    
        System.Windows.Forms.Application.Run(GetForm<HomeView, IHomeView>(container));
    }
    
    private static void Bootstrap()
    {
        // Create the container
        container = new Container();
    
        // Register types
        // NOTE: We register HomeView as concrete type; not by its interface.
        container.Register<HomeView>(Lifestyle.Singleton);
    
        // Here we batch-register all presenters with one line of code and
        // since the forms depend on them, they need to be singletons as well.
        container.Register(typeof(IPresenter<>),  AppDomain.CurrentDomain.GetAssemblies(), 
            Lifestyle.Singleton);
        ...
    
        // Verify the container
        container.Verify();
    }
    
    private static TForm GetForm<TForm, TView>() where TForm : Form, TView
    {
        var form = container.GetInstance<TForm>();
        container.GetInstance<IPresenter<TView>>().View = form;
        return form;
    }
    

    The factory class is now replaced with the GetForm method that is part of the composition root; Forms don't have access to it. The generic types allow us to resolve the proper presenter, while keeping the code type-safe.