6

Background

I'm building a winforms application where I am using an IoC container (SimpleInjector) to register my types. In my application, a majority of the screens (i.e. forms) will only have one instance at any given time.

Problem

For forms that only need one instance at any given time, I can register them as singletons:

container.Register<IHomeView, HomeView>(Lifestyle.Singleton);

This allows me to use the container to keep track of all forms. In this case, however, when a form gets closed it will then get disposed (forms implement IDisposable). If the application tries to open that form again using the container, the container's instance of the form will be disposed, and an exception is thrown.

Question

What is the proper way to deal with this? I currently see two solutions:

  1. For each form, override the form close to instead hide the form, rather than actually close it. I don't really like this idea. I feel like I'd rather close the form each time and start with a new/fresh form.
  2. Register the form with a transient lifestyle rather than as a singleton. In this case, the container really just acts more as a factory. I run into two problems: a) I lose the ability to track forms through the container, and, b) the container throws an exception during verification saying that disposable types should not be registered as transient (which I don't understand why that is). These problems both apply to forms where I will need multiple instances at once as well.

I can get around problem b) by suppressing the diagnostic warning during verification.

registration = container.GetRegistration(typeof(ILoginView)).Registration;
registration.SuppressDiagnosticWarning(
    DiagnosticType.DisposableTransientComponent,
    "Winforms registration supression.");

What is the correct approach to be taking here? Am I missing something?

Steven
  • 166,672
  • 24
  • 332
  • 435
Andrew
  • 893
  • 12
  • 28
  • 1
    Consider overriding and suppressing the closing event, calling Form.Hide instead. That being said Form as singleton seems a bit janky (What happens when you want a form to be parented by two different entities?). I'd prefer a factory that handles creation. – Warty Jul 17 '16 at 03:36
  • Thank you for you input. I've been leaning more towards using a factory and tracking the forms on my own. Do you know why SimplerInjector doesn't like me registering Forms as transient? – Andrew Jul 17 '16 at 03:44

1 Answers1

19

Ideally, you would want to register your forms as Singleton. In my experience, however, this will result in hard to debug errors, especially when you use a BindingSource for binding your data to whatever.

A second problem with using Singleton as the lifestyle is that if your application uses modeless windows, this windows will throw an ObjectDisposedException when opened a second time, because the Windows Forms Application framework will dispose the Form on the first close, while Simple Injector should be in charge of that. So Simple Injector will create one–and exactly one–instance, if registered as Singleton. If somebody else (e.g. your application, the windows forms framework) will dispose the object, it won't be recreated.

The most easy solution, which is also easy to understand, is to register your forms as Transient. And yes, you need to suppress the diagnostic warnings. The reason for this diagnostic warning according to the documentation:

A component that implements IDisposable would usually need deterministic clean-up but Simple Injector does not implicitly track and dispose components registered with the transient lifestyle.

Simple Injector is unable to dispose a transient component because it is unable to determine when the object should be disposed. This means, however, that forms that are opened in a modal fashion with a call to .ShowDialog() will never be disposed! And because a windows forms application typically runs for a long time, maybe even a week or month, this will eventually result in a 'Win32Exception' with a message: "Error Creating Window Handle". Which essentially means you exhausted all resources of the computer.

Disposing of the forms is therefore important. And although Simple Injector is able to do this job if you would use a Scope, this is with Windows Forms not so easy to implement. So you yourself have to take care of disposing the closed Forms which have been shown using ShowDialog().

Depending on your specific use case there are several ways to implement a FormOpener or NavigationService. One way to do it:

public interface IFormOpener
{
    void ShowModelessForm<TForm>() where TForm : Form;
    DialogResult ShowModalForm<TForm>() where TForm : Form;
}

public class FormOpener : IFormOpener
{
    private readonly Container container;
    private readonly Dictionary<Type, Form> openedForms;

    public FormOpener(Container container)
    {
        this.container = container;
        this.openedForms = new Dictionary<Type, Form>();
    }

    public void ShowModelessForm<TForm>() where TForm : Form
    {
        Form form;
        if (this.openedForms.ContainsKey(typeof(TForm)))
        {
            // a form can be held open in the background, somewhat like 
            // singleton behavior, and reopened/reshown this way
            // when a form is 'closed' using form.Hide()   
            form = this.openedForms[typeof(TForm)];
        }
        else
        {
            form = this.GetForm<TForm>();
            this.openedForms.Add(form.GetType(), form);
            // the form will be closed and disposed when form.Closed is called
            // Remove it from the cached instances so it can be recreated
            form.Closed += (s, e) => this.openedForms.Remove(form.GetType());
        }

        form.Show();
    }

    public DialogResult ShowModalForm<TForm>() where TForm : Form
    {
        using (var form = this.GetForm<TForm>())
        {
            return form.ShowDialog();
        }
    }

    private Form GetForm<TForm>() where TForm : Form
    {
        return this.container.GetInstance<TForm>();
    }
}

This class must be registered as Singleton:

container.RegisterSingleton<IFormOpener, FormOpener>();

And can be used by injecting this service in for example your root form of the application:

public partial class RootForm : Form
{
    private readonly IFormOpener formOpener;

    public RootForm(IFormOpener formOpener)
    {
        this.formOpener = formOpener;
        this.InitializeComponent();
    }

    private void ShowCustomers_Click(object sender, EventArgs e)
    {
        this.formOpener.ShowModelessForm<AllCustomersForm>();
    }

    private void EditCustomer_Click(object sender, EventArgs e)
    {
        var result = this.formOpener.ShowModalForm<EditCustomerForm>();
        // do something with result
    }
}
Steven
  • 166,672
  • 24
  • 332
  • 435
Ric .Net
  • 5,540
  • 1
  • 20
  • 39
  • Fantastic answer, thank you! I had begun setting up an application controller that would manage opening forms, as well as other things like having a global event aggregator. I like how you handle hidden forms! To a point you discussed, my application will be comprised of all modeless forms. This is makes it difficult to have a single form manager injected into the root form (what happens when child forms want to open further forms?). I have started by creating my application controller (including the form manager) as set of static classes. What are your thought on that? – Andrew Jul 17 '16 at 17:02
  • @Andrew, I maybe have some ideas. Could you post a new question which has more detail about the domain and intended behavior of the application? If you post a comment here linked the new question I will try to answer it asap. – Ric .Net Jul 19 '16 at 09:35
  • I would love to hear your feedback @Ric. I have create a new question [here](http://stackoverflow.com/questions/38471351/winforms-mvp-pattern-using-static-applicationcontroller-to-coordinate-applica) – Andrew Jul 20 '16 at 02:40
  • 1
    I followed the WinForms integration guide https://simpleinjector.readthedocs.io/en/latest/windowsformsintegration.html so why is this problem occurring? I'm lost as to why the verbatim code provided on their own page fails. – Stephen York Oct 25 '19 at 03:00
  • And what about wrapping the resolved transient form in a using block in order to force the disposing by the application framework? – jacktric Jun 23 '20 at 16:17
  • @jacktric did you see 'ShowModalForm' in the FormOpener class? – Ric .Net Jun 23 '20 at 20:43
  • Oh, well. Now that I looked it better I've seen that ShowModalForm wraps the form instance in a using block itself. – jacktric Jun 24 '20 at 05:26
  • And so what would one do if the form to be opened needed service dependencies? – AMG Jul 19 '23 at 12:59