65.9K
CodeProject 正在变化。 阅读更多。
Home

利用 WPF 中的 IoC

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016 年 5 月 26 日

CPOL

7分钟阅读

viewsIcon

22665

downloadIcon

387

使用依赖注入来管理 MVVM 设计中的导航和数据上下文分配。

引言

首先,我来介绍一下 IoC —— 控制反转。软件是由固定部分和移动部分组成的。在我看来,IoC 用于通过提供代码的固定部分来选择和动态选择移动部分,而不是将固定部分与移动部分紧密耦合,从而使代码难以应对软件世界中唯一不变的“变化”。IoC 可以通过多种方式实现,我们将使用依赖注入和工厂模式,在这种情况下,外部框架是我们的 DI 容器 Castle Windsor。我们将使用 DI 来促进视图、视图模型和业务逻辑之间的松耦合。这还不是全部;我们还将看到 IoC 如何在 WPF 应用程序中使用树视图和向导时使生活变得更轻松。

要理解本文,您至少需要具备以下基本知识:

  • C# 语言
  • WPF
  • 依赖注入和工厂设计模式

背景

当时的情况是,我们已经构建了一个使用 DI 和 CQRS 模式的 Windows 服务。后来,我们决定构建一个与该服务共享通用存储库的 WPF 应用程序。UI 团队提出了一个通常不使用 DI 的 WPF 项目。我承担了将 WPF UI 项目转换为使用 DI 以重用现有存储库的任务。在我克服挑战的过程中,我意识到代码转换不仅遵循了我们使用 DI 的架构,而且变得更易于维护、更易读、更简洁,并且还解决了一些设计问题。

使用代码

我将解释将 Castle Windsor 容器集成到项目所需的代码。

步骤 1

为您的具体视图和视图模型定义相应的抽象(接口)。接口的作用是促进与其他视图和视图模型之间的松耦合。我的示例员工管理 WPF 应用程序的类图如下所示:

第二步

创建 ViewSelector,它像下面一样覆盖 Windsor 容器的 DefaultTypedFactoryComponentSelectorViewSelector 的作用是在传入视图名称作为参数时返回视图的抽象类型。

class ViewSelector : DefaultTypedFactoryComponentSelector
{
    private const string DotSeperator = ".";
    private const string InterfacePrefix = "I";
    private const string ViewDirectory = "Interfaces.Views";
    private const string ViewSuffix = "View";

    protected override string GetComponentName(MethodInfo method, object[] arguments)
    {
        return null;
    }

    protected override Type GetComponentType(MethodInfo method, object[] arguments)
    {
        StringBuilder typeName = new StringBuilder();
        typeName.Append(Assembly.GetExecutingAssembly().GetName().Name);
        typeName.Append(DotSeperator);
        typeName.Append(ViewDirectory);
        typeName.Append(DotSeperator);
        typeName.Append(InterfacePrefix);
        typeName.Append(arguments[0]);
        typeName.Append(ViewSuffix);

        return Type.GetType(typeName.ToString());
    }
}

步骤 3

创建一个接口 IViewFactoryIViewFactory 的作用是在传入视图名称作为参数时返回具体的视图实例。

public interface IViewFactory
{
   IView GetView(string viewName);
}

步骤 4

创建 ViewsInstaller 类,它实现了 Castle Windsor 的 IWindsorInstallerViewsInstaller 的作用是将视图和视图模型的具体实现注册到抽象接口。换句话说,这是让 DI 容器知道在运行时为抽象接口注入的具体实现。

