Osheep

时光不回头,当下最重要。

Dotspatial源码剖析-Dotspatial.DemoMap

《Dotspatial源码剖析-Dotspatial.DemoMap》

Dotspatial

第三章 Dotspatial.DemoMap


最新版本的Dotspatial.Core中包含一个project:Dotspatial.DemoMap,哈,一看名字就知道,想了解Dotspatial,这里是个绝佳的入口。本章内容围绕该部分的源码展开,目标是知晓Demo中各个组件的加载套路,进而介绍如何按Dotspatial的套路来开发组件,或者叫扩展,同时和后续章节呼应,争取从宏观和微观的角度对Dotspatial进行解读。

3.1 程序启动和初始配置


DemoMap是一个WinForm应用程序,程序的启动以Main()方法调用Application.Run(new MainForm())开始。MainForm类即是主窗体类,该类中会加载各类需要的控件Control(自定义或.net定义),常见的搞法自然是在MainForm的构造函数里InitializeComponent,但是这种搞法在主窗体控件很多(很多没有具体的标准),且交互逻辑复杂的时候将给代码维护带来灾难,尤其是在WinForm相关的UI测试还没那么方便的时候更是如此,很棒的是Dotspatial.DemoMap并非如此,这里采用了MEF的搞法来加载组件,MEF(参看:拖把的博客)。

3.2 加载和激活组件


这里使用了MEF的搞法,但是这并不是唯一使用Dotspatial.Core组件的方法,为了方便解读,建议读者一定先阅读拖把的博客,对MEF有基本了解之后再继续,否者会比较困惑现有代码的逻辑。

顺着MainForm的构造函数,很容易找到appManager.LoadExtensions();其中appManager是AppManager类的实例,LoadExtensions()中核心代码如下:

Thread updateThread = new Thread(AppLoadExtensions); updateThread.Start();  //加载组件

ActivateAllExtensions(); //激活或者配置组件

加载组件中AppLoadExtensions()方法包含了MEF的常规配置,配置目录,配置组合容器,以及配对初始化等。

但是MEF只是实例化对象,控件的属性怎么配置呢?这是第二步的主要工作:激活或者配置组件。

DemoMap中加载的Extension包括两种类型,第一种实现了ISatisfyImportsExtension接口,第二种实现了IExtension接口,这两个接口都包含Activate(),处理组件的初始配置工作,IExtension还包括一个Deactivate()方法,顾名思义,这里处理当组件处于非激活状态时的工作。接下来针对这两个接口进行详细的说明,因为这两个接口其实是MEF组件定义的标准接口,如果想以现有的模式给Dotspatial添加自定义组件,自定义组件的入口必须实现这两个接口之一,当然,仍需要配置组件编译的dll输出路径,这是后话,依据加载组件的流程。

3.2.1 ISatisfyImportsExtension解读


《Dotspatial源码剖析-Dotspatial.DemoMap》

ISatisfyImportsExtension

哈,好简单,那一般什么样的Extension需要实现这个接口呢?在DemoMap中DockManager,HeaderControl,SatisfyImportsOnStartupExtension和StatusControl分别实现了该接口,这些Extension都是很基础的Extension,必须在程序启动时配置完成,其中DockManager,HeaderControl,StatusControl在界面上看分别指布局相关,界面头相关(一般包含菜单栏),界面底部相关(一般包含状态栏),其中Priority属性用来标记激活的优先顺序,值越小,越先被激活,Activate()方法是激活组件的入口,组件激活相关的逻辑都写在实现类里。

DockManager : IDockManager,ISatisfyImportsExtension


DockManager除了继承ISatisfyImportsExtension以外,还继承了IDockManager,和ISatisfyImportsExtension关注组件的激活和配置不同,IDockManager定义了DockManager自身应该实现的功能,即布局相关,这是该组件的核心Feature,该部分细节可参看Dotspatial.Controls模块中对于IDockManager的解读。就目前而言,我们更关注DockManager组件的激活及配置。

DockManager.Priority = 0,很显然,DockManager将是最先被激活的组件,再看DockManager.Activate()实现细节:

《Dotspatial源码剖析-Dotspatial.DemoMap》

DockManager.Activate()

75行避免多次激活,76行很有意思,直接配置不行吗,非得从MEF组合容器里取一个实例?这恰恰是作者用心的地方,78-79的注释对这种费解的行为做了解释,当前DockManager是Default,如果用户没有自定义自己的DockManger,才会激活该组件,这有两层含义:对于主窗体而言,只存在一个DockManager是合理的,多个就比较奇怪,当然对于不同的窗体是可以有多个DockManager的。怎么区分当前的DockManager是针对哪个窗体定义的呢?嗯,看Shell属性。

《Dotspatial源码剖析-Dotspatial.DemoMap》

Shell

配对工作由MEF完成,如果想给A窗体定义DockManager,那么A窗体的导出声明和自定义DockManager.Shell属性的导入声明就必须配对。第二层含义则是允许用户针对MainForm自定义DockManager,自定义DockManager就可以舒服的直接进行属性配置。82行和75行是对应的,84-95有意思,这里是真正配置DockManager的地方。这里新创建了一个SpatialDockManager,这是真正的布局控件,是Dotspatial在SplitContainer基础上派生出来的布局控件,代码很简单,在SplitContainer的两个Panel中分别添加了两个TabControl,SplitContainer的事件提升至DockManager中,从而允许自定义Hook。但是,为了不影响DockManager的MEF配置,SpatialDockManager在定义时添加了新的特性声明:

