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:
- How can I prevent myself using
this.view.SetController(this);
in theSpreadsheetController
, here it feels like I should be using the IoC Container, but using pure Ctor-Injection leads to circular reference and aStackOverflowException
. 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).
- Is my use of Ninject/DI correct here. The way I am using my
CompositionRoot
and the methodGetDocumentController(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.