public class ViewsInstaller : IWindsorInstaller
{        
    private IWindsorContainer _container;
    private string _assemblyName = Assembly.GetExecutingAssembly().FullName;

    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
       this._container = container;
       this.RegisterViews();
       this.RegisterViewModels();
    }

    private void RegisterViews()
    {
        this._container.Register(
                 Component.For<itypedfactorycomponentselector>().ImplementedBylt;viewselector>(),

                Castle.MicroKernel.Registration.Classes.FromAssemblyNamed(this._assemblyName)
                  .BasedOn(typeof(IView))
                  .WithService.FromInterface()
                  .Configure(c => c.LifeStyle.Is(LifestyleType.Transient)),

             Component.For<iviewfactory>().AsFactory(c => c.SelectedWith<viewselector>()).LifestyleSingleton()
         );
     }

    private void RegisterViewModels()
    {
        this._container.Register(                
             Castle.MicroKernel.Registration.Classes.FromAssemblyNamed(this._assemblyName)
              .BasedOn(typeof(IViewModel))
              .WithService.FromInterface()
              .Configure(c => c.LifeStyle.Is(LifestyleType.Transient)));
    }
}

步骤 5

创建一个 Bootstrapper,它创建一个 DI 容器的实例。

internal static class Bootstrapper
{
    private static IWindsorContainer container;

    internal static IWindsorContainer InitializeContainer()
    {
       container = new WindsorContainer();
       container.AddFacility<typedfactoryfacility>(); // Provides automatically generated factories
       container.Install(FromAssembly.This()); 
       // Instantiates the installers that implements IWindsorInstaller in this assembly and invokes the constructor
       return container;
    }
}

您会看到,我们没有为 IViewFactory 提供显式实现,而是将 typedfactoryfacility 添加到了我们的 Windsor 容器中,从而确保 Castle Windsor 为 IViewFactory 提供实现。我们使用了工厂模式,在传入视图名称时动态生成视图。

好的!现在我们已经准备好在应用程序中使用我们的 DI 容器了。让我们深入了解用例。这是展示我的 WPF 应用程序 UI 设计的线框图。

还有一个 GIF 图像,让您对我们将要实现的目标有一个大致的了解。

请原谅我的 UI。我是一名 XAML 初学者 :)

首先,我需要在应用程序启动时调用 Bootstrapper 并实例化我的应用程序的 DI 容器。然后显式解析 IMainView 以获得具体的 MainView

public partial class App : Application
{
    private IWindsorContainer _container;
    private IMainView _mainView;

    public App()
    {            
       this._container = Bootstrapper.InitializeContainer();            
    }

    private void Application_Startup(object sender, StartupEventArgs e)
    {
       this._mainView = this._container.Resolve<imainview>();
       this._mainView.ShowDialog();
    }       
}

MainView 中,我们需要 ShellView 的实例。因此,我们在构造函数中设置 ShellView 以便注入。

public MainView(IShellView shellView)
{
    InitializeComponent();
    this.grdContainer.Children.Clear();
    this.grdContainer.Children.Add((UIElement)shellView);
}

ShellView 中,我们需要 ViewFactoryShellViewModel 的实例。

public ShellView(IShellViewModel shellViewModel, IViewFactory viewFactory)
{
    InitializeComponent();
    this._viewFactory = viewFactory;
    this.DataContext = shellViewModel;
}

正如您在框线图中看到的,ShellView 包含 TreeView 控件。现在,让我们看看如何利用 DI 来使用 TreeView 控件。要将数据绑定到 TreeView,请创建一个名为 ViewMenu 的类。

public class ViewMenu
{
    private string menuName;
    private string itemName;        
    private string toolTip;

    public bool IsRoot { get; set; }
    public bool IsExpand { get; set; }
    public List<viewmenu> SubMenuItems { get; set; }

    public string MenuName
    {
       get { return menuName; }
       set { menuName = value; }
    }

    public string ItemName
    {
       get { return itemName; }
       set { itemName = value; }
    }        

    public string ToolTip
    {
       get { return toolTip; }
       set { toolTip = value; }
   }
}

并在 ShellView 中填充 viewmenu 集合。

