繁体   English   中英

带有嵌套控件的MVVM和DI

[英]MVVM and DI with nested controls

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

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

public partial class ExampleView
{
    public ExampleView()
    {
        InitializeComponent();

        var viewModel = new ExampleViewModel();
        DataContext = viewModel;
    }
}

除了绑定属性外, ExampleView.xaml没有与ExampleViewModel有关的代码。

ExampleViewModel.cs (命名空间:Example.ViewModels)

public ExampleViewModel()
{
    // No important code in here concerning this topic. Code here is only used in this class.
}

以下是简化的MainWindowView.xaml

<Window ...
        xmlns:views="clr-namespace:Example.Views">
    <Grid>
        <views:ExampleView />
    </Grid>
</Window>

MainWindowView.xaml.cs与ExampleView.xaml.cs类似。 MainWindowViewModel.cs没有与此主题相关的重要代码。

最后, App.xaml包含StartupUri="Views/MainWindowView.xaml"

如果这不是一个好的模式,我就能使我的应用程序正常工作。 由于该应用程序不再由我一个人维护,因此现在有2-3个人正在处理该应用程序,从而造成了一些问题。 一个人在进行大部分编码(基本上是ViewModels),一个人在进行GUI(视图),一个人在进行“框架”编码。 (使用“”是因为这并不是一个真正的框架,但是我想不出一个更好的词。)

现在,我是从事框架编码的人,并且我一直在阅读有关依赖注入的几个主题,下面的代码是我从Windows使用UnityContainer

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

public partial class ExampleView
{
    public ExampleView()
    {
        InitializeComponent();
    }
}

除了绑定属性外, ExampleView.xaml没有与ExampleViewModel有关的代码。

ExampleViewModel.cs (命名空间:Example.ViewModels)

public string MyText { get; set; }

public ExampleViewModel(ILocalizer localizer)
{
    MyText = localizer.GetString("Title");
}

以下是简化的MainWindowView.xaml

<Window ...
        xmlns:views="clr-namespace:Example.Views">
    <Grid>
        <views:ExampleView DataContext="{Binding ExampleViewModel}" />
    </Grid>
</Window>

MainWindowView.xaml.cs与ExampleView.xaml.cs类似。

MainWindowViewModel.cs

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

最后, 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();
}

由于某种原因,我需要自己初始化Localizer和ExceptionHandler类。 可以在下面找到Localizer和ExceptionHandler构造函数。 两者都具有所有参数都具有默认值的构造函数。 添加没有参数的构造函数,例如

public ExceptionHandler() : this(Path.Combine(Directory.GetCurrentDirectory(), "ErrorLogs", DateTime.Now.ToString("dd-MM-yyyy") + ".log")) { }

不会改变任何事情。

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(...)而不是一直将其注入最小的元素。 我对LocalizerExceptionHandler有类似的看法,因为它们将被更多使用。

我最后关心的是以下几点。 可以说我有一个带有多个参数的类,其中一个参数是Localizer (因为它将在几乎所有类中使用)。 每次都必须添加ILocalizer localizer

var myClass = new MyClass(..., ILocalizer localizer);

感觉很烦。 这将把我推向一个静态的本地化器,我初始化一次并且不再关心它。 如何解决这个问题?

如果您有一堆在许多类中使用的“服务”,则可以创建一个外观类,该类封装所需的服务,并将外观注入到您的类中。

这样做的好处是,您可以轻松地向该外观添加其他服务,并且它们可以在所有其他注入的类中使用,而无需修改构造函数参数。

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和IExceptionHandler实现...如果ExceptionHandler需要Localizer而localizer需要string参数,则有两个选项,具体取决于是否需要在运行时稍后确定文件名,或者在运行过程中仅确定一次文件名。应用程序初始化。

重要

如果要使用依赖注入,请不要使用可选的构造函数参数。 对于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);

基本上,在引导程序中,您实例化并配置Localizer和ExceptionHandler,然后将其注册为容器的实例。

如果出于某种原因,您需要稍后(在Bootstrapper配置和初始化之后)确定日志文件名或语言的名称,则需要使用其他方法:您需要一个工厂类。

工厂将注入到您的类中,而不是ILocalizer / IExceptionHandler的实例中,并在知道参数时创建它的实例。

public interface ILocalizerFactory 
{
    ILocalizer Create(ResourceDictionary appResDic, string projectName);
}

public class ILocalizerFactory
{
    public ILocalizer Create(ResourceDictionary appResDic, string projectName)
    {
        var localizer = new Localizer(appResDic, projectName, "Languages",  "Language", "en");
        return localizer;
    }
}

使用上面的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]);
    }
}

注意事项和提示

将默认配置移到类本身之外

不要在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)引导程序内(如果不需要运行时参数)。

不要在界面内使用与视图相关的类型

不要在公共接口中使用ResourceDictionary ,这会将View知识泄漏到ViewModels中,并要求您对包含与View / Application相关的代码的程序集进行引用(我知道我在上面使用了它,基于您的Locator构造函数)。

如果需要,请将其作为构造函数参数传递,并在Application / View程序集中实现该类,同时将Interface放在ViewModel程序集中。 构造函数是实现细节,可以隐藏(通过在允许引用所涉及类的不同程序集中实现该类)。

静态类是邪恶的

您已经意识到,静态类是不好的。 注入它们是必经之路。 您的应用程序很可能也需要导航。 因此,您可以将“导航”(导航到某个视图),“消息框”(显示信息)和打开新的Windows(也是一种导航)置于一项服务或导航外观中(类似于上述一种),并传递所有与之相关的服务可以作为单个依赖项导航到您的对象。

将参数传递给ViewModel

在“自制”框架中传递参数可能会有些麻烦,并且您不应该通过ViewModel构造函数传递参数(防止DI解决它或强迫您使用工厂)。 而是考虑编写导航服务(或使用现有框架)。 Prims很好地解决了该问题,您获得了一个导航服务(它将导航到某个View及其ViewModel,还提供了带有NavigateToNavigateFrom方法的INavigationAware接口,当一个方法导航到一个新视图时会调用该接口(其中一个方法参数可用于向ViewModel提供参数)以及从ViewModel导航时(例如,确定从视图导航是否可行或在必要时取消导航),例如:要求用户在保存或丢弃数据之前导航到另一个ViewModel)。

但这有点题外话了。

例:

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 mainWindowViewModel = container.Resolve<MainWindowViewModel>();

然后Unity将解决所有其他依赖项(它将ICustomerRepository注入Example2ViewModel ,然后将Example2ViewModel注入ExampleViewModel ,最后将ExampleViewModel注入您的MainWindowViewModel并返回其实例。

要注意的是:您不能在ViewModels中使用container (尽管在用例中可以在View的后台代码中使用它。但是,最好在XAML中使用导航服务或ViewModel Locator(请参见Prism了解如何使用它们)做到了)) 。

因此,如果需要从ViewModels中进行导航,则需要一种导航服务。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM