第七章-Pure DI 和 应用组成
Pure DI
在第一章中,我们简要介绍了DI的三个方面:对象组合(Object Composition),生命周期管理(Lifetime Management)和拦截(Interception)。在本书的这一部分,我们将深入探讨这些维度,为每个维度提供各自的章节。许多DI容器(DI Container)具有与这些维度直接相关的特性。一些提供了所有三维的特性,而另一些只支持其中的一部分。
因为DI容器(DI Container)是一个可选工具,所以我们认为解释容器通常用于实现这些特性的基本原理和技术更为重要。鉴于此,第3部分将研究如何在根本不使用DI容器(DI Container)的情况下应用DI。一个实用的自己动手指南,这就是我们所说的Pure DI。
第7章介绍了如何在各种框架(例如ASP.NET Core MVC,控制台应用程序等)中组成对象。 并非所有框架都同样出色地支持DI,即使在那些框架中,细节也相差很大。 对于每个框架,可能很难确定启用DI的缝隙(Seam)。 但是,一旦找到了缝隙(Seam),您便拥有了使用该特定框架的所有应用程序的解决方案。 在第7章中,我们已经针对最常见的.NET应用程序框架完成了这项工作。 可以将其视为框架缝隙(Seam)的目录。
尽管使用Pure DI组合对象并不特别困难,但是在阅读了第8章中关于生命周期管理的内容之后,您应该开始了解真正DI容器(DI Container)的好处。可以在对象图中正确地管理各种对象的生存期,但是它需要更多的自定义代码而不是对象组合。所有这些代码都不会为应用程序增加任何特定的业务价值。除了解释生命周期管理的基础知识外,第8章还包含了一个常见生活方式的目录。这个目录是第四部分讨论生活方式的词汇表。尽管您不必手动实现这些功能,但了解它们的工作原理是很好的。
第3部分的其余各章介绍了DI的最后一个方面:拦截(Interception)。在第9章中,我们将探讨经常发生的以基于组件的方式实现横切关注点(Cross-Cutting Concerns)的问题。 我们将使用装饰器设计模式来完成此操作。 第9章还作为其后两章的简介。
我们将在第10章中研究面向方面的编程(Aspect-Oriented Programming)(AOP)范例,并了解如何基于SOLID原理进行仔细的应用程序设计,使您无需使用任何特殊工具即可创建高度可维护的代码。 我们认为本章是本书的高潮–在这里,许多使用抢先体验程序的读者说,他们开始看到一种非常强大的软件建模方法的轮廓。
除了应用SOLID设计原则之外,还有其他方式可以进行面向方面的编程。 除了使用模式和原理,您还可以使用专用工具,例如编译时编织和动态拦截工具。这些将在第11章中介绍。
应用组成(Application composition)
分几道菜烹饪一顿美食是一项艰巨的任务,特别是如果您想参与消费的话。 您不能同时吃饭和煮饭,但是许多菜肴都需要最后一刻才能煮熟。 专业厨师知道如何解决许多挑战。 在交易的许多技巧中,它们使用就地交易的一般原则,可以将其大致地翻译成就地的一切。可以事先准备好的一切,都应事先准备好。 蔬菜被清洗和切碎,切肉,煮熟的原料,烤箱预热,工具摆放等等。
如果冰淇淋是甜点的一部分,则可以在前一天制作。 如果第一道菜含有贻贝,可以在数小时前将它们清洗干净。 甚至可以在一个小时之前准备好酱汁贝纳斯酱这样的易碎成分。 当客人准备好就餐时,只需要做最后的准备:在煎炸肉时重新加热酱汁,依此类推。 在许多情况下,这顿饭的最终组成不必花费超过5至10分钟的时间。 图7.1说明了该过程。
| 图7.1 Mise就位涉及提前准备餐点的所有成分,以便可以尽可能快,轻松地完成餐点的最终组成。 |
|---|
![]() |
替换的原理类似于使用DI开发松散耦合(Loose Coupling)应用程序。 您可以提前编写所有必需的组件,只有在绝对必要时才可以编写它们。
与所有类推一样,到目前为止,我们只能采用这一类比。 在烹饪过程中,准备工作和成分会随着时间而分离,而在应用程序开发中,组件和层之间会发生分离。 图7.2显示了如何在组合根(Composition Root)中组成组件。
在运行时,发生的第一件事是对象组合(Object Composition)。一旦连接了对象图,就完成了对象组合(Object Composition),并且组成组件将接管工作。 在本章中,我们将重点介绍几个应用程序框架的组合根(Composition Root)。与现场部署相反,对象组合不会尽早发生,而是在需要集成不同模块的地方进行。
| 图7.2 组合根(Composition Root)构成了应用程序的所有独立模块。 |
|---|
![]() |
定义 对象组合(Object Composition)是建立相关组件层次结构的行为。该合成发生在组合根(Composition Root)内部。
对象组合(Object Composition)是DI的基础,也是最容易理解的部分之一。您已经知道该怎么做,因为在创建包含其他对象的对象时始终会组合对象。
在4.1节中,我们介绍了何时以及如何编写应用程序的基础知识。本章不再重复这些信息。相反,我们希望帮助您解决在编写对象时可能出现的一些挑战。 这些挑战不是源于对象组合(Object Composition)本身,而是源于您工作所在的应用程序框架。这些问题往往是针对每个框架的,决议也是如此。 根据我们的经验,这些挑战是成功应用DI的最大障碍,因此我们将重点关注这些挑战。 这样做会使本章比前几章理论性更强,更实用。
注意 如果您只想阅读有关在选择的框架中应用DI的知识,则可以跳到本章的该部分。 每个部分都打算独立存在。
当您完全控制应用程序的生存期时(就像使用命令行应用程序一样),可以轻松组成应用程序的整个依赖关系层次结构。但是.NET中的某些框架(例如ASP.NET Core)涉及控制反转,这有时会使应用DI更加困难。了解每个框架的接缝是将DI用于该特定框架的关键。在本章中,我们将研究如何在最常见的.NET Core框架中实现组合根(Composition Root)。
我们将在每个部分的开头对在特定框架中应用DI进行一般性介绍,然后在本书的大部分内容中以基于电子商务示例的广泛示例为基础。我们将从最简单的应用DI的框架开始,然后逐步研究更复杂的框架。到目前为止,将DI应用于最简单的类型是控制台应用程序,因此我们将在下面讨论。
注意 一些旧的.NET框架(例如PowerShell和ASP.NET Web Forms的较早版本)是彻头彻尾的敌意环境,可在其中应用DI。 另一方面,更新的.NET Core框架对DI更友好。 在本书中,我们主要关注那些较新的.NET Core框架。 如果您有兴趣了解如何将DI应用于ASP.NET MVC,Web Froms,WCF,WPF或PowerShell,请阅读本书第一版的数字副本; 购买此版本时随附。 第7章详细讨论了每一个。
编写控制台应用程序(Composing console applications)
毫无疑问,控制台应用程序是最容易组成的应用程序类型。与大多数其他.NET BCL应用程序框架相反,控制台应用程序实际上不涉及控制反转。当执行到达应用程序的切入点(通常是Program类中的Main方法)时,您就自己一个人了。没有要订阅的特殊事件,没有要实现的接口,可以使用的服务很少。
程序类是合适的组合根(Composition Root)。 在其Main方法中,您可以组成应用程序的模块,然后让它们接管。 没什么,但让我们看一个例子。
示例:使用UpdateCurrency程序更新货币
在第4章中,我们研究了如何为示例电子商务应用程序提供货币换算功能。第4.2.4节介绍了ICurrencyConverter抽象,该抽象将汇率从一种货币转换为另一种货币。因为ICurrencyConverter是一个接口,所以我们可以创建许多不同的实现,但是在示例中,我们使用了一个数据库。 第4章中示例代码的目的是演示如何检索和实现货币换算,因此我们从没有研究如何更新数据库中的汇率。
为了继续该示例,让我们研究如何编写一个简单的.NET Core控制台应用程序,使管理员或超级用户无需直接与数据库进行交互即可更新汇率。控制台应用程序与数据库对话并处理传入的命令行参数。由于该程序的目的是更新数据库中的汇率,因此我们将其称为UpdateCurrency。它带有两个命令行参数:
- 货币代码
- 从主要货币(USD)到该货币的汇率
美元是我们系统中的主要货币,我们存储所有其他相对于它的货币汇率。例如,美元对欧元的汇率表示为1美元,费用为0.88欧元(2018年12月)。当我们想在命令行上更新汇率时,它看起来像这样:
d:\> dotnet commerce\UpdateCurrency.dll EUR "0.88"
Updated: 0.88 EUR = 1 USD.
注 在.NET Core中,控制台应用程序是.dll(不是.exe),可以通过运行以DLL名称作为第一个参数的dotnet命令来启动。
执行程序将更新数据库,并将新值写回到控制台。让我们看看我们如何构建这样的控制台应用程序。
建立UpdateCurrency程序的组合根(Composition Root)
UpdateCurrency使用控制台程序的默认入口点:Program类中的Main方法。这充当应用程序的组合根(Composition Root)。
清单7.1 控制台应用程序的组合根(Composition Root)
class Program
{
static void Main(string[] args)
{
string connectionString = LoadConnectionString(); <---加载配置值
CurrencyParser parser = CreateCurrencyParser(connectionString); <--建立对象图
ICommand command = parser.Parse(args);
command.Execute(); <--调用所需的功能
}
static string LoadConnectionString()
{
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.Build();
return configuration.GetConnectionString("CommerceConnectionString");
}
static CurrencyParser CreateCurrencyParser(string connectionString) ...
}
Program类的唯一职责是加载配置值,编写所有相关模块并让组合的对象图处理功能。在此示例中,应用程序模块的组成被提取到CreateCurrencyParser方法中,而Main方法则负责在组合对象图上调用方法。CreateCurrencyParser使用硬连接的依赖组合其对象图。 我们将很快返回到它,以检查它是如何实现的。
任何组合根(Composition Root)都只应该做四件事:加载配置值,构建对象图,调用所需的功能,以及如我们将在下一章中讨论的那样,释放对象图。 一旦做到这一点,它就应该摆脱它,将其余部分留给调用的实例。
注 如我们在第4.1.3节中所述,您应该将配置值的加载与执行对象组合的方法分开,如清单7.1所示。这使对象组成与使用中的配置系统脱钩,从而可以在不存在(有效)配置文件的情况下进行测试。
有了此基础结构之后,您现在可以要求CreateCurrencyParser创建一个CurrencyParser来解析传入的参数并最终执行相应的命令。这个示例使用的是Pure DI,但可以像第4部分中所述的那样用DI容器(DI Container)直接替换它。
在CreateCurrencyParser中组成对象图(Composing object graphs in CreateCurrencyParser)
存在CreateCurrencyParser方法的明确目的是为UpdateCurrency程序连接所有依赖项。以下清单显示了实现。
清单7.2 组成对象图的
CreateCurrencyParser方法
static CurrencyParser CreateCurrencyParser(string connectionString)
{
IExchangeRateProvider provider = new SqlExchangeRateProvider(
new CommerceContext(connectionString));
return new CurrencyParser(provider);
}
在此清单中,对象图相当浅。CurrencyParser类需要IExchangeRateProvider接口的实例,并且您可以在CreateCurrencyParser方法中构造SqlExchangeRateProvider以便与数据库进行通信。
CurrencyParser类使用构造函数注入(Constructor Injection),因此您将刚刚创建的SqlExchangeRateProvider实例传递给它。然后,从该方法返回新创建的CurrencyParser。如果您想知道,这里是CurrencyParser的构造函数签名:
public CurrencyParser(IExchangeRateProvider exchangeRateProvider)
回想一下,IExchangeRateProvider是由SqlExchangeRateProvider实现的接口。作为组合根(Composition Root)的一部分,CreateCurrencyParser包含从IExchangeRateProvider到SqlExchangeRateProvider的硬编码映射。但是,其余代码仍保持松散耦合(Loose Coupling),因为它仅消耗抽象。
这个示例看似简单,但是它由三个不同的应用程序层组成类型。在本示例中,让我们简要检查一下这些层如何相互作用。
仔细查看UpdateCurrency的分层 (A closer look at UpdateCurrency's layering)
组合根(Composition Root)是所有层中的组件连接在一起的地方。入口点和组合根(Composition Root)构成可执行文件的唯一代码。如图7.3所示,所有实现都委托给了较低的层。
图7.3 中的图可能看起来很复杂,但它几乎代表了控制台应用程序的整个代码库。大多数应用程序逻辑包括解析输入参数并根据输入选择正确的命令。所有这些操作都在应用程序服务层中进行,该应用程序服务层仅通过IExchangeRateProvider接口和Currency类直接与领域层进行通信。
IExchangeRateProvider由组合根(Composition Root)注入到CurrencyParser中,随后用作抽象工厂(Abstract Factory)来创建由UpdateCurrencyCommand使用的Currency实例。数据访问层提供了基于SQL Server的领域抽象的实现。尽管没有其他应用程序类直接与这些实现联系,但CreateCurrencyParser将抽象映射到具体类。
图7.3 UpdateCurrency应用程序的组件组成 |
|---|
![]() |
注 您可能从6.2节回忆起,应该对抽象工厂(Abstract Factory)的使用持怀疑态度。但是,在这种情况下,使用抽象工厂(Abstract Factory)很好,因为只有组合根(Composition Root)才使用它。
在控制台应用程序上使用DI很容易,因为实际上不涉及外部控制反转。.NET Framework加速了流程,并将控制权移交给Main方法。 这与使用通用Windows编程(UWP)相似,后者允许对象组成而没有任何缝隙(Seam)。
编写UWP应用程序
编写UWP应用程序几乎与编写控制台应用程序一样容易。在本部分中,我们将实现一个小型UWP应用程序,以使用Model-View-ViewModel(MVVM)模式管理电子商务应用程序的产品。 我们将研究组合根(Composition Root)目录的放置位置,如何构建和初始化视图模型,如何将视图绑定到其相应的视图模型以及如何确保我们可以从一页导航到下一页。
UWP应用程序的切入点并不复杂,尽管它没有提供明确旨在启用DI的缝隙(Seam),但是您可以按照自己喜欢的任何方式轻松地组成应用程序。
什么是UWP应用程序?
Microsoft已通过以下方式定义了
UWP:通用Windows平台(
UWP)是Windows 10的应用程序平台。您可以使用一个API集,一个应用程序包和一个商店为UWP开发应用程序,以访问所有Windows 10设备(PC,平板电脑,手机,Xbox,HoloLens) ,Surface Hub等)。 无论是触摸屏,鼠标和键盘,游戏控制器还是笔,都可以轻松支持多种屏幕尺寸和各种交互模型。 UWP应用程序的核心思想是用户希望他们的体验在所有设备上都是通用的,并且他们希望使用最方便或最有效率的设备来完成手头的任务。
在本节中,我们将不教UWP本身。 假定具有有关构建UWP应用程序的基本知识。
UWP组成(Composing UWP applications)
UWP应用程序的入口点是在其App类中定义的。 与UWP中的大多数其他类一样,该类分为两个文件:App.xaml和App.xaml.cs。 您可以在App.xaml.cs中定义在应用程序启动时发生的情况。
当您在Visual Studio中创建新的UWP项目时,App.xaml.cs文件定义一个OnLaunched方法,该方法定义在应用程序启动时显示哪个页面。 在这种情况下,为MainPage。
清单7.3
App.xaml.cs文件的OnLaunched方法
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
...
rootFrame.Navigate(typeof(MainPage), e.Arguments);
<---除其他外,Visual Studio中的默认UWP项目在启动时通过调用 Frame.Navigate 将用户导航到MainPage。
...
}
OnLaunched方法类似于控制台应用程序的Main方法,它是应用程序的入口点。 App类成为应用程序的组合根(Composition Root)。 您可以使用DI容器(DI Container)或Pure DI来构成页面。 下一个示例使用Pure DI。
示例:连接产品管理富客户端
上一节中的示例创建了用于设置汇率的我们的商务控制台应用程序。 在此示例中,您将创建一个UWP应用程序,使您可以管理产品。 图7.4和7.5显示了此应用程序的屏幕截图。
| 图7.4 产品管理的主页是产品列表。 您可以通过点击一行来编辑或删除产品,也可以通过点击“添加产品”来添加新产品。 |
|---|
![]() |
| 图7.5 产品管理的“产品编辑”页面可让您更改以美元为单位的产品名称和单价。 该应用程序使用UWP的默认命令栏。 |
|---|
![]() |
整个应用程序是使用MVVM方法实现的,并且包含图7.6中所示的四层。 我们将逻辑最多的部分与其他模块隔离开来。 在这种情况下,这就是演示逻辑。 UWP客户端层是一个薄层,除了定义UI和将实现委派给其他模块外,几乎没有什么其他作用。
图7.6中的图与您在上一章中看到的图相似,只是增加了表示逻辑层。数据访问层可以像在电子商务Web应用程序中那样直接连接到数据库,也可以连接到产品管理Web服务。 信息的存储方式与表示逻辑层无关,因此在本章中我们将不对此进行详细介绍。
| 图7.6 产品管理富客户端应用程序的四个截然不同的程序集 |
|---|
![]() |
MVVM
Model-View-ViewModel(MVVM)是一种特别适合UWP的设计模式。 它将UI代码划分为三个不同的职责:
- 该模型是应用程序的基础模型。 这通常是域模型,但并非总是如此。 它通常由普通旧CLR对象(POCO)组成。 请注意,模型通常以与用户界面无关的方式表示; 它不假定它将直接由UI公开,因此它不公开任何特定于UWP的功能。
- 视图是我们要查看的
UI。 在UWP中,您可以在XAML中声明性地表达View,并使用数据绑定和数据模板来呈现数据。 无需使用后台代码就可以表达视图,实际上,它通常是首选,因为它有助于使视图完全专注于UI。ViewModel是视图和模型之间的桥梁。 每个ViewModel都是一个类,以特定于技术的方式转换和公开Model的类。 在UWP中,这意味着它可以将列表公开为System.Collections.ObjectModel.ObservableCollection,将用户操作公开为System.Windows.Input.ICommand,依此类推。
MVVM中ViewModel的角色不同于MVC应用程序中的View Model。对于MVC,视图模型是无行为的数据对象,并且已在您的应用程序代码中更新。 另一方面,MVVMViewModel是具有依赖关系的组件。 在您的UWP应用程序中,ViewModels将使用DI组成。
使用MVVM,您可以将ViewModel分配给页面的DataContext属性,并且在绑定新ViewModel或更改现有ViewModel中的数据时,数据绑定和数据模板引擎会正确显示数据。但是,在创建第一个ViewModel之前,需要定义一些使ViewModels导航到其他ViewModels的构造。同样,要使用向用户显示页面时所需的运行时数据初始化ViewModel,必须使ViewModels实现自定义接口。下一节在介绍应用程序之前,先解决了这些问题:MainViewModel。
将依赖项注入MainViewModel(Injecting Dependencies into the MainViewModel)
MainPage仅包含XAML标记,并且不包含任何自定义代码。相反,它使用数据绑定来显示数据和处理用户命令。要启用此功能,必须将MainViewModel分配给其DataContext属性。但是,这是属性注入(Property Injection)的一种形式。我们想改用构造函数注入(Constructor Injection)。为此,我们使用一个接受MainViewModel作为参数的重载构造函数来删除MainPage的默认构造函数,其中该构造函数在内部分配该DataContext属性:
public sealed partial class MainPage : Page
{
public MainPage(MainViewModel vm)
{
this.InitializeComponent();
this.DataContext = vm;
}
}
MainViewModel公开数据,例如产品列表以及创建,更新或删除产品的命令。 启用此功能取决于提供对产品目录的访问的服务:IProductRepository抽象。除了IProductRepository之外,MainViewModel还需要一个可用于控制其窗口环境的服务,例如导航到其他页面。 此其他依赖项称为INavigationService:
public interface INavigationService
{
void NavigateTo<TViewModel>(Action whenDone = null, object model = null)
where TViewModel : IViewModel;
}
注 C#4引入了可选的方法参数,使您可以省略某些参数的参数。 在这种情况下,C#编译器为调用提供声明的默认值。 在前面的清单中,两个方法参数都是可选的。 清单7.4调用了NavigateTo,有时会省略参数。
NavigateTo方法是通用的,因此必须将其导航到的ViewModel的类型作为其通用类型参数提供。 导航服务将方法参数传递给创建的ViewModel。 为此,ViewModel必须实现IViewModel。 因此,NavigateTo方法指定了通用类型约束,其中TViewModel:IViewModel.以下代码段显示了IViewModel:
public interface IViewModel
{
void Initialize(Action whenDone, object model); <---初始化一个ViewModel
}
Initialize方法包含与INavigationService.NavigateTo方法相同的参数。 导航服务将在构造的ViewModel上调用Initialize。 该模型表示ViewModel需要初始化的数据,例如Product。 当用户退出此ViewModel时,whenDone操作将使原始ViewModel收到通知,我们将在稍后进行讨论。
使用前面的接口定义,现在可以为MainPage构造一个ViewModel。 以下清单充分展示了MainViewModel。
public class MainViewModel : IViewModel,
INotifyPropertyChanged <----为了能够通知视图应该对其进行更新,ViewModel必须实现INotifyPropertyChanged。
{
private readonly INavigationService navigator;
private readonly IProductRepository repository;
public MainViewModel(
INavigationService navigator,
IProductRepository repository)
{
this.navigator = navigator;
this.repository = repository;
this.AddProductCommand = new RelayCommand(this.AddProduct);
this.EditProductCommand = new RelayCommand(this.EditProduct);
}
public IEnumerable<Product> Model { get; set; }
public ICommand AddProductCommand { get; }
public ICommand EditProductCommand { get; } <-3rows ViewModel包含MainPage的XAML绑定到的几个属性。 型号是在网格视图中显示的产品列表; ICommand属性表示按下相应按钮时执行的操作。
public event PropertyChangedEventHandler
PropertyChanged = (s, e) => { };
public void Initialize( <--由IViewModel接口指定Initialize方法,每个ViewModel都必须实现该接口。 对于MainViewModel,您无需使用参数,但可以使用注入的IProductRepository加载所有产品。
object model, Action whenDone)
{
this.Model = this.repository.GetAll();
this.PropertyChanged.Invoke(this, <--通过调用已实现的INotifyPropertyChanged接口的PropertyChanged事件,并向其提供要更改的属性的名称,UWP可以弄清楚应如何重新绘制屏幕。
new PropertyChangedEventArgs("Model"));
}
private void AddProduct()
{
this.navigator.NavigateTo<NewProductViewModel>(
whenDone: this.GoBack);
}
private void EditProduct(object product) <--当用户点击产品表中的一行时,将调用EditProduct方法。 通过调用,UWP将从列表中传递绑定项,该绑定项将是Model集合中的Product。
{
this.navigator.NavigateTo<EditProductViewModel>(
whenDone: this.GoBack,
model: product; <---初始化时,EditProductViewModel加载要编辑的产品。 这要求您将产品ID与调用一起传递给NavigateTo。
}
private void GoBack()
{
this.navigator.NavigateTo<MainViewModel>();
}
}
两种命令方法AddProduct和EditProduct都指示INavigationService导航到相应ViewModel的页面。在AddProduct的情况下,它对应于NewProductViewModel.NavigateTo方法随附有一个委托,当用户完成该页面的工作时,NewProductViewModel将调用该委托。 这将导致调用MainViewModel的GoBack方法,该方法会将应用程序导航回MainViewModel。为了完整描述,清单7.5显示了MainPage XAML定义的简化版本,以及XAML如何绑定到MainViewModel的Model,EditProductCommand和AddProductCommand属性。
清单7.5
MainPage的XAML
<Page x:Class="Ploeh.Samples.ProductManagement.UWPClient.MainPage"
xmlns:commands="using:ProductManagement.PresentationLogic.UICommands"
...>
<Grid>
<Grid.RowDefinitions>
...
</Grid.RowDefinitions>
<GridView ItemsSource="{Binding Model}"
commands:ItemClickCommand.Command="{Binding EditProductCommand}"
IsItemClickEnabled="True">
<GridView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Name}" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding UnitPrice}" />
</StackPanel>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<CommandBar Grid.Row="5" Grid.ColumnSpan="3" Grid.Column="0">
<AppBarToggleButton Icon="Add" Label="Add product"
Command="{Binding AddProductCommand}" />
</CommandBar>
</Grid>
</Page>
注
XAML的GridView使用名为ItemClickCommand的自定义UI命令,允许对GridView行的点击和单击绑定到ViewModel的EditProductCommand。 关于UI命令的讨论不在本书的讨论范围之内,但是自定义命令在本书随附的源代码中可用,您可以在其中查看XAML的完整版本。
尽管以前的XAML使用了较旧的Binding标记扩展,但作为UWP开发人员,您可能习惯于使用较新的x:Bind标记扩展。x:Bind提供了编译时支持,但要求类型在编译时固定,通常在视图的代码隐藏类中定义。由于您绑定到存储在无类型DataContext属性中的ViewModel,因此失去了编译时支持,因此需要使用Binding标记扩展 。
MainPage XAML中的两个主要元素是GridView和CommandBar。GridView用于显示可用产品并绑定到Model和EditProductCommand属性。 其DataTemplate绑定到模型的Product元素的Name和UnitPrice属性。CommandBar显示带有允许用户调用的操作的通用功能区。 CommandBar绑定到AddProductCommand属性。 使用MainViewModel和MainPage的定义,您现在可以开始连接应用程序。
连接MainViewModel(Wiring up MainViewModel)
在连接MainViewModel之前,让我们看一下该依赖图中涉及的所有类。 图7.7显示了从MainPage开始的应用程序图。
现在,您已经确定了应用程序的所有构建块,可以进行组合了。为此,您必须同时创建MainViewModel和MainPage,然后将ViewModel注入MainPage的构造函数。 要连接MainViewModel,您必须将其与依赖组合在一起:
IViewModel vm = new MainViewModel(navigationService, productRepository);
Page view = new MainPage(vm);
如清单7.3所示,默认的Visual Studio模板调用Frame.Navigate(Type)。 Navigate方法代表您创建一个新的Page实例,并将该页面显示给用户。 无法提供Page实例进行导航,但是您可以通过将创建的页面手动分配给应用程序主框架的Content属性来解决此问题:
var frame = (Frame)Window.Current.Content;
frame.Content = view;
因为这些是将应用程序粘合在一起的重要部分,所以这正是您在组合根(Composition Root)中将要做的。
| 图7.7产品管理富客户端的依赖关系图 |
|---|
![]() |
在UWP应用程序中实现组合根(Composition Root)(Implementing the Composition Root in the UWP application)
有很多方法可以创建组合根(Composition Root)。对于此示例,我们选择将导航逻辑和View/ViewModel对的构造都放置在App.xaml.cs文件中,以使该示例相对简洁。 该应用程序的组合根(Composition Root)显示在图7.8中。
注 组合根(Composition Root)的重要组成部分是设计者(Composer)。这是一个统一的术语,指的是组成依赖关系的任何对象或方法,下一章将对此进行更详细的讨论。
| 图7.8产品管理丰富客户的构成根 |
|---|
![]() |
下一个清单显示了我们的组合根(Composition Root)。
清单7.6 包含组合根(Composition Root)的产品管理
App类
public sealed partial class App : Application, INavigationService
{
protected override void OnLaunched(
LaunchActivatedEventArgs e)
{
if (Window.Current.Content == null)
{
Window.Current.Content = new Frame();
Window.Current.Activate();
this.NavigateTo<MainViewModel>(null, null);
}
}
public void NavigateTo<TViewModel>(
Action whenDone, object model)
where TViewModel : IViewModel
{
var page = this.CreatePage(typeof(TViewModel));
var viewModel = (IViewModel)page.DataContext;
viewModel.Initialize(whenDone, model);
var frame = (Frame)Window.Current.Content;
frame.Content = page;
}
private Page CreatePage(Type vmType)
{
var repository = new WcfProductRepository();
if (vmType == typeof(MainViewModel))
{
return new MainPage(
new MainViewModel(this, repository));
}
else if (vmType == typeof(EditProductViewModel))
{
return new EditProductPage(
new EditProductViewModel(repository));
}
else if (vmType == typeof(NewProductViewModel))
{
return new NewProductPage(
new NewProductViewModel(repository));
{
else
{
throw new Exception(“Unknown view model.”);
}
...
}
CreatePage工厂方法类似于我们在4.1节中讨论的组合根(Composition Root)示例。 它由else语句的大量列表组成,以相应地构造正确的对。
注 为简单起见,清单7.6的CreatePage在每次调用时都创建新的Page实例。 这不是严格要求的,但更易于实现。
UWP为组合根(Composition Root)提供了一个简单的位置。 您需要做的就是从OnLaunched删除对Frame.Navigate(Type)的调用,并使用手动创建的Page类设置Frame.Content,该类使用ViewModel及其依赖项组成。
在大多数其他框架中,控制反转的程度更高,这意味着我们需要能够识别正确的可扩展性点以连接所需的对象图。一种这样的框架是ASP.NET Core MVC。
组成ASP.NET Core MVC应用程序
ASP.NET Core MVC的构建和设计旨在支持DI。它带有自己的内部合成引擎,您可以用来构建自己的组件。如您所见,尽管它并没有强制您的应用程序组件使用DI容器(DI Container)。您可以使用Pure DI或任何您喜欢的DI容器(DI Container)。
在本节中,您将学习如何使用ASP.NET Core MVC的主要扩展点,该扩展点使您可以插入逻辑,以将控制器类与它们的依赖项进行组合。 本节从DI对象组合的角度介绍ASP.NET Core MVC。 但是,构建ASP.NET Core应用程序的功能远远超出我们在单个章节中可以讨论的范围。 如果您想了解有关如何使用ASP.NET Core构建应用程序的更多信息,请查看安德鲁·洛克(Andrew Lock)的ASP.NET Core in Action(Manning,2018)。 之后,我们将介绍如何插入需要依赖项的自定义中间件。
注 在ASP.NET“经典”中,Microsoft为MVC和Web API开发了单独的框架。 借助ASP.NET Core,Microsoft在ASP.NET Core MVC的保护下创建了一个统一框架来处理MVC和Web API。 从DI的角度来看,Web API的连接与ASP.NET Core中的MVC应用程序相同。 这意味着本节也适用于在.NET Core中构建Web API。
与在应用程序框架中实践DI一样,应用它的关键是找到正确的可扩展性点。 在ASP.NET Core MVC中,这是一个称为IControllerActivator的接口。 图7.9说明了它如何适合框架。
控制器是ASP.NET Core MVC的核心。 他们处理请求并确定如何响应。 如果需要查询数据库,验证和保存传入数据,调用域逻辑等,则可以从控制器启动此类操作。 控制器本身不应该做这些事情,而应该将工作委托给适当的依赖项。 这就是DI的用武之地。
| 图7.9 ASP.NET Core MVC请求管道 |
|---|
![]() |
您希望能够为给定的控制器类提供依赖项,最好是通过构造函数注入(Constructor Injection)。 使用自定义IControllerActivator可以做到这一点。
创建自定义控制器激活器
创建自定义控制器激活器并不是特别困难。 它要求您实现IControllerActivator接口:
public interface IControllerActivator
{
object Create(ControllerContext context);
void Release(ControllerContext context, object controller);
}
Create方法提供了一个ControllerContext,其中包含诸如HttpContext和控制器类型之类的信息。 通过这种方法,您有机会在返回实例之前连接所有必需的依赖项并将其提供给控制器。 您稍后会看到一个示例。
如果创建了需要显式处理的任何资源,则可以在调用Release方法时执行此操作。 在下一章中,我们将介绍有关发布组件的更多详细信息。 确保摆脱依赖关系的一种更实用的方法是使用HttpContext将它们添加到一次性请求对象的列表中。Response.RegisterForDispose方法。 尽管实现自定义控制器激活器很困难,但是除非我们告知ASP.NET Core MVC,否则不会使用它
在ASP.NET Core中使用自定义控制器激活器
可以将自定义控制器激活器添加为应用程序启动序列的一部分—通常在Startup类中。 通过在IServiceCollection实例上调用AddSingleton <IControllerActivator>来使用它们。 下一个清单显示了示例电子商务应用程序中的Startup类。
清单7.7
Commerce应用程序的Startup类
public class Startup
{
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var controllerActivator = new CommerceControllerActivator(
Configuration.GetConnectionString("CommerceConnectionString"));
services.AddSingleton<IControllerActivator>(controllerActivator);
}
public void Configure(ApplicationBuilder app, IHostingEnvironment env)
{
...
}
}
此清单创建了自定义CommerceControllerActivator的新实例。 通过使用AddSingleton将其添加到已知服务列表中,可以确保您的自定义控制器激活器可以拦截控制器的创建。 如果这段代码看起来有些陌生,那是因为您在4.1.3节中看到了类似的内容。 那时,我们承诺在第7章中向您展示如何实现自定义控制器激活器,您知道什么? 这是第7章。
示例:实现CommerceControllerActivator
您可能会从第2章和第3章中回想起,电子商务示例应用程序向网站的访问者显示了产品及其价格的列表。 在6.2节中,我们添加了一项功能,该功能允许用户计算两个位置之间的路线。尽管我们展示了几个组合根(Composition Root)片段,但我们没有展示完整的示例。 连同清单7.7的Startup类一起,清单7.8的CommerceControllerActivator类显示了完整的组合根(Composition Root)。
电子商务示例应用程序需要一个自定义的控制器激活器,以将控制器与所需的依赖项关联起来。 尽管整个对象图要深得多,但从控制器本身的角度来看,所有直接依赖关系的并集只有两个项目(图7.10)。
| 图7.10 示例应用程序中的两个控制器及其依赖关系 |
|---|
![]() |
下面的列表显示了一个CommerceControllerActivator,它将HomeController和RouteController及其依赖项组合在一起。
清单7.8 使用自定义控制器激活器创建控制器
public class CommerceControllerActivator : IControllerActivator
{
private readonly string connectionString;
public CommerceControllerActivator(string connectionString)
{
this.connectionString = connectionString;
}
public object Create(ControllerContext context)
{
Type type = context.ActionDescriptor <--获取要从ControllerContext创建的控制器类型
.ControllerTypeInfo.AsType();
if (type == typeof(HomeController))
{
return this.CreateHomeController();
}
else if (type == typeof(RouteController))
{
return this.CreateRouteController();
} <---假设请求的类型是HomeController或RouteController,则根据给定类型返回适当的控制器
else
{
throw new Exception("Unknown controller " + type.Name);
}
}
private HomeController CreateHomeController()
{
return new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(
this.connectionString)),
new AspNetUserContextAdapter()));
}
private RouteController CreateRouteController()
{
var routeAlgorithms = ...;
return new RouteController(
new RouteCalculator(routeAlgorithms));
} <--25-39rows 用所需的依赖项显式连接控制器并返回它们。 两种类型都使用构造函数注入,因此您可以通过它们的构造函数来提供依赖项。
public void Release(
ControllerContext context, object controller) <--我们现在将Release方法留空,因为我们将在8.2节中返回到此。
{
}
}
注 如前所述,
ASP.NET Core包含其自己的内置DI容器(DI Container)(我们将在第15章中进行讨论)。或者,您可以使用此内置的DI容器(DI Container)注册您的依赖关系。在第12章中,我们将讨论如何决定使用Pure DI还是DI容器(DI Container)。在本书的这一部分中,我们将坚持使用Pure DI。
在启动中注册CommerceControllerActivator实例后,它会正确创建具有所需依赖关系的所有请求的控制器。除了控制器之外,ASP.NET Core所谓的中间件通常需要使用DI的其他常见组件。
使用Pure DI构造自定义中间件组件(Constructing custom middleware components using Pure DI)
ASP.NET Core使得在请求管道中插入额外行为相对容易。这种行为会影响请求和响应。在ASP.NET Core中,对请求管道的这些扩展称为中间件。 将中间件连接到请求管道的典型用法是通过Use扩展方法:
var logger = loggerFactory.CreateLogger("Middleware"); <--创建一个ILogger实例供中间件使用
app.Use(async (context, next) => <--与每个请求一起运行。context参数是HttpContext,下一个参数是Func<Task>。
{
logger.LogInformation("Request started"); <--在继续执行管道的其余部分之前运行一些代码
await next(); <--调用next()会导致其余管道运行。 由于next()返回一个任务,因此您必须等待该任务的结果。
logger.LogInformation("Request ended"); <--在管道的其余部分运行之后运行一些代码
});
注 这是本书中第一次展示使用C#5.0 async和await关键字的代码示例。 如果您是C#开发人员,则可能已经遇到了异步编程的示例,因为ASP.NET Core是围绕异步编程模型构建的。但是,有关异步编程的讨论不在本书的讨论范围之内。幸运的是,在应用DI时,异步编程不是问题,因为对象图的构建应始终快速,绝不依赖于任何I/O对象,因此应始终保持同步.
但是,在请求的主要逻辑运行之前或之后,通常需要做更多的工作。 因此,您可能希望将此类中间件逻辑提取到其自己的类中。这样可以防止您的Startup类混乱不堪,并且可以根据需要对这种逻辑进行单元测试(Unit testing)。 您可以将我们先前的使用 lambda的主体提取到新创建的LoggingMiddleware类上的Invoke方法中:
public class LoggingMiddleware
{
private readonly ILogger logger;
public LoggingMiddleware(ILogger logger) <--构造函数接受所需的依赖关系。
{
this.logger = logger;
}
public async Task Invoke( <--Invoke方法包含以前在线提供的逻辑。
HttpContext context, Func<Task> next)
{
this.logger.LogInformation("Request started");
await next();
this.logger.LogInformation("Request ended");
}
}
现在将中间件逻辑移到LoggingMiddleware类中,可以将启动配置最小化为以下代码:
var logger = loggerFactory.CreateLogger("Middleware");
app.Use(async (context, next) =>
{
var middleware = new LoggingMiddleware(logger); <--使用其依赖关系构造一个新的中间件组件
await middleware.Invoke(context, next); <--通过传递上下文和下一个参数来调用中间件
});
注 当创建的中间件组件的对象图变得更加复杂时,可能有必要将组件的创建移动到其他组件的组合位置。 在我们之前的示例中,将是CommerceControllerActivator。 但是,我们将其留给读者练习。
ASP.NET Core MVC的伟大之处在于它在设计时就考虑到了DI,因此,在大多数情况下,您只需要知道并使用单个可扩展性点就可以为应用程序启用DI。 对象组成是DI的三个重要维度之一(其他是生命周期管理和拦截)。
在本章中,我们向您展示了如何在各种不同的环境中由松散耦合(Loose Coupling)的模块组成应用程序。 某些框架实际上使它变得容易。在编写控制台应用程序和Windows客户端(例如UWP)时,您或多或少可以直接控制应用程序入口点的情况。 这为您提供了一个独特且易于实现的组合根(Composition Root)。 其他框架(例如ASP.NET Core)使您的工作更加辛苦,但是它们仍然提供了缝隙(Seam),可用于定义应用程序的组成方式。 ASP.NET Core在设计时就考虑到了DI,因此编写应用程序就像实现自定义IControllerActivator并将其添加到框架一样容易。
没有对象组合(Object Composition),就没有DI,但是当我们将对象的创建从使用类中移出时,您可能还没有完全意识到对对象生命周期的影响。 您可能会发现很明显,外部调用者(通常是DI容器(DI Container))创建了新的依赖项实例-但是何时将注入的实例解除分配? 而且,如果外部调用者不是每次都创建新实例,而是将现有实例交给您,该怎么办? 这些是下一章的主题。
总结
- 对象组合(Object Composition)是建立相关组件层次结构的操作,它发生在组合根内部。
- 组合根(Composition Root)目录仅应执行以下四项操作:加载配置值,构建对象图,调用所需的功能以及释放对象图。
- 只有组合根(Composition Root)才应依赖配置文件,因为它可以更加灵活地使库可以由其调用者强制性地进行配置。
- 将配置值的加载与执行对象组合(Object Composition)的方法分开。这样就可以在没有配置文件的情况下测试对象组成。
Mode-View-ViewModel(MVVM)是一种设计,其中ViewModel是视图和模型之间的桥梁。每个ViewModel都是一个类,以特定于技术的方式转换和公开模型。 在MVVM中,ViewModels是将使用DI组成的应用程序组件。- 在控制台应用程序中,
Program类是一个合适的组合根(Composition Root)。 - 在
UWP应用程序中,App类是合适的组合根(Composition Root),其OnLaunched方法是主要的入口点。 - 在
ASP.NET Core MVC应用程序中,IControllerActivator是插入对象组合的正确扩展点。 - 确保在
ASP.NET Core中处理依赖项的一种实用方法是使用HttpContext.Response.RegisterForDispose方法将其添加到一次性请求对象列表中。 - 通过将一个函数注册到实现组合根(Composition Root)的一小部分的管道,可以将中间件添加到
ASP.NET Core。这将构成中间件组件并调用它。











浙公网安备 33010602011771号