List<viewmenu> subMenus = new List<viewmenu>();
subMenus.Add(new ViewMenu() { MenuName = "Employee Profile", ItemName = "EmployeeProfile", ToolTip = "Employee Profile" });
subMenus.Add(new ViewMenu() { MenuName = "Employee Contact", ItemName = "EmployeeContact", ToolTip = "Employee Contact" });
subMenus.Add(new ViewMenu() { MenuName = "Employee Registration", ItemName = "EmployeeRegistration", ToolTip = "Employee Registration" });           
this.ViewMenuItems = new ObservableCollection<viewmenu>();            
this.ViewMenuItems.Add(new ViewMenu() { MenuName = "Employee", ItemName = "EmployeeProfile", ToolTip = "Settings", SubMenuItems = subMenus });

这是 TreeView 的 XAML 代码。

<TreeView x:Name="MytreeView" ItemsSource="{Binding ViewMenuItems}" helper:TreeViewExtension.SelectedItem="{Binding ViewSelected, Mode=TwoWay}" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Hidden" Margin="0" ItemTemplate="{DynamicResource Navigation_DataTemplate}" />

选择菜单项时,将设置 ViewSelected 属性。

public ViewMenu ViewSelected
{
    get
    {
       return this._viewSelected;
    }

    set
    {
        if (this._viewSelected != value)
        {
           this._viewSelected = value;
           this.ChildViewSelected(this._viewSelected.ItemName);
        }
    }
}

下面是加载与 不使用 DIViewMenu 选定项对应的视图的代码。

public IView ChildViewSelected(string viewName)
{
    switch (viewName)
    {
        case "EmployeeProfile":
            EmployeeProfileView employeeProfileView = new EmployeeProfileView();
            employeeProfileView.DataContext = new EmployeeProfileViewModel();
            break;
        case "EmployeeContact":
            EmployeeContactView employeeContactView = new EmployeeContactView();
            employeeContactView.DataContext = new EmployeeContactViewModel();
             break;
    }
}

看起来很简单,对吧!等等……当您向 ViewMenuItems 集合添加子菜单时会发生什么?您必须记住为添加的每个子菜单添加一个 case。维护起来难道不麻烦吗?此外,您的 ShellView 与其他视图对象紧密耦合。使用 DI 会使其变得更简单。我们来看看。

private void OnChildViewSelected(string itemName)
{            
    IView viewSelected = this._viewFactory.GetView(itemName);
    this.pnlWorkArea.Children.Clear();
    this.pnlWorkArea.Children.Add((UIElement)viewSelected);
}

我们调用 ViewFactory 来获取相应视图名称的视图实例。Windsor 使用 ViewSelector 获取视图类型,并使用 ViewFactory 实现实例化的视图,这是 Windsor 生成的。

如您所见,代码干净且易读。我们消除了 ShellView 中对其他具体视图实现的依赖,从而实现了松耦合。此外,我们将视图选择和实例化的责任转移到了 ViewSelectorViewFactory。代码更易于维护,因为您不必担心为每个子菜单添加 case。您只需要创建视图并实现 IView 接口。

就这样吗?不,依赖注入的主要优势——松耦合——在您需要为特定受众定制应用程序时会派上用场。假设您有 EmployeeContactView 的多种表示形式。您只需在 ViewMenuItems 集合中设置所需的视图名称,ViewFactory 就会处理其余的事情。

现在,当所有内容都能无缝处理树视图控件时,让我们看看是否有任何方法可以改进我们的 DI 容器设置。您可能会回想起,在 ShellView 构造函数中,我们显式地为 ShellViewModel 分配了 DataContext。其他使用 DI 的视图也会出现这种情况。我们可以通过创建一个通用的解决方案来跳过此步骤,该解决方案将 DataContext 分配给注入的 ViewModel。为了实现这一点,我们需要创建一个 ViewActivator,它覆盖 Windsor 容器的 DefaultComponentActivator

protected override object CreateInstance(Castle.MicroKernel.Context.CreationContext context, ConstructorCandidate c, object[] arguments)
{
    var component = base.CreateInstance(context, c, arguments);
    this.AssignViewModel(component, arguments);
    return component;
}

