Prism研究(for WPF & Silverlight)5.Module研究

终于要坐下来说一说Module的相关技术了。

      本来不想讨论Module,因为一旦写好这些框框,以后就再也不会改变了。要知道,我们在Prism中更关心的是MVP模式的拆分。

      Module相关技术包括两部分,一是如何加载Module,也就是在Shell的Bootstarpper中重写它的InitializeModules方法,从而把所有需要的Module加载到主程序中。二是如何创建映射,即在每个Module内,实现IModule接口的Initialize方法,从而建立Region和View之间的Mapping关系。

      (一)在Bootstarpper中加载模块:对抽象类UnityBootstrapper的实现

      UnityBootstrapper这个类可称得上是Prism框架的中枢神经。

      我们知道,在App.xaml.cs中,执行的是Bootstarpper类的Run方法,就在这个方法中,都包括了哪些关键步骤呢?

            1.调用ConfigureContainer()方法,配置容器。包括3小步:

                  1)注入ILoggerFacade,于是我们可以定义自己的Log系统,这个技术我会在以后章节单独介绍。

                  2)调用GetModuleCatalog方法,从而获取并注册全部Module。我们通常会重写这个方法。

                  3)根据Run方法中的布尔值(默认为true),决定是否要默认注册8对接口和相应的类:

protected virtual void ConfigureContainer()
{
    Container.RegisterInstance<ILoggerFacade>(LoggerFacade);

    IModuleCatalog catalog = GetModuleCatalog();
    if (catalog != null)
    {
        this.Container.RegisterInstance(catalog);
    }

    if (useDefaultConfiguration)
    {
        RegisterTypeIfMissing(typeof(IServiceLocator), typeof(UnityServiceLocatorAdapter), true);
        RegisterTypeIfMissing(typeof(IModuleInitializer), typeof(ModuleInitializer), true);
        RegisterTypeIfMissing(typeof(IModuleManager), typeof(ModuleManager), true);
        RegisterTypeIfMissing(typeof(RegionAdapterMappings), typeof(RegionAdapterMappings), true);
        RegisterTypeIfMissing(typeof(IRegionManager), typeof(RegionManager), true);
        RegisterTypeIfMissing(typeof(IEventAggregator), typeof(EventAggregator), true);
        RegisterTypeIfMissing(typeof(IRegionViewRegistry), typeof(RegionViewRegistry), true);
        RegisterTypeIfMissing(typeof(IRegionBehaviorFactory), typeof(RegionBehaviorFactory), true);

        ServiceLocator.SetLocatorProvider(() => this.Container.Resolve<IServiceLocator>());
    }
}

            我们经常会重写这个方法,从而加上自己的逻辑,存在两种场景:

                  1) 基类方法中默认注册的Mapping不满意,注销其中的一个或全部。代码如下:

                  2) 当我们想要连Shell都进行MVP模式重构时,以下是StrokeTrader RI中的示例代码:

protected override void ConfigureContainer()
{
    Container.RegisterType<IShellView, Shell>();

    base.ConfigureContainer();
}

            2.调用ConfigureRegionAdapterMappings方法,注册基础控件和相应适配器之间的Mapping关系。WPF下有3个基础控件(Selector、ItemsControl、ContentControl,我们经常使用的是ContentControl),在Silverlight下还要额外注册TabControl。

        protected virtual RegionAdapterMappings ConfigureRegionAdapterMappings()
        {
            RegionAdapterMappings regionAdapterMappings = Container.TryResolve<RegionAdapterMappings>();
            if (regionAdapterMappings != null)
            {
#if SILVERLIGHT
                regionAdapterMappings.RegisterMapping(typeof(TabControl), this.Container.Resolve<TabControlRegionAdapter>());
#endif
                regionAdapterMappings.RegisterMapping(typeof(Selector), this.Container.Resolve<SelectorRegionAdapter>());
                regionAdapterMappings.RegisterMapping(typeof(ItemsControl), this.Container.Resolve<ItemsControlRegionAdapter>());
                regionAdapterMappings.RegisterMapping(typeof(ContentControl), this.Container.Resolve<ContentControlRegionAdapter>());
            }

            return regionAdapterMappings;
        }

            我们可以定义自己的适配器,这将在以后章节中看到。

            这三个基础控件的使用场合是什么呢?我一开始是很明白的,后来完全被Prism自带的Sample搞糊涂了。我会在RI项目分析时,讨论这个问题。

            3.调用ConfigureDefaultRegionBehaviors方法,配置Behavior。

            这块涉及到另一门新的技术了,我准备另开章节,这里不宜展开。

            4.调用RegisterFrameworkExceptionTypes方法,这涉及到Prism内部的异常处理机制,再议再议。

            5.调用CreateShell方法,注意到,UnityBootstrapper类之所以是抽象的,就是因为这个方法是虚的,所以我们要重写它,创建并返回Shell窗体对象,并依赖注入IRegionManager对象。

            6.调用InitializeModules方法,决定了哪些Module要被加载。终于说到这里了,累死我了。这个方法是要仔细分析的。

            如何把Module加载到Shell中呢?一种方式就是重写前面的GetModuleCatalog方法,如下所示:

const string moduleBAssemblyQualifiedName = "Modules.ModuleB.ModuleB, Modules, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
protected override IModuleCatalog GetModuleCatalog()
{
    ModuleCatalog catalog = new ModuleCatalog();

    catalog.AddModule(typeof (ModuleA), "ModuleD")
        .AddModule(typeof (ModuleD), "ModuleB")
        .AddModule(typeof (ModuleC), InitializationMode.OnDemand)
        ;

    catalog.AddModule(new ModuleInfo("ModuleB", moduleBAssemblyQualifiedName));
        
    return catalog;
}

            另一种方式就是重写InitializeModules方法了,也能达到同样的效果:

protected override void InitializeModules()
{
    IModule projectModule = this.Container.Resolve<ProjectModule>();
    projectModule.Initialize();

    IModule employeeModule = this.Container.Resolve<EmployeeModule>();
    employeeModule.Initialize();
}

            只是这次,我们要手动调用Module的Initialize方法了。

            而在GetModuleCatalog方法中是自动加载Module的:实例化ModuleManager,依次调用该实例的Run方法——LoadModulesWhenAvailable方法——LoadModuleTypes方法——LoadModulesThatAreReadyForLoad方法——InitializeModule方法——调用ModuleInitializer实例的Initialize方法——调用IModule的Initialize方法。

            终于绕出来了,算是殊途同归了吧。

            那么,为什么要有两种调用方法呢?我曾经困惑过一段时间,莫衷一是。不过在研究了Prism框架的源码时候我明白了,这两个方法都是可以重写的,只是调用GetModuleCatalog方法的位置位于InitializeModules的前面,我们可以任选一个而忽略另一个。什么?你要在GetModuleCatalog方法中加载ModuleA,而在InitializeModules方法中加载ModuleB——等着被同事Complaint吧。

            现在我们着重分析GetModuleCatalog方法。

            通常有两种加载模块的方式:一,手动编码加载;二,在配置文件中设置。

            此外,在WPF中,还可以遍历目录进行加载;而在Silverlight中,又有一种Remoting加载的技术。

            Prism文档为这4种技术分别提供了一个Demo。依次讨论如下:

            1.手动加载Module

            参加目录\Quickstarts\Modularity\DefiningModulesInCodeQuickstart

            这种方式是很直接的。就像我们刚才写的代码那样:

protected override IModuleCatalog GetModuleCatalog()
{
    ModuleCatalog catalog = new ModuleCatalog();

    catalog.AddModule(typeof (ModuleA), "ModuleD")
        .AddModule(typeof (ModuleD), "ModuleB")
        .AddModule(typeof (ModuleC), InitializationMode.OnDemand)
        ;

    catalog.AddModule(new ModuleInfo("ModuleB", moduleBAssemblyQualifiedName));
        
    return catalog;
}

            不要小看这几行代码,麻雀虽小五脏俱全,涵盖了手动加载的所有技巧。

            效果图如下:

clip_image002

            最好是F5一步步调试,就可以看到,先加载模块B,然后是D,最后是A。而模块C开始时并不显示,只有当点击模块B中的按钮时,才会显示。

clip_image004

            这是因为,AddModule方法有5种重载:

public ModuleCatalog AddModule(Type moduleType, params string[] dependsOn)

public ModuleCatalog AddModule(Type moduleType, InitializationMode initializationMode, params string[] dependsOn)

public ModuleCatalog AddModule(string moduleName, string moduleType, params string[] dependsOn)

public ModuleCatalog AddModule(string moduleName, string moduleType, InitializationMode initializationMode, params string[] dependsOn)

public ModuleCatalog AddModule(string moduleName, string moduleType, string refValue, InitializationMode initializationMode, params string[] dependsOn)

            在前两个重载方法中,我们指定了第1个参数moduleType为模块的类型,而dependsOn参数指定了前面的模块依赖于后面这个模块,initializationMode参数是一个枚举。

public enum InitializationMode
{
    WhenAvailable,
    OnDemand
}

            WhenAvailable是默认选项,就是说一开始就加载;而OnDemand则表示按需加载,一开始并不会进行加载。因为我们在代码中指定了

.AddModule(typeof (ModuleC), InitializationMode.OnDemand)

            所以,一开始并不会显示模块C。在点击模块B中按钮的时候,调用ModuleManager实例的LoadModule方法:

private void OnLoadModuleCClick(object sender, RoutedEventArgs e)
{
    moduleManager.LoadModule("ModuleC");
}

            顺藤摸瓜,沿着LoadModule方法一级级找下去——调用LoadModuleTypes方法——LoadModulesThatAreReadyForLoad方法——InitializeModule方法——调用ModuleInitializer实例的Initialize方法——调用IModule的Initialize方法。于是,模块C被加载了。

            第3个和第4个重载方法不常用,因为moduleName实际上就是moduleType.Name。第5个重载方法是给Silverlight用的,它有一个refValue参数,用来指定远程XAP的地址。

2.根据配置文件动态加载Module

            还是刚才那个效果,我们把模块的前后顺序和依赖关系写在App.config的配置节点modules中:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="modules" type="Microsoft.Practices.Composite.Modularity.ModulesConfigurationSection, Microsoft.Practices.Composite"/>
  </configSections>
  <modules>
    <module assemblyFile="Modules/ModuleD.dll" moduleType="ModuleD.ModuleD, ModuleD" moduleName="ModuleD">
      <dependencies>
        <dependency moduleName="ModuleB"/>
      </dependencies>
    </module>
    <module assemblyFile="Modules/ModuleB.dll" moduleType="ModuleB.ModuleB, ModuleB" moduleName="ModuleB"/>
    <module assemblyFile="Modules/ModuleA.dll" moduleType="ModuleA.ModuleA, ModuleA" moduleName="ModuleA">
      <dependencies>
        <dependency moduleName="ModuleD"/>
      </dependencies>
    </module>
    <module assemblyFile="Modules/ModuleC.dll" moduleType="ModuleC.ModuleC, ModuleC" moduleName="ModuleC" startupLoaded="false"/>
  </modules>
</configuration>

            这样的话,我只需在GetModuleCatalog方法中直接返回ConfigurationModuleCatalog对象就可以了:

protected override IModuleCatalog GetModuleCatalog()
{
    ModuleCatalog catalog = new ConfigurationModuleCatalog();
    return catalog;
}

            罗嗦几句,根据配置文件加载Module的方式远远优于手动编程的方式,虽然配置起来很麻烦,但是在大型项目中是首选。

 

      (二)具体Module内的编程:IModule接口

      Module介于Shell和View之间,我们可以认为它是View的载体。因此,在把一个复杂的xaml拆分成若干零散的View的时候,我们会手动创建若干以Module名称命名的项目,并把这些View按照类别放到不同的Module项目中。比如说Prism自带的StrockTraderRI(简称RI),参考下面的截图:

clip_image006

      看到没有,RI有4个Module,它们都作为项目而存在,并且每个项目都带有一个类似于MarketModule这样的类,它派生自接口IModule:

public interface IModule
{
    void Initialize();
}

      于是,实现了IModule接口的类,都具有这样的格式:

public class MarketModule : IModule
{
    private readonly IUnityContainer container;
    private readonly IRegionManager regionManager;

    public MarketModule(IUnityContainer container, IRegionManager regionManager)
    {
        this.container = container;
        this.regionManager = regionManager;
    }

    #region IModule Members

    public void Initialize()
    {
        RegisterViewsAndServices();

        this.regionManager.RegisterViewWithRegion(RegionNames.ResearchRegion, () => this.container.Resolve<ITrendLinePresentationModel>().View);
    }

    protected void RegisterViewsAndServices()
    {
        container.RegisterType<IMarketHistoryService, MarketHistoryService>(new ContainerControlledLifetimeManager());
        container.RegisterType<IMarketFeedService, MarketFeedService>(new ContainerControlledLifetimeManager());
        container.RegisterType<ITrendLineView, TrendLineView>();
        container.RegisterType<ITrendLinePresentationModel, TrendLinePresentationModel>();
    }

    #endregion
}

      我们看到:

            1.要在构造函数中实现依赖注入,需要什么就注入什么。

            2.实现Initialize方法,包括:

                  1)在容器中注册接口和实现了该接口的类的mapping关系,比如说Service、View、Model、Presenter,我们一般都会同时添加相应的接口。我们将这些注册封装在一个名为RegisterViewsAndServices的方法中。

                  2)还要在Initialize方法中,注册Register和View之间的关系,也就是RegisterViewWithRegion方法。

            所有的Module类都是按照这样的格式来实现。而关于注册View的技术,还有一些小变体。

            比如说,我们可以把

this.regionManager.RegisterViewWithRegion("MainRegion", typeof(Views.HelloWorldView));

      替换为:

regionViewRegistry.RegisterViewWithRegion("MainRegion", typeof(Views.HelloWorldView));

      这里,regionViewRegistry的声明和注入是这样的:

private readonly IRegionViewRegistry regionViewRegistry;

public ProjectModule(IRegionViewRegistry regionViewRegistry)
{
    this.regionViewRegistry = regionViewRegistry;
}

      这就引入了一个新的问题,IRegionViewRegistry和IRegionManager都具有RegisterViewWithRegion方法,二者有区别么?

      答案是——没有。我们已经分析过,在UnityBootstrapper的中,已经默认建立了IRegionManager和RegionManager的映射关系。所以,只要查看Prism框架中的RegionManager就可以了。

      以下则是RegionManager的RegisterViewWithRegion方法,这是一个扩展方法:

public static IRegionManager RegisterViewWithRegion(this IRegionManager regionManager, string regionName, Type viewType)
{
    var regionViewRegistry = ServiceLocator.Current.GetInstance<IRegionViewRegistry>();

    regionViewRegistry.RegisterViewWithRegion(regionName, viewType);

    return regionManager;
}

      哦,原来还是要间接地调用RegionViewRegistry的RegisterViewWithRegion方法。

      还是那句老话,殊途同归。

 

 

下回内容提示:

      我们还可以把

this.regionManager.RegisterViewWithRegion("MainRegion", typeof(Views.HelloWorldView));

      替换为:

this.regionManager.Regions["MainRegion"].Add(new Views.HelloWorldView());

      ——这就涉及到了View的两种模式:View Injection和View Discovery。

 

      我们还可以将其替换为:

this.regionManager.RegisterViewWithRegion(RegionNames.ResearchRegion, () => this.container.Resolve<IHelloWorldViewPresentationModel>().View);

      ——这就涉及到了MVP的两种变体:View -first和Presenter-first。

 

      欲知详情,请看下回《Prism研究——MVP模式之七十二变》。

相关文章
相关标签/搜索