具有依赖注入的 WinForms MVC
WinForms MVC with Dependency Injection
我正在从头开始重写 WinForms 应用程序(它 必须 是 WinForms,因为我想使用 WPF 和 MVVM)。这样做我选择使用 MVC 模式并尝试在可能的情况下使用依赖注入 (DI) 来提高可测试性、可维护性等。
我遇到的问题是使用 MVC 和 DI。使用基本 MVC 模式,控制器必须能够访问视图,而视图必须能够访问控制器(参见 here 的 WinForms 示例);这导致在使用 Ctor-Injection 时出现循环引用,这是我问题的症结所在。首先请考虑我的代码
Program.cs(WinForms应用程序的主要入口点):
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(申请的主要形式)
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; }
}
}
其中:
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")
};
}
}
}
其中实现的接口是:
public interface IDocumentController
{
bool Handles(string path);
void SetViewActive(bool isActive);
IDocumentView New(string fileName);
IDocumentView Open(string path);
IEnumerable<DocumentFileType> FileTypes { get; }
}
现在与此控制器关联的视图是:
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; }
}
}
查看界面是:
public interface ISpreadsheetView : IDocumentView
{
WorkbookView WorkbookView { get; set; }
}
并且:
public interface IDocumentView
{
void SetController(IDocumentController controller);
string DisplayName { get; set; }
bool StatusBarVisible { get; set; }
}
我是 DI 新手,Ninject 所以我有两个问题:
- 如何防止自己在
SpreadsheetController
中使用this.view.SetController(this);
,这里感觉我应该使用IoC Container,但是使用纯Ctor-Injection会导致循环引用和WhosebugException
。这可以使用纯 DI 来完成吗?
因为我没有像 WPF 这样的绑定框架(或者 ASP.NET 隐式 link 视图和控制器的能力),我必须公开视图和控制器明确地相互联系。这不 "feel" 正确,我认为这应该可以通过 Ninject IoC 容器实现,但我没有经验来确定如何做到这一点(如果可以的话)。
- 我在这里使用 Ninject/DI 是正确的吗?我使用
CompositionRoot
和方法 GetDocumentController(string path)
的方式感觉像是服务定位器反模式,我怎样才能做到这一点?
目前这段代码工作正常,但我想把它弄好。非常感谢您的宝贵时间。
首先我必须说我不使用 WinForms,但我认为您的实现存在一些问题。首先,这个区块
private IDocumentController GetDocumentController(string path)
{
return return CompositionRoot.ResolveAll<IDocumentController>()
.SingleOrDefault(provider => provider.Handles(path));
}
表示你的容器中可能注册了多个IDocumentController。现在请注意 SpreadsheetController
在构造函数中使用 ISpreadsheetView
。这意味着当这个控制器被解析时,SpreadsheetView 被解析和构建,并且该视图是 UI 控件,构建起来可能很昂贵。现在假设您注册了 20 个 IDocumentController。当上面的代码(GetDocumentController
)执行时,它们被全部解析并构造了20个UI控件,其中19个被立即丢弃。
这不好,表明您不需要在该控制器的构造函数中获取视图的实例。相反,您需要一种 方式 来在需要时创建该实例,这将我们引向工厂模式。创建 ISpreadsheetViewFactory(或什至 IDocumentViewFactory),它将为您创建 IDocumentViews 的实例。像这样:
interface IDocumentViewFactory {
ISpreadsheetView Create(IDocumentController controller);
}
并实施
class DocumentViewFactory : IDocumentViewFactory {
public ISpreadsheetView Create(IDocumentController controller) {
return new SpreadsheetView(controller);
}
}
然后,在您的容器中注册该工厂,更改 SpreadsheetView 的构造函数,删除 SetContainer 方法,并将 SpreadsheetController 的构造函数更改为接受 IDocumentViewFactory。然后,不要 直接在控制器构造函数中创建视图,因为见上文 - 这可能会创建许多 UI 控件。相反,使用 Lazy 模式并仅在需要时实例化 SpreadsheetView(使用工厂)。
关于你的第二个问题 - 是的,你在你的 GetDocumentController
中使用你的容器作为服务定位器。如果您想避免这种情况,请使用 Multi Injection 并将 IDocumentController
数组注入主视图的构造函数。
我正在从事一个具有类似架构的项目。
我猜你的主要问题是视图的事件处理程序直接调用控制器。例如:
private void ribbonButtonTest_Click(object sender, EventArgs e)
{
controller.OpenNewSpreadsheet();
}
尽量避免这种情况。让您的控制器对象成为您应用程序的主人。让视图和模型为 "blind and deaf".
当您的视图遇到用户操作时,只需引发另一个事件。让控制器负责注册到这个事件并处理它。你的观点将是这样的:
public event EventHandler<EventArgs> RibbonButtonTestClicked ;
protected virtual void ribbonButtonTest_Click(object sender, EventArgs e)
{
var handler = RibbonButtonTestClicked;
if (handler != null) handler(this, EventArgs.Empty);
}
如果这样做,您应该能够删除视图中的所有控制器引用。您的控制器构造函数将如下所示:
[Inject]
public ApplicationShellController(IApplicationShellView view)
{
this.shellView = view;
this.shellView.RibbonButtonTestClicked += this.RibbonButtonTestClicked;
}
由于您无法再从视图中解析对象树,请将方法 "GetView()" 添加到控制器并更改 Program.Main() 方法:
CompositionRoot.Initialize(new DependencyModule());
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var appCtrl = CompositionRoot.Resolve<ApplicationShellController>()
Application.Run(appCtrl.GetView());
我正在从头开始重写 WinForms 应用程序(它 必须 是 WinForms,因为我想使用 WPF 和 MVVM)。这样做我选择使用 MVC 模式并尝试在可能的情况下使用依赖注入 (DI) 来提高可测试性、可维护性等。
我遇到的问题是使用 MVC 和 DI。使用基本 MVC 模式,控制器必须能够访问视图,而视图必须能够访问控制器(参见 here 的 WinForms 示例);这导致在使用 Ctor-Injection 时出现循环引用,这是我问题的症结所在。首先请考虑我的代码
Program.cs(WinForms应用程序的主要入口点):
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(申请的主要形式)
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; }
}
}
其中:
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")
};
}
}
}
其中实现的接口是:
public interface IDocumentController
{
bool Handles(string path);
void SetViewActive(bool isActive);
IDocumentView New(string fileName);
IDocumentView Open(string path);
IEnumerable<DocumentFileType> FileTypes { get; }
}
现在与此控制器关联的视图是:
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; }
}
}
查看界面是:
public interface ISpreadsheetView : IDocumentView
{
WorkbookView WorkbookView { get; set; }
}
并且:
public interface IDocumentView
{
void SetController(IDocumentController controller);
string DisplayName { get; set; }
bool StatusBarVisible { get; set; }
}
我是 DI 新手,Ninject 所以我有两个问题:
- 如何防止自己在
SpreadsheetController
中使用this.view.SetController(this);
,这里感觉我应该使用IoC Container,但是使用纯Ctor-Injection会导致循环引用和WhosebugException
。这可以使用纯 DI 来完成吗?
因为我没有像 WPF 这样的绑定框架(或者 ASP.NET 隐式 link 视图和控制器的能力),我必须公开视图和控制器明确地相互联系。这不 "feel" 正确,我认为这应该可以通过 Ninject IoC 容器实现,但我没有经验来确定如何做到这一点(如果可以的话)。
- 我在这里使用 Ninject/DI 是正确的吗?我使用
CompositionRoot
和方法GetDocumentController(string path)
的方式感觉像是服务定位器反模式,我怎样才能做到这一点?
目前这段代码工作正常,但我想把它弄好。非常感谢您的宝贵时间。
首先我必须说我不使用 WinForms,但我认为您的实现存在一些问题。首先,这个区块
private IDocumentController GetDocumentController(string path)
{
return return CompositionRoot.ResolveAll<IDocumentController>()
.SingleOrDefault(provider => provider.Handles(path));
}
表示你的容器中可能注册了多个IDocumentController。现在请注意 SpreadsheetController
在构造函数中使用 ISpreadsheetView
。这意味着当这个控制器被解析时,SpreadsheetView 被解析和构建,并且该视图是 UI 控件,构建起来可能很昂贵。现在假设您注册了 20 个 IDocumentController。当上面的代码(GetDocumentController
)执行时,它们被全部解析并构造了20个UI控件,其中19个被立即丢弃。
这不好,表明您不需要在该控制器的构造函数中获取视图的实例。相反,您需要一种 方式 来在需要时创建该实例,这将我们引向工厂模式。创建 ISpreadsheetViewFactory(或什至 IDocumentViewFactory),它将为您创建 IDocumentViews 的实例。像这样:
interface IDocumentViewFactory {
ISpreadsheetView Create(IDocumentController controller);
}
并实施
class DocumentViewFactory : IDocumentViewFactory {
public ISpreadsheetView Create(IDocumentController controller) {
return new SpreadsheetView(controller);
}
}
然后,在您的容器中注册该工厂,更改 SpreadsheetView 的构造函数,删除 SetContainer 方法,并将 SpreadsheetController 的构造函数更改为接受 IDocumentViewFactory。然后,不要 直接在控制器构造函数中创建视图,因为见上文 - 这可能会创建许多 UI 控件。相反,使用 Lazy 模式并仅在需要时实例化 SpreadsheetView(使用工厂)。
关于你的第二个问题 - 是的,你在你的 GetDocumentController
中使用你的容器作为服务定位器。如果您想避免这种情况,请使用 Multi Injection 并将 IDocumentController
数组注入主视图的构造函数。
我正在从事一个具有类似架构的项目。
我猜你的主要问题是视图的事件处理程序直接调用控制器。例如:
private void ribbonButtonTest_Click(object sender, EventArgs e)
{
controller.OpenNewSpreadsheet();
}
尽量避免这种情况。让您的控制器对象成为您应用程序的主人。让视图和模型为 "blind and deaf".
当您的视图遇到用户操作时,只需引发另一个事件。让控制器负责注册到这个事件并处理它。你的观点将是这样的:
public event EventHandler<EventArgs> RibbonButtonTestClicked ;
protected virtual void ribbonButtonTest_Click(object sender, EventArgs e)
{
var handler = RibbonButtonTestClicked;
if (handler != null) handler(this, EventArgs.Empty);
}
如果这样做,您应该能够删除视图中的所有控制器引用。您的控制器构造函数将如下所示:
[Inject]
public ApplicationShellController(IApplicationShellView view)
{
this.shellView = view;
this.shellView.RibbonButtonTestClicked += this.RibbonButtonTestClicked;
}
由于您无法再从视图中解析对象树,请将方法 "GetView()" 添加到控制器并更改 Program.Main() 方法:
CompositionRoot.Initialize(new DependencyModule());
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var appCtrl = CompositionRoot.Resolve<ApplicationShellController>()
Application.Run(appCtrl.GetView());