private void AssignViewModel(object component, object[] arguments)
{
   var frameworkElement = component as FrameworkElement;

   if (frameworkElement == null || arguments == null)
   {
       return;
   }

   var vm = arguments.Where(a => a is IViewModel).FirstOrDefault();

   if (vm != null)
   {
       frameworkElement.DataContext = vm;
   }
}

另外,让 Windsor 使用 ViewActivator,方法是在视图安装程序中显式指定激活器类型。

Castle.MicroKernel.Registration.Classes.FromAssemblyNamed(this._assemblyName)
                  .BasedOn(typeof(IView))
                  .WithService.FromInterface()
                  .Configure(c => c.LifeStyle.Is(LifestyleType.Transient)
                  .Activator<viewactivator>());

好了,我们通过稍微调整 DI 容器来节省了一些时间。现在,让我们进入用例的第二部分:向导。这是我的示例应用程序中向导的 GIF 图像。

想法与 TreeView 控件 pretty much 相同,使用我们的 ViewFactory 在视图之间切换。但还有一项额外的事情需要完成,即在每个作用域中使用相同的 ViewModel 实例作为向导所有视图的 DataContext

等等,为什么对整个向导使用相同的 ViewModel 实例?

向导最初只是为了演示目的而创建的。ViewModel 与 UI 无关,应该根据将数据绑定到视图的职责来设计。在这种情况下,一个用于员工注册的 ViewModel 就足够了。它减少了映射数据和保存到数据库时的额外开销。此外,还可以更轻松地验证整个向导。

那么我们如何实现呢?我们首先需要了解 DI 容器的一个附加功能:生命周期。生命周期控制实例在什么作用域下被重用以及何时释放它们。

那么,让我们分析一下哪种生命周期适合我们的用例。

我们为 ViewModel 使用了瞬态生命周期。每次需要 ViewModel 实例时,容器都会生成一个新的实例,从不重用它们。因此,它不适合该用例。

使用单例生命周期会在有人首次请求 ViewModel 时创建它,并在之后每次需要时重用它。听起来不错,对吧?但有一个警告。我们的用例只需要每个员工注册作用域的同一个 ViewModel 实例,而不是应用程序级别的。

幸运的是,Windsor 容器提供了适合我们用例的生命周期——LifestyleBoundToNearest,但不幸的是,当使用工厂模式时,该生命周期不起作用。我不得不使用 Windsor 添加的 Lifestyle scoped 来指定组件实例生命周期/重用的任意作用域。我不得不借助 Guid 来定义我的作用域。

我们需要为 IScopeAccessor 提供实现。

class CustomScopeAccessor : IScopeAccessor
{
private static readonly ConcurrentDictionary<guid, ilifetimescope=""> Collection = new ConcurrentDictionary<guid, ilifetimescope="">();

public ILifetimeScope GetScope(Castle.MicroKernel.Context.CreationContext context)
{
    return Collection.GetOrAdd((Guid)Application.Current.Resources["ScopeId"], id => new DefaultLifetimeScope());
}
}

此外,我们必须在视图安装程序中为员工注册 ViewModel 显式指定生命周期。

this._container.Register(
                Component.For<iemployeeregistrationviewmodel>()
                .ImplementedBy(typeof(EmployeeRegistrationViewModel)).LifestyleScoped<customscopeaccessor>());

在完成之前,我想强调在应用程序结束时释放任何显式解析的实例并处理容器以避免内存泄漏的必要性!

private void Application_Exit(object sender, ExitEventArgs e)
{
    this._container.Release(this._mainView);
    this._container.Dispose();
}

结论

正如您所见,在 WPF 中利用依赖注入除了促进松耦合和提高代码的可维护性和可读性之外,还可以在设计中发挥作用。欢迎任何评论!

参考文献

https://visualstudiomagazine.com/articles/2011/10/01/wpf-and-inversion-of-control.aspx

https://github.com/castleproject/Windsor/blob/master/docs/lifestyles.md

https://github.com/castleproject/Windsor/blob/master/docs/implementing-custom-scope.md

修订历史

23-05-2016

原始文章

© . All rights reserved.