C# 具有嵌套控件的MVVM和DI
我已经在WPF中使用MVVM有一段时间了,但我一直是这样做的: ExampleView.xaml.cs(名称空间:Example.Views) ExampleView.xaml除了绑定到属性外,没有与C# 具有嵌套控件的MVVM和DI,c#,wpf,xaml,mvvm,C#,Wpf,Xaml,Mvvm,我已经在WPF中使用MVVM有一段时间了,但我一直是这样做的: ExampleView.xaml.cs(名称空间:Example.Views) ExampleView.xaml除了绑定到属性外,没有与ExampleViewModel相关的代码 ExampleViewModel.cs(名称空间:Example.ViewModels) 下面是一个简化的MainWindowView.xaml <Window ... xmlns:views="clr-namespace:Examp
ExampleViewModel
相关的代码
ExampleViewModel.cs(名称空间:Example.ViewModels)
下面是一个简化的MainWindowView.xaml
<Window ...
xmlns:views="clr-namespace:Example.Views">
<Grid>
<views:ExampleView />
</Grid>
</Window>
<Window ...
xmlns:views="clr-namespace:Example.Views">
<Grid>
<views:ExampleView DataContext="{Binding ExampleViewModel}" />
</Grid>
</Window>
ExampleView.xaml除了绑定到属性外,没有与ExampleViewModel
相关的代码
ExampleViewModel.cs(名称空间:Example.ViewModels)
下面是一个简化的MainWindowView.xaml
<Window ...
xmlns:views="clr-namespace:Example.Views">
<Grid>
<views:ExampleView />
</Grid>
</Window>
<Window ...
xmlns:views="clr-namespace:Example.Views">
<Grid>
<views:ExampleView DataContext="{Binding ExampleViewModel}" />
</Grid>
</Window>
最后,App.xaml不再包含StartupUri=“…”
。它现在在App.xaml.cs中完成。这里也是初始化“UnityContainer”的地方
protected override void OnStartup(StartupEventArgs e)
{
// Base startup.
base.OnStartup(e);
// Initialize the container.
var container = new UnityContainer();
// Register types and instances with the container.
container.RegisterType<ILocalizer, Localizer>();
container.RegisterType<IExceptionHandler, ExceptionHandler>();
// For some reason I need to initialize this myself. See further in post what the constructor is of the Localizer and ExceptionHandler classes.
container.RegisterInstance<ILocalizer>(new Localizer());
container.RegisterInstance<IExceptionHandler>(new ExceptionHandler());
container.RegisterType<MainWindowViewModel>();
// Initialize the main window.
var mainWindowView = new MainWindowView { DataContext = container.Resolve<MainWindowViewModel>() };
// This is a self made alternative to the default MessageBox. This is a static class with a private constructor like the default MessageBox.
MyMessageBox.Initialize(mainWindowView, container.Resolve<ILocalizer>());
// Show the main window.
mainWindowView.Show();
}
什么都改变不了
public Localizer(ResourceDictionary appResDic = null, string projectName = null, string languagesDirectoryName = "Languages", string fileBaseName = "Language", string fallbackLanguage = "en")
{
_appResDic = appResDic ?? Application.Current.Resources;
_projectName = !string.IsNullOrEmpty(projectName) ? projectName : Application.Current.ToString().Split('.')[0];
_languagesDirectoryName = languagesDirectoryName.ThrowArgNullExIfNullOrEmpty("languagesFolder", "0X000000066::The languages directory name can't be null or an empty string.");
_fileBaseName = fileBaseName.ThrowArgNullExIfNullOrEmpty("fileBaseName", "0X000000067::The base name of the language files can't be null or an empty string.");
_fallbackLanguage = fallbackLanguage.ThrowArgNullExIfNullOrEmpty("fallbackLanguage", "0X000000068::The fallback language can't be null or an empty string.");
CurrentLanguage = _fallbackLanguage;
}
public ExceptionHandler(string logLocation = null, ILocalizer localizer = null)
{
// Check if the log location is not null or an empty string.
LogLocation = string.IsNullOrEmpty(logLocation) ? Path.Combine(Directory.GetCurrentDirectory(), "ErrorLogs", DateTime.Now.ToString("dd-MM-yyyy") + ".log") : logLocation;
_localizer = localizer;
}
我现在的大问题是,我是否正确地进行依赖注入,是否有几个静态类我初始化一次都不好。我读过的几个主题指出,静态类是一种不好的做法,因为它的可测试性差,代码紧密耦合,但是现在依赖注入的权衡比使用静态类更大
但是,正确地进行依赖项注入将是获得不那么紧密耦合的代码的第一步。我喜欢静态MyMessageBox
的方法,我可以初始化一次,并且它在应用程序中全局可用。我想这主要是为了“方便使用”,因为我可以简单地调用MyMessageBox.Show(…)
,而不是将其注入到最小的元素中。我对定位器
和异常处理程序
有类似的看法,因为它们将被更多地使用
我最后关心的是以下几点。假设我有一个具有多个参数的类,其中一个参数是定位器
(因为这将在几乎任何类中使用)。每次都必须添加ILocalizer定位器
var myClass = new MyClass(..., ILocalizer localizer);
感觉很烦人。这会把我推向一个静态定位器,我初始化过一次,再也不用关心它了。如何解决这个问题?如果您有一组在许多类中使用的“服务”,那么您可以创建一个facade类来封装所需的服务,并将facade注入到您的类中
_appResDic = appResDic ?? Application.Current.Resources;
_projectName = !string.IsNullOrEmpty(projectName) ? projectName : Application.Current.ToString().Split('.')[0];
_languagesDirectoryName = languagesDirectoryName.ThrowArgNullExIfNullOrEmpty("languagesFolder", "0X000000066::The languages directory name can't be null or an empty string.");
_fileBaseName = fileBaseName.ThrowArgNullExIfNullOrEmpty("fileBaseName", "0X000000067::The base name of the language files can't be null or an empty string.");
_fallbackLanguage = fallbackLanguage.ThrowArgNullExIfNullOrEmpty("fallbackLanguage", "0X000000068::The fallback language can't be null or an empty string.");
CurrentLanguage = _fallbackLanguage;
这样做的好处是,您可以轻松地将其他服务添加到该外观中,并且它们可以在所有其他注入类中使用,而无需修改构造函数参数
public class CoreServicesFacade : ICoreServicesFacade
{
private readonly ILocalizer localizer;
private readonly IExceptionHandler excaptionHandler;
private readonly ILogger logger;
public ILocalizer Localizer { get { return localizer; } }
public IExceptionHandler ExcaptionHandler{ get { return exceptionHandler; } }
public ILogger Logger { get { return logger; } }
public CoreServices(ILocalizer localizer, IExceptionHandler exceptionHandler, ILogger logger)
{
if(localizer==null)
throw new ArgumentNullException("localizer");
if(exceptionHandler==null)
throw new ArgumentNullException("exceptionHandler");
if(logger==null)
throw new ArgumentNullException(logger);
this.localizer = localizer;
this.exceptionHandler = exceptionHandler;
this.logger = logger;
}
}
然后您可以将其传递给您的类:
var myClass = new MyClass(..., ICoreServicesFacade coreServices);
(在使用依赖项注入时,无论如何都不应该这样做,除了工厂和模型之外,不应该使用new
关键字)
至于ILocalizer和IEExceptionHandler实现。。。如果ExceptionHandler需要定位器,而定位器需要字符串参数,则有两个选项,具体取决于文件名是需要在运行时稍后确定,还是在应用程序初始化期间仅确定一次
重要
如果要使用依赖项注入,请不要使用可选的构造函数参数。对于DI,构造函数参数应声明构造函数中的依赖项,并且构造函数依赖项始终被视为必需的(不要在构造函数中使用ILocalizer-localizer=null
)
如果您只在应用程序初始化期间创建日志文件,那么就很容易了
var logFilePath = Path.Combine(Directory.GetCurrentDirectory(), "ErrorLogs", DateTime.Now.ToString("dd-MM-yyyy") + ".log");
var localizer = new Localizer(...);
var exceptionHandler = new ExceptionHandler(logFilePath, localizer);
container.RegisterInstance<ILocalizer>(localizer);
container.RegisterInstance<IExceptionHandler>(exceptionHandler);
使用上面的facade示例:
public class CoreServicesFacade : ICoreServicesFacade
{
private readonly ILocalizer localizer;
public ILocalizer Localizer { get { return localizer; } }
public CoreServices(ILocalizerFactory localizerFactory, ...)
{
if(localizer==null)
throw new ArgumentNullException("localizerFactory");
this.localizer = localizerFactory.Create( Application.Current.Resources, Application.Current.ToString().Split('.')[0]);
}
}
注意事项和提示
ExampleViewModel ExampleViewModel { get; set; }
private readonly ILocalizer _localizer;
private readonly IExceptionHandler _exHandler;
public MainWindowViewModel(ILocalizer localizer, IExceptionHandler exHandler)
{
_localizer = localizer;
_exHandler = exHandler;
ExampleViewModel = new ExampleViewModel(localizer);
}
将默认配置移到类本身之外
不要在Localizer/ExceptionHandler类中使用此类代码
_appResDic = appResDic ?? Application.Current.Resources;
_projectName = !string.IsNullOrEmpty(projectName) ? projectName : Application.Current.ToString().Split('.')[0];
_languagesDirectoryName = languagesDirectoryName.ThrowArgNullExIfNullOrEmpty("languagesFolder", "0X000000066::The languages directory name can't be null or an empty string.");
_fileBaseName = fileBaseName.ThrowArgNullExIfNullOrEmpty("fileBaseName", "0X000000067::The base name of the language files can't be null or an empty string.");
_fallbackLanguage = fallbackLanguage.ThrowArgNullExIfNullOrEmpty("fallbackLanguage", "0X000000068::The fallback language can't be null or an empty string.");
CurrentLanguage = _fallbackLanguage;
这几乎使其不稳定,并将配置逻辑置于错误的位置。您应该只接受和验证传递到构造函数中的参数,并在a)工厂的create方法或b)引导程序中确定值和回退(如果不需要运行时参数)
不要在界面中使用视图相关类型
ExampleViewModel ExampleViewModel { get; set; }
private readonly ILocalizer _localizer;
private readonly IExceptionHandler _exHandler;
public MainWindowViewModel(ILocalizer localizer, IExceptionHandler exHandler)
{
_localizer = localizer;
_exHandler = exHandler;
ExampleViewModel = new ExampleViewModel(localizer);
}
不要在公共界面中使用ResourceDictionary
,这会将视图知识泄漏到ViewModels中,并要求您引用包含视图/应用程序相关代码的程序集(我知道我在上面使用了它,基于您的定位器构造函数)
如果需要,请将其作为构造函数参数传递,并在应用程序/视图程序集中实现该类,同时在ViewModel程序集中具有接口)。构造函数是实现细节,可以隐藏(通过在不同的程序集中实现该类,从而允许引用所讨论的类)
静态类是邪恶的
正如您已经意识到的,静态类是不好的。给他们注射是最好的方法。您的应用程序很可能也需要导航。因此,您可以将导航(导航到某个视图)、消息框(显示信息)和打开新窗口(也是一种导航)放入一个服务或导航外观(类似于上述内容),并将与导航相关的所有服务作为单个依赖项传递到您的对象中
将参数传递给ViewModel
传递参数在“home brew”框架中可能有点麻烦,您不应该通过ViewModel构造函数传递参数(阻止DI解析它或强迫您使用工厂)。取而代之的是考虑编写导航服务(或者使用退出框架)。Prims已经很好地解决了这个问题,您得到了一个导航服务(它将对某个视图和它的ViewModel进行导航,还提供了INavigationAware
接口,其中包含NavigateTo
和NavigateFrom
方法
public class CoreServicesFacade : ICoreServicesFacade
{
private readonly ILocalizer localizer;
public ILocalizer Localizer { get { return localizer; } }
public CoreServices(ILocalizerFactory localizerFactory, ...)
{
if(localizer==null)
throw new ArgumentNullException("localizerFactory");
this.localizer = localizerFactory.Create( Application.Current.Resources, Application.Current.ToString().Split('.')[0]);
}
}
_appResDic = appResDic ?? Application.Current.Resources;
_projectName = !string.IsNullOrEmpty(projectName) ? projectName : Application.Current.ToString().Split('.')[0];
_languagesDirectoryName = languagesDirectoryName.ThrowArgNullExIfNullOrEmpty("languagesFolder", "0X000000066::The languages directory name can't be null or an empty string.");
_fileBaseName = fileBaseName.ThrowArgNullExIfNullOrEmpty("fileBaseName", "0X000000067::The base name of the language files can't be null or an empty string.");
_fallbackLanguage = fallbackLanguage.ThrowArgNullExIfNullOrEmpty("fallbackLanguage", "0X000000068::The fallback language can't be null or an empty string.");
CurrentLanguage = _fallbackLanguage;
public class ExampleViewModel : ViewModelBase
{
public ExampleViewModel(Example2ViewModel example2ViewModel)
{
}
}
public class Example2ViewModel : ViewModelBase
{
public Example2ViewModel(ICustomerRepository customerRepository)
{
}
}
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel(ExampleViewModel example2ViewModel)
{
}
}
// Unity Bootstrapper Configuration
container.RegisterType<ICustomerRepository, SqlCustomerRepository>();
// You don't need to register Example2ViewModel and ExampleViewModel unless
// you want change their container lifetime manager or use InjectionFactory
MainWindowViewModel mainWindowViewModel = container.Resolve<MainWindowViewModel>();