Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WinForms MVC with Dependency Injection

I am in the process of re-writing a WinForms application from scratch (and it has to be WinForms, as much as I want to use WPF and MVVM). Do do this I have chosen to use the MVC pattern and attempt to use Dependency Injection (DI) where possible to increase testability, maintainability etc.

The problem I am having is with use of MVC and DI. With the baisic MVC pattern, the controller must have access to the view and the view must have access to the controller (see here for an WinForms example); this leads to a circular reference when using Ctor-Injection and this is the crux of my question. First please consider my code

Program.cs (the main entry point of the WinForms application):

static class Program
{
    [STAThread]
    static void Main()
    {
        FileLogHandler fileLogHandler = new FileLogHandler(Utils.GetLogFilePath());
        Log.LogHandler = fileLogHandler;
        Log.Trace("Program.Main(): Logging initialized");

        CompositionRoot.Initialize(new DependencyModule());
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(CompositionRoot.Resolve<ApplicationShellView>());
    }
}

DependencyModule.cs

public class DependencyModule : NinjectModule
{
    public override void Load()
    {
        Bind<IApplicationShellView>().To<ApplicationShellView>();

        Bind<IDocumentController>().To<SpreadsheetController>();
        Bind<ISpreadsheetView>().To<SpreadsheetView>();
    }
}

CompositionRoot.cs

public class CompositionRoot
{
    private static IKernel ninjectKernel;

    public static void Initialize(INinjectModule module)
    {
        ninjectKernel = new StandardKernel(module);
    }

    public static T Resolve<T>()
    {
        return ninjectKernel.Get<T>();
    }

    public static IEnumerable<T> ResolveAll<T>()
    {
        return ninjectKernel.GetAll<T>();
    }
}

ApplicationShellView.cs (the main form of the application)

public partial class ApplicationShellView : C1RibbonForm, IApplicationShellView
{
    private ApplicationShellController controller; 

    public ApplicationShellView()
    {
        this.controller = new ApplicationShellController(this);
        InitializeComponent();
        InitializeView();
    }

    public void InitializeView()
    {
        dockPanel.Extender.FloatWindowFactory = new CustomFloatWindowFactory();
        dockPanel.Theme = vS2012LightTheme;
    }

    private void ribbonButtonTest_Click(object sender, EventArgs e)
    {
        controller.OpenNewSpreadsheet();
    }

    public DockPanel DockPanel
    {
        get { return dockPanel; }
    }
}

Where:

public interface IApplicationShellView
{
    void InitializeView();

    DockPanel DockPanel { get; }
}

ApplicationShellController.cs

public class ApplicationShellController
{
    private IApplicationShellView shellView;

    [Inject]
    public ApplicationShellController(IApplicationShellView view)
    {
        this.shellView = view;
    }

    public void OpenNewSpreadsheet(DockState dockState = DockState.Document)
    {
        SpreadsheetController controller = (SpreadsheetController)GetDocumentController("new.xlsx");
        SpreadsheetView view = (SpreadsheetView)controller.New("new.xlsx");
        view.Show(shellView.DockPanel, dockState);
    }

    private IDocumentController GetDocumentController(string path)
    {
        return return CompositionRoot.ResolveAll<IDocumentController>()
            .SingleOrDefault(provider => provider.Handles(path));
    }

    public IApplicationShellView ShellView { get { return shellView; } }
}

SpreadsheetController.cs

public class SpreadsheetController : IDocumentController 
{
    private ISpreadsheetView view;

    public SpreadsheetController(ISpreadsheetView view)
    {
        this.view = view;
        this.view.SetController(this);
    }

    public bool Handles(string path)
    {
        string extension = Path.GetExtension(path);
        if (!String.IsNullOrEmpty(extension))
        {
            if (FileTypes.Any(ft => ft.FileExtension.CompareNoCase(extension)))
                return true;
        }
        return false;
    }

    public void SetViewActive(bool isActive)
    {
        ((SpreadsheetView)view).ShowIcon = isActive;
    }

    public IDocumentView New(string fileName)
    {
        // Opens a new file correctly.
    }

    public IDocumentView Open(string path)
    {
        // Opens an Excel file correctly.
    }

