我一直在使用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(...)
而不是一直注入到最小元素。我对Localizer
和ExceptionHandler
有类似的看法,因为这些会被更多地使用。
我最后一个问题是以下问题。假设我有一个带有多个参数的类,其中一个参数是Localizer
(因为这几乎可以在任何类中使用)。每次都必须添加ILocalizer localizer
var myClass = new MyClass(..., ILocalizer localizer);
感觉非常烦人。这将把我推向一个静态的Localizer,我初始化一次并且永远不再关心它了。如何解决这个问题?
答案 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,还提供INavigationAware
与NavigateTo
和NavigateFrom
的界面方法,当导航到新视图时调用(其中一个方法参数可用于向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进行此操作,则需要一种导航服务。