当解决方案的规模和范围扩大时,保持整体应用程序的灵活性变得更加困难。 对象之间的依赖关系不断增长,更改一个类可能需要更新其他类,依赖注入 (DI) 可以帮助解决这一挑战。
如您所知,依赖注入是“控制反转”(IoC)编程原理的一种形式。 这意味着类不会创建它们所依赖的对象,DI 框架具有负责揭示和解决依赖关系的容器。
依赖注入可以解决哪些问题?
假设您有一个使用数据服务获取数据的视图模型:
public class UserViewModel { MyDataService dataService; public UserViewModel() { this.dataService = new MyDataService(); } }
视图模型依赖于一个服务——这意味着 MyDataService 是 UserViewModel 的依赖,直接在视图模型类中创建服务非常容易,但是这种方法有几个缺点:
- 类紧密相连,每次使用 UserViewModel 时,它都会隐式创建 MyDataService 的一个实例。
- 如果以后修改 MyDataService 的初始化方式,则必须修改 MyDataService 初始化的所有视图模型。
- 您不能专门为 UserViewModel 类创建单元测试,因为它依赖于 MyDataService。 如果测试失败,您可能无法确定错误是在 UserViewModel 中还是在 MyDataService 中。
如果将 MyDataService 传递给 UserViewModel 的构造函数,则可以避免这些问题:
public class UserViewModel { MyDataService dataService; public UserViewModel(MyDataService dataService) { this.dataService = dataService; } }
不幸的是,这种技术也有缺陷:
- 在每个类中创建不同的视图模型,并且它们都必须知道如何创建 MyDataService。
- 如果不创建静态属性,可能很难在多个视图模型之间共享同一个 MyDataService 实例。
依赖注入的主要思想是集中解决所有依赖,这意味着您的程序中有一个单独的块来初始化新的类实例并将参数传递给它们。 尽管您可以为此实现自己的逻辑,但使用 DI 框架来帮助避免/消除示例代码会更方便。
依赖注入模式具有以下优点:
- 类之间是松散耦合的,因此您可以轻松地修改依赖项,例如,将MyDataService替换为MyDataServiceEx。
- 创建单元测试很容易,因为您可以将模拟参数传递给测试的类。
- 您的项目结构良好,因为始终知道所有依赖项的管理位置。
将依赖注入应用到WPF应用程序
.NET 社区有许多很棒的框架来帮助您在应用程序中实现依赖注入模式,所有这些框架都有两个主要特点:
- 您可以在容器中注册类。
- 您可以创建具有已初始化依赖项的对象。
容器是 DI 框架中的中心对象,可以自动检测和解决类依赖关系。 某些框架可以将参数注入类属性,但最常见的方法是将参数注入构造函数。
让我们修改 MainViewModel 构造函数,使其接受接口替代类:
public class MainViewModel { IDataService dataService; public MainViewModel(IDataService dataService) { this.dataService = dataService; } }
这将允许您在将来使用不同的 IDataService 实现。
进行此更改后,我们需要创建一个 DI 容器来注册 MyDataService 并实例化 MainViewModel:
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var builder = new ContainerBuilder(); //allow the Autofac container resolve unknown types builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource()); //register the MyDataService class as the IDataService interface in the DI container builder.RegisterType<MyDataService>().As<IDataService>().SingleInstance(); IContainer container = builder.Build(); //get a MainViewModel instance MainViewModel mainViewModel = container.Resolve<MainViewModel>(); }
在此示例中,我们使用了 Autofac 框架,但您可以使用任何其他 DI 框架,例如 Unity 或 Ninject。 DI 容器创建 MainViewModel 并自动将 MyDataService 注入 MainViewModel 构造函数,这允许您在每次创建具有 IDataService 参数类型的类时避免 MyDataService 初始化。
然后我们需要将 MainViewModel 连接到它的视图:MainView,最明显的策略是在视图构造函数中设置 DataContext:
public MainView() { InitializeComponent(); this.DataContext = container.Resolve<MainViewModel>(); }
但是,要访问 DI 容器,您必须将其设为静态或将其传递给每个视图构造函数。 一个更好的解决方案是创建一个标记扩展,它根据其类型返回一个视图模型实例:
public class DISource : MarkupExtension { public static Func<Type, object> Resolver { get; set; } public Type Type { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) => Resolver?.Invoke(Type); }
<UserControl DataContext="{local:DISource Type=local:MainViewModel}">
最初,标记扩展未绑定到任何 DI 容器。 要允许扩展使用您的容器,请按以下方式指定视图模型解析器:
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); //... IContainer container = builder.Build(); DISource.Resolver = (type) => { return container.Resolve(type); }; } }
这种技术允许 DISource 与任何可能的容器一起工作。
依赖注入和DevExpress服务
在上面的示例中,我们使用了不与可视控件通信的抽象数据服务。 如您所知,许多DevExpress WPF服务使用视图控件,因此服务必须知道使用哪个控件。 例如,如果您希望将 NavigationFrameService 注入到视图模型中,还需要将此服务附加到相应的 NavigationFrame 控件。
在视图模型中创建一个包含服务的公共属性,并将服务绑定到 NavigationFrame:
public class MainViewModel { public INavigationService NavigationService { get; } public MainViewModel(INavigationService navigationService) => NavigationService = navigationService; }
<dxwui:NavigationFrame> <dxmvvm:Interaction.Behaviors> <common:AttachServiceBehavior Service="{Binding NavigationService}"/> </dxmvvm:Interaction.Behaviors> </dxwui:NavigationFrame>
AttachServiceBehavior 是一个简单的附加操作,它在服务属性更改时调用 NavigationFrameService.Attach。 虽然 AttachServiceBehavior 不包含在我们的库中,但您可以在此处获取其代码:How to use our Services with Dependency Injection/AttachServiceBehavior。即使 MainViewModel 使用 NavigationFrameService,它也不必实现 ISupportServices 接口。 此外,导航中涉及的所有子视图都可以在不附加到 NavigationFrame 的情况下使用该服务——因为它已经在主视图级别进行了配置。
并非所有DevExpress 服务都需要可视化组件,某些服务,例如 DXMessageBoxService或 DXOpenFileDialogService,不需要显式附加,因此您可以将它们作为任何其他非 DevExpress 服务注入。
在某些情况下,如果需要为特定视图配置服务,则使用 DI 容器注入服务可能并不明智。 例如,如果您有一个绑定到视图模型命令的服务,就会出现这种情况,直接在视图中定义服务并在那里配置所有绑定要容易得多。
DevExpress WPF拥有120+个控件和库,将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序,这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件的衍伸产品,还是以数据为中心的商业智能产品,都能通过DevExpress WPF控件来实现。
DevExpress技术交流群6:600715373 欢迎一起进群讨论