    public IEnumerable<DocumentFileType> FileTypes
    {
        get
        {
            return new List<DocumentFileType>()
            {
                new DocumentFileType("CSV",  ".csv" ),
                new DocumentFileType("Excel", ".xls"),
                new DocumentFileType("Excel10", ".xlsx")
            };
        }
    }
}

Where the implemented interface is:

public interface IDocumentController
{
    bool Handles(string path);

    void SetViewActive(bool isActive);

    IDocumentView New(string fileName);

    IDocumentView Open(string path);

    IEnumerable<DocumentFileType> FileTypes { get; }
}

Now the view ascociated with this controller is:

public partial class SpreadsheetView : DockContent, ISpreadsheetView
{
    private IDocumentController controller;

    public SpreadsheetView()
    {
        InitializeComponent();
    }

    private void SpreadsheetView_Activated(object sender, EventArgs e)
    {
        controller.SetViewActive(true);
    }

    private void SpreadsheetView_Deactivate(object sender, EventArgs e)
    {
        controller.SetViewActive(false);
    }

    public void SetController(IDocumentController controller)
    {
        this.controller = controller;
        Log.Trace("SpreadsheetView.SetController(): Controller set successfully");
    }

    public string DisplayName
    {
        get { return Text; }
        set { Text = value; }
    }

    public WorkbookView WorkbookView
    {
        get { return workbookView; }
        set { workbookView = value; }
    }

    public bool StatusBarVisible
    {
        get { return statusStrip.Visible; }
        set { statusStrip.Visible = value; }
    }

    public string StatusMessage
    {
        get { return statusLabelMessage.Text; }
        set { statusLabelMessage.Text = value; }
    }
}

The view interfaces are:

public interface ISpreadsheetView : IDocumentView
{
    WorkbookView WorkbookView { get; set; } 
}

And:

public interface IDocumentView
{
    void SetController(IDocumentController controller);

    string DisplayName { get; set; }

    bool StatusBarVisible { get; set; }
}

I am new to DI and Ninject so I have two questions:

  1. How can I prevent myself using this.view.SetController(this); in the SpreadsheetController, here it feels like I should be using the IoC Container, but using pure Ctor-Injection leads to circular reference and a StackOverflowException. Can this be done using pure DI?

because I don't have a binding framework like with WPF (or with ASP.NET the ability to link the view and the controller implicitly), I have to expose the view and the controller to each other explicitly. This does not "feel" right and I think this should be possible vie the Ninject IoC container but I don't have the experience to establish how this can be done (if it can).

  1. Is my use of Ninject/DI correct here. The way I am using my CompositionRoot and the method GetDocumentController(string path) feels like the service locator anti-pattern, how can I make this right?

At the moment this code works fine, but I want to get it right. Thanks very much for your time.

like image 293
MoonKnight Avatar asked Apr 01 '16 09:04

MoonKnight


1 Answers

I am working on a project with a similar architecture.

I guess your main problem is that the event handlers of your view directly call the controller. E.g:

private void ribbonButtonTest_Click(object sender, EventArgs e)
{
    controller.OpenNewSpreadsheet();
}

Try to avoid this. Let your controller objects be the masters of your application. Let the views and models be "blind and deaf".

When your view encounters a user action, just raise another event. Let the controller be responsible to register to this event and handle it. Your view is going to be like this:

public event EventHandler<EventArgs> RibbonButtonTestClicked ;

protected virtual void ribbonButtonTest_Click(object sender, EventArgs e)
{
    var handler = RibbonButtonTestClicked;
    if (handler != null) handler(this, EventArgs.Empty);
}

If you do this, you should be able to get rid of all the controller reference in the view. Your controller contstructor will look like this:

[Inject]
public ApplicationShellController(IApplicationShellView view)
{
    this.shellView = view;
    this.shellView.RibbonButtonTestClicked += this.RibbonButtonTestClicked;
}

Since you can not resolve your object tree from a view anymore, add a method "GetView()" to your controller and change your Program.Main() method:

CompositionRoot.Initialize(new DependencyModule());
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var appCtrl = CompositionRoot.Resolve<ApplicationShellController>()
Application.Run(appCtrl.GetView());
like image 193
Chrigl Avatar answered Oct 06 '22 00:10

Chrigl