具有嵌套控件的MVVM和DI

时间:2015-07-18 11:58:29

标签: c# wpf xaml mvvm

我一直在使用MVVM for WPF很长一段时间但是我一直这样做:

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);
感觉非常烦人。这将把我推向一个静态的Localizer,我初始化一次并且永远不再关心它了。如何解决这个问题?

1 个答案:

答案 0 :(得分:2)

如果您有一堆&#34;服务&#34;在许多类中使用,您可以创建一个外观类,它封装了所需的服务并将外观注入到类中。

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

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并且本地化程序需要字符串参数,则有两个选项,具体取决于是否需要在运行时稍后确定文件名或在Application初始化期间只执行一次。

重要

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

使用外观上面的示例:

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

警告&amp;提示

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

不要在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)在bootstrapper内部(如果不需要运行时参数)后退。

请勿在界面中使用与视图相关的类型

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

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

静态类是邪恶的

正如您已经意识到的那样,静态类很糟糕。注入它们是要走的路。您的应用程序很可能也需要导航。因此,您可以将导航(导航到某个视图),MessageBoxes(显示信息)和新Windows(也是一种导航)打开到一个服务或导航外观(类似于上面的一个)并传递所有相关的服务将导航作为单个依赖项导入对象。

将参数传递给ViewModel

传递参数可能会让家庭酿造过程变得有点痛苦。框架,你不应该通过ViewModel构造函数传递参数(防止DI解析它或强迫你使用工厂)。而是考虑编写导航服务(或使用现有框架)。 Prims有很好的解决方案,你有一个导航服务(它将导航到某个View及其ViewModel,还提供INavigationAwareNavigateToNavigateFrom的界面方法,当导航到新视图时调用(其中一个方法参数可用于向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 {1}}并返回它的实例。

问题是:您无法在ViewModel中使用container(尽管在您的使用案例中使用View中的代码隐藏是可以的。但是它更好在XAML中使用导航服务或ViewModel定位器(参见Prism他们是如何做到的))。

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