C# 具有嵌套控件的MVVM和DI

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

我已经在WPF中使用MVVM有一段时间了,但我一直是这样做的:

ExampleView.xaml.cs(名称空间:Example.Views)

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>
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>();