《Dotspatial源码剖析-Dotspatial.DemoMap》

SpatialDockManager

有一个细节需要留意,Dotspatial.DockManager定义中添加了DefaultRequiredImport的特性声明,有什么用呢?留个伏笔吧。

HeaderControl : MenuBarHeaderControl,ISatisfyImportsExtension


类似于DockManager,这里先略去了MenuBarHeaderControl相关内容,只关注ISatisfyImportsExtension接口相关的实现内容。

HeaderControl.Priority = 1,这意味着HeaderControl将是第二个被激活的组件。

《Dotspatial源码剖析-Dotspatial.DemoMap》

HeaderControl.Activate()

43-51行做了和DockManager一样的事,避免多次激活,同时提供默认HeaderControl配置。

53-64行是构造了工具栏和菜单栏的实例,并进行了布局配置,66行特别有一行是针对工具栏和菜单栏控件的初始化设置(在这里有定义当控件加载完成时,记录并保存其布局信息至本地文件),而67行其实就在AppManager组件激活完成后从之前保存的布局信息(某个本地文件)中读取工具栏控件布局。70行是初始化菜单栏中的默认菜单,这里的内容很丰富,Dotspatial提供了很多丰富的菜单控件可供使用,从代码中可知:Msg.File_New,Msg.File_Open,Msg.File_Save,Msg.File_SaveAs,Msg.File_Options,Msg.File_Print,Msg.File_Reset_Layout,Msg.File_Exit,Msg.Add_Layer,Msg.Remove_Layer,Msg.Pan,Msg.Zoom_In,Msg.Zoom_Out,Msg.Zoom_To_Extents,Msg.Zoom_Previous,Msg.Zoom_Next,Msg.Zoom_To_Layer,Msg.Zoom_To_Coordinates,Msg.Select,Msg.Deselect,Msg.Identify。

StatusControl : IStatusControl,ISatisfyImportsExtension


StatusControl.Priority = 2,这意味着StatusControl将是第三个被激活的组件。

《Dotspatial源码剖析-Dotspatial.DemoMap》

StatusControl.Activate()

StatusControl的激活流程和之前DockManager,HeaderControl的套路是一样的,这里封装了一个SpatialStatusStrip实例,激活StatusControl的过程就是配置SpatialStatusStrip实例的过程。

SpatialStatusStrip控件派生自StatusStrip,并增加了新的功能,允许用户添加和移除StatusPanel控件,哈,这有是什么?!StatusPanel其实是一个Binder,或者理解为ViewModel(这里的类比是参考Web前端的相关概念),其属性的改变可以以文字形式显示在StatusControl中。

SatisfyImportsOnStartupExtension : ISatisfyImportsExtension


SatisfyImportsOnStartupExtension.Proority = 0,哈,这个神奇了,Dotspatial的设计者为用户考虑的非常周全,虽然,已经提供了默认的DockManager,HeaderControl和StatusControl,但是万一你没有正确的配置MEF,导致默认的实现也没能加载至组合容器咋办?嗯,好办,有SatisfyImportsOnStartupExtension来最后补救,不废话,看看激活该组件的逻辑:

《Dotspatial源码剖析-Dotspatial.DemoMap》

SatisfyImportsOnStartupExtension.Activate()

当这三个组件都没有找到,就去扩展组件里找,逻辑很简单,但是呢,不一定合理,这里是必须三个都没有找到,才去找替代品,Pi君的理解,理应是分别判断才是合理的。

抛开“补救”的逻辑,我们好奇的是Install的实现逻辑,其实,是用了Negut.Package去官网上下载需要的包而已,源码之下,了无秘密。不过,还是要为设计者的周全考虑点个赞。

IExtension解读


在DockManager,HeaderControl和StatusControl配置和激活完成后,接下来开始激活其他组件,所谓其他组件,就是实现了IExtension接口,且配置了MEF导出的组件。这个有很多,几乎涵盖了Dospatial.Control模块的全部组件,这是Dotspatial体现其良好封装的地方,值得细究。同时也是其他用户扩展Dotspatial功能的官方指定套路,各位看官,使用Dotspatial的方式并不限于这一种,但是如果能按官方指定套路来,一来是避免无谓的Bug,二来可以方便其他用户使用自己开发的功能,中肯之言哈~

IExtension由抽象类Extension直接继承,具体实例则实现Extension,目前的套路都是如此,并且基本是一个Feature,一个Project,当然也就是一个Plugin,现有Dotspatial的Plugins已经很多了,在DemoMap运行是都会被加载,但是不一定全都会用,类似于AOC容器的一般套路,Plugin的开发基本都是依赖于Dospatial.Core中那几个组件,所以想基于Dotspatial.Core搞开源Gis二次开发的小伙伴,可以自行研究已有Plugin的实现,反正是开源的嘛,如果懒得看(像我一般都去找书看,万不得已,不爱看源码),也可以等Pi君有时间写这个源码分析的后续部分,本来是按部就班一章一章的写,也可以根据Core的组件进行专题分析,看小伙伴们的需求吧,如有开发需要可以私信我,这样也可以帮助Pi君确定后续的写作计划,嗯,留个微信吧,有兴趣的一起来~

《Dotspatial源码剖析-Dotspatial.DemoMap》

点赞