解剖MFC程序

现代工业的特征之一就是分工,一旦能够分工,就会出现“术业有专攻”的局面。 建筑业需要钢材,冶金需要矿石、采矿需要设备,……,由此带来生机勃勃的现代文明社会。我们很难想象:建造一座楼房的时候,需要的钢材自己炼、需要的砖瓦 自己烧、需要的电梯自己造,会是一种什么感觉?MFC编程可能就是这样,如果MFC程序是一座楼,今天的MFC程序员必须亲历亲为,需要亲自盖起楼的主体 结构、形成层次、……、装修房间、完成布线等等一系列工作。因此,我们有必要对现在的MFC程序进行一次外科解剖手术,使MFC程序的构造能够实现分工。

我们从剖析一个典型的MFC多文档程序开始,典型的MFC程序,通常由一个文档框架主窗口(CMainFrame)以及一组“文档”模板构成。程序的形态 取决于CMainFrame,内容取决于其包含的文档模板。在代码结构上,主窗口、文档、文档框架窗口(CChildFrame)、View等类型对象耦 合在一起形成了一个通常意义下的MFC程序,对象耦合的过程,被MFC框架巧妙地封装了。因此,多少年来绝大多数场合下人们看到的是一个近乎“永恒”的代 码结构:

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_MsdnPluginSamplTYPE1,

RUNTIME_CLASS(CSampleDoc1),

RUNTIME_CLASS(CChildFrame1),

RUNTIME_CLASS(CUserCtrlView));

if (!pDocTemplate)

return FALSE;

AddDocTemplate(pDocTemplate);

pDocTemplate = new CMultiDocTemplate(

IDR_MsdnPluginSamplTYPE2,

RUNTIME_CLASS(CSampleDoc2),

RUNTIME_CLASS(CChildFrame2),

RUNTIME_CLASS(CUserCtrlView));

if (!pDocTemplate)

return FALSE;

AddDocTemplate(pDocTemplate);

...

pDocTemplate = new CMultiDocTemplate(

IDR_MsdnPluginSamplTYPEn,

RUNTIME_CLASS(CSampleDocn),

RUNTIME_CLASS(CChildFramen),

RUNTIME_CLASS(CUserCtrlView));

if (!pDocTemplate)

return FALSE;

AddDocTemplate(pDocTemplate);

CMainFrame* pMainFrame = new CMainFrame;

if (!pMainFrame ||

!pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

m_pMainWnd = pMainFrame;

pMainFrame->ShowWindow(m_nCmdShow);

pMainFrame->UpdateWindow();

只要你用过MFC,你一定接触过上述代码,从中你会感受到一张沧桑的面孔。这段代码贴切地显示出MFC框架的呆板、僵化。深入了解了这些代码背后发生的故事,你会发现解开MFC框架臃肿、僵化的玄机就在这里。从上面给出的代码中可以看出,MFC程序在初始化阶段通过AddDocTemplate(pDocTemplate);填 充了一个文档模板队列。完成文档队列填充后的工作就是实例化一个主窗口,然后创建该窗口并显示出来。这个初始化过程许多年来一直在以相同的模式重复着,就 像一个物理规律,几乎时时刻刻地发生在MFC的世界里。每增加一个文档模板,就需要在程序中增加一个文档类、一个文档框架窗口和一个或多个视图类,然后在 程序初始化阶段重新构造一个文档模板类,将其填充到文档模板队列中……你的程序至少需要增加3个类。如果要构造支持5个文档类型的MFC程序,得到的程序 结构将十分丰满,因为保守估计该程序也得包含15个以上的C++类。对初学者而言,会因此而极大地增强信心,因为他终于写出很大的C++程序了;然而对一 个大型的综合程序而言却是一个噩梦,一个系统如果要求100个用户视图、20个文档类型,用MFC框架开发就是件十分“恐怖”的事情。由此我们看到“文档 模板队列”是基于文档的MFC程序结构臃肿之症结所在。

然而,换个角度看基于文档的MFC程序的结构就很简单:无外乎一个文档模板队列,以及一个支撑文 档显示的主框架窗口,许许多多其它对象均属于“亚”层次的二级结构元部件。结构臃肿的症结既然已经暴露,化解的方案也就呼之欲出了。从程序初始化的过程中 可以看到,文档模板队列的填充,是通过调用AddDocTemplate完成的,这个调用的参数的数据类型是CMultiDocTemplate*。 因此,一个文档模板并非一定是程序内部提供的,也并非必须在程序初始化过程中填充。认识到这一点极为重要!即使在同一个程序内部,不同文档模板之间的关联 性一般也很弱,这一点表明,文档模板完全可以从程序结构中剥离。实现文档模板与应用程序结构剥离,意味着实现功能与程序主体的分离,即:应用的内容可以单 独开发,根据需要加载。剥离机制面临的技术问题是:是否存在一个可接受的途径使得外部构造的模板可以根据需要插入到一个特定程序的模板队列中。由此可见, 调整文档模板队列的填充机制,是化解MFC工程臃肿的良好策略。此外,MFC程序的主线程窗口的匹配,也是在初始化过程中通过:

CMainFrame* pMainFrame = new CMainFrame;

if (!pMainFrame

|| !pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

m_pMainWnd = pMainFrame;

完成的,m_pMainWnd是个CWnd*类型的指针, 即:程序的主线程仅仅需要一个同线程窗口对象的指针,因此,主窗口对象也不一定由程序主体内部提供。主窗口的创建,同样可以从程序中剥离出来,认识到这一 点同样重要!对于一个基于文档的MFC程序来说,主窗口的主要责任只是为一系列文档模板形成的体系提供可供显示的框框,我们完全没有必要将程序和一个主窗 口死死的焊接在一起,更没必要将一组固定的模板固化在一个特定的程序之中。我们看到:如果实现文档模板从程序中剥离出来,那么全部剥离出来文档模板将形成 一个“社会”;如果能够将程序的主窗口也剥离出来,那么剥离出来的主窗口将形成文档社会中的一个个的建筑或组织形态。而这时候,所谓的应用,就是根据特定 的需求组织文档模板形成解决方案的一种策略。此时主窗口是特定场合下组织模式的体现,而“文档模板”就是组织成员。因此,我们需要挖掘出一种灵活的文档组 织策略,以使得现在的MFC程序员摆脱僵化、臃肿的开发模式。

构造最小的MFC程序

现在,我们就开始一个精简MFC程序的过程吧。我们记如下类型代码段为(a):

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_MsdnPluginSamplTYPE,

RUNTIME_CLASS(CMsdnPluginSampleDoc),

RUNTIME_CLASS(CChildFrame),

RUNTIME_CLASS(CUserCtrlView));

if (!pDocTemplate)

return FALSE;

AddDocTemplate(pDocTemplate);

而将如下类型代码段记为(b):

CMainFrame* pMainFrame = new CMainFrame;

if (!pMainFrame

|| !pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

m_pMainWnd = pMainFrame;

pMainFrame->ShowWindow(m_nCmdShow);

我们的思路是,首先在常规MFC多文档程序中删除上述与主窗口和文档相关的创建代码,使原生应用程序空无一物。然后,创建新的工程,在其中完成主窗口和文档的创建工作;最后一步自然是把它们装回去。

先进行第一步,我们按照常规的步骤生成一个标准的MFC多文档程序,将(a)、(b)对应的代码删除掉,再删除所有与(a)、(b)相关的所有文件(注意 保留应用程序对象对应的文件),并适当调整使这个程序能够正确编译。经过以上处理,将得到一个最小的MFC程序,这个程序中除了一个应用程序对象和一个 CAboutDlg之外,什么都没有。因此,运行时什么都不会发生。

现在,一个有意思的问题出现了:如何寻找一个合适的途径,使得(a)、(b)中的代码合理地“回归”到我们刚刚得到的最小的MFC程序中?我们回顾一下: (a)的目的仅仅是要填充一个队列。如果忽略具体的软件需求,只要是一个有效的CMultiDocTemplate*指针,只要这个指针能够顺利地传递到 (a),填充就能顺利进行,并且填充过程不具备排斥性。这意味着什么?这意味着如果能找到合适的办法,你就可以填充任意多数量的文档模板!

(b)的目的是实例化一个窗口对象,创建、加载并匹配给MFC的主线程,但并不局限于规定的窗口类型。虽然这个过程对每个程序的运行时只有一次,正像一个 人或者一个公司可以根据意愿选择自己的住所或办公地一样,如果找到合适的办法,一个程序完全可以根据场合匹配适当的主窗口。为了实现(a)、(b)回归到 原来的程序,我们需要在原始程序的初始化工程中嵌入两个“回调机制”:一个用于加载(a),另一个用于加载(b)。

现在,可以形成策略了:我们需要两类“对象”,一类用来解决(a),另一类用来解决(b)。同时,我们还希望这两类对象能够独立于程序存在,需要的时候能 够在程序的合适位置创建即可。从纯粹MFC的角度解决所提到的两类对象是完全可能的,但这样会带来很大的局限,也十分可惜。因此,结合现代的软件技术,我 们应该从更广泛的意义上考虑这个问题,由此挖掘出MFC框架与现在主流软件技术的强有力的结合点。我们着重考虑基于COM、.NET、Java等框架考虑 实现这两种对象的可能性;本文中,我们工作的基础是.NET。

动态加载原理

动态加载,是个应用很广泛的程序技巧,COM、.NET、Java均存在软件动态加载的机制。对任何一个.NET程序,CLR提供两种途径检索可动态加载 的对象——一个途径是全局的,这类对象必须在全局对象缓冲区中注册;另一个途径是局部的,通过程序的配置文件指定。

.NET框架为每个程序提供一个配置文件,这个文件的名字是:“程序名.exe.config”,这个文件实际上就是个标准的XML文件。一个典型的配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<runtime>

<assemblyBinding

xmlns="urn:schemas-microsoft-com:asm.v1"

>

<probing

privatePath=

"bin;usercontrol;component"

/>

</assemblyBinding>

</runtime>

</configuration>

CLR通过<probing privatePath="bin;usercontrol;component "/>中指定的信息发现当前程序的局部组件路径。可以给privatePath重新赋值以指定新的局部组件检索路径,不同的路径用‘;’分隔。也就 是说,除了全局对象缓冲区中注册的对象外,只有程序所在目录以及子目录bin、usercontrol、component中的动态链接库中包含的局部. NET对象才能够被该程序加载。具体的加载实现如下:

Assembly* m_pDotNetAppAssembly =

Assembly::Load(S"LibName");

Type* m_pDotNetAppType =

m_pDotNetAppAssembly->GetType(

S"ObjectID",true,true);

MethodInfo* mi =

m_pDotNetAppType->GetMethod(S"SomeMethod");

Object* m_pDotNetAppServer =

Activator::CreateInstance(m_pDotNetAppType);

String* p[] = new String*[1];

p[0] = S"SomStrParam";

mi->Invoke(m_pDotNetAppServer,p);

上述代码给出了动态加载包含于LibName.dll中名称为“ObjectID”的.NET对象的一般方法,并显示了如何调用这个动态对象中的一个方法("SomeMethod")。其中关于Invoke的更详细的信息,请参考.NET FrameWork SDK技术文档。

插件的一般理论

插件,通常是指具有相同“虚”行为规范的一类动态可加载对象。插件事实上是一种委托行为,当程序需要一种可以预见“粗略”轮廓而无法确定具体功能的行为或 对象描述时,通常将类似行为委托给一个后期介入的对象进行解析,此时就有了插件。随着软件技术的不断进步,人们已经不再满足于开发仅有有限静态功能的软 件,一个更合理的软件构思是:使得软件在一定的意义下可以与来自用户的新的需求对接。这一点客观上要求软件主体必须有一个可“暴露”的对象模型,同时这个 模型应该能够接受来自用户的新的软件拓展对象;而且这个模型具有一个虚拟的拓展规范。

我们以MFC程序为例对此进行简要的说明。如果希望一个基于文档的MFC MDI程序支持虚拟拓展,首先必须对拓展的目的进行清晰的描述,其次是给拓展行为制定规范。假设计划该MDI程序支持一组新的可加载文档类型,那么这组新 的文档应该具备什么样的“虚”的行为规范?所谓“虚”的行为规范,本质上相当于一个C++类对象中的一组虚函数,或者相当于一个虚接口中的一组方法。当一 个对象重载了对应的C++类或重新实现了给定的虚接口,这个对象就有可能被另一个系统接纳。如果我们计划为给定的MDI程序实现一组新的可加载文档,并且 希望这类文档加载后,能够与原程序无缝地集成并在指令上能够与原模型实现对接(例如,主窗口可以显示一组菜单,菜单中能够显示一些常规的指令与新文档进行 交互等等),那么我们必须在主程序中虚设这些指令的执行(例如,用一个“虚”指针调用一个指令,只要确保运行时指针是有效的),而在真正的运行时产生新的 对象与虚的交互进行对接(即将一个虚的指针有效化)。为实现这个目的,主程序必须维护一个加载对象的字典,或与之相当的机制,使得程序主体能够判断出新加 载对象的属性,这样就能够正确地定位虚的行为发生的时间以便将其对应到一个确实的对象之中。

具体地说,假设一组扩展行为的名称为:Action1、Action2、Action3,那么我们需要一个类(或一个COM、Java接口等等):

class SomePlugInObj

{

// Overrides

virtual void Action1 ()=0;

virtual void Action2 ()=0;

virtual void Action3 ()=0;

}

我们希望每个新加载的文档能够包含一个SomePlugInObj对象的指针,根据需要,这个指针指向的对象可以是SomePlugInObj的派生对 象。这一点可以确保扩展行为的多态性,运行时程序可以通过加载的文档得到这个指针,并在适当的时刻(例如选择菜单、关闭文档等等)以公共的方式触发 SomePlugInObj的关联行为。如果是这样的话,新的文档就自然地被主系统接纳了。

现在的问题是,我们必须在给定的程序之外实现SomePlugInObj及其宿主文档。根据动态加载原理,我们可以实现一个基于.NET的组件库,在这个 库中创建一个.NET对象,使得该对象中维护一个SomePlugInObj对象的实例;然后根据需要实现运行时加载该.NET对象,使得需要的 SomePlugInObj能够正确传递到原生程序。一般来说,你可以在新的组件库中派生SomePlugInObj,重载其中的全部方法,这样用户的定 制化功能就可以自然地导入到原生程序之中。采用.NET技术的优势在于,你可以将SomePlugInObj的方法进一步“虚化”到一个.NET对象中, 例如,如果基于.NET的组件库为SomeLib.dll,我们可以在其中派生新的类:

class DotNetSomePlugInObj : public SomePlugInObj

{

gcroot<SomeLib::SomeLibObject*> m_pDotNetObj;

virtual void Action1 ()

{

pDotNetObj-> Action1();

}

virtual void Action2 ()

{

pDotNetObj-> Action2();

}

virtual void Action3 ()

{

pDotNetObj-> Action3();

}

}

此时,我们可以构造一个SomeLib.dll中的.NET对象SomeLibObject,其中包含三个可重载的虚方法——Action1、 Action2、Action3,那么我们就实现了SomePlugInObj对象的.NET“虚化”。进一步,用户就可以基于其它.NET语言 (VB.NET、C#等等)利用.NET框架的重载机制(在其它.NET支持的语言中以SomeLib::SomeLibObject为基类派生新的. NET对象),提供不同版本的m_pDotNetObj,从而为原生软件带来更加广泛、灵活的扩展途径。

检索、管理可加载组件是支持插件的至关重要的环节,为此我们必须对主程序构造提出必要的先决条件,也就是说,程序主体必须具备接纳机制。通常程序主体必须 明确地具备检索可加载对象的能力——这是程序具备可拓展性的最起码的要求。COM机制通常利用COM组件的范畴(Category)机制实现组件接口的枚 举、检索;.NET框架还可以提供更灵活的检索机制。由于.NET程序的组件是可配置的,即我们可以在程序所属的目录中建立子目录用来存储可加载对象的包 容库,以此方式可以灵活地进行组件分配、部署;因此,.NET非常适合实现可加载插件的检索机制。值得考虑的方案是通过.NET程序的配置文件指定插件对 象的包容组件库存储的目录,进而制定插件的管理方法。此时如果插件包容库已经进入指定的目录,则其中的组件就可以被原始程序自然地检索了。描述基于“动态 插入目的”开发的组件的常规做法是用XML文件技术。这一做法的好处是使得描述局部化了,避开了复杂的“注册表”,而且容易实现。我们可以为每个组件提供 一个相关的XML文件,其中描述组件所属的组件库、程序集、对象名以及其它相关信息,然后将全部这类的XML文件存储在同一个目录中。只要能够合理的检 索、枚举这些同目录下的XML文件,就可以开发出灵活、强大的插件管理器。一个典型的插件管理器的实现可以参考我们提供的样板程序,根据这个样板,你完全 可以开发自己的插件管理器。

一旦明确了插件的工作原理,那么插件的运行时管理、调度就是一个关键的课题了。事实上,主程序系统就是一个插件的调度、管理枢纽中心.程序对象模型如何 虚、实结合是个艺术性的问题,如果你希望一个有价值的软件行为有延续的必要、或局部调整的可能、再或者打算在此留下一些“头绪”以便其他人续写,此时一个 巧妙的做法是调用一个“虚函数”或触发一个事件。因此,支持插件的代价也就暴露了,程序中会因插件而出现许多虚的实现和虚的调用,程序的危险因素增加了, 健壮、稳定就成了程序成败的关键因素。解决上述问题的办法是,使程序主体能够识别所有虚拟行为的描述,无论有多么“虚”,都不能失去规范。因此,维护一个 动态虚拟对象的字典是绝对必要的,只有这样,你才能在一个运行时刻确保正确地解析虚拟对象的虚拟行为,并将这些“虚”的指令行为委托给一个“实”的对象, 这就是虚实结合。

构造第一个可动态加载的主窗口插件

我们现在的目的是:将主窗口的匹配过程实现为一种插件机制,为此,我们需要建立一个支持托管扩展的MFC动态链接库工程。在这个工程中,添加一个名为 “AppObject”的托管C++对象,在其中添加一个名为“ConnectMainFrame”的方法。根据动态加载原理,可以在最小MFC程序的初 始化过程中实现对该对象的动态加载。注意,你的最小MFC程序必须修改,使其支持托管扩展。对象的动态加载实现的位置是前述的(b)对应的代码位置。第一 步是在“ConnectMainFrame”中写些简单的代码,然后看看程序运行的效果。例如,写一个最简单的实现过程:

voidAppObject::ConnectMainFrame ()

{

AfxMessageBox(_T(“Hello, MFC!”));

}

如果你成功地完成了上述步骤,你就已经走完了正确的一步并且可以进行下一步了。现在,在创建的动态链接库中添加一个CMainFrame对象。一般的方法 是:建立一个辅助的MFC工程,然后将其中有关CMainFrame的代码以及相关的资源文件全部添加到你的动态链接库工程之中。经过适当的调整,使得这 个工程能够被正确地编译。这是个比较可行的手工方法,但相对繁琐且容易出错。一个行之有效的办法是实现一个集成在Visual Studio IDE中的Wizard自动的完成上述工作(相关的Wizard在附赠的光盘中)。在ConnectMainFrame方法实现中,我们可以将代码(b) 嵌入到其中。这样,最小的MFC程序就成功地与一个CMainFrame对接了:

void AppObject::ConnectMainFrame ()

{

CWinApp* pApp = AfxGetApp();

AfxSetResourceHandle(theApp.m_hInstance);

CMainFrame* pMainFrame = new CMainFrame;

theApp.m_pAppObj = this;

if (!pMainFrame

|| !pMainFrame->LoadFrame(IDR_MAINFRAME))

return;

pMainFrame->ShowWindow(pApp->m_nCmdShow);

pMainFrame->UpdateWindow();

}

我们可以看到,主窗口插件的目的就是将一个无窗口的程序通过一个实体表现出来,使得运行时具有程序交互的基础。

构造可动态加载的文档模型

与实现主窗口插件的原理一样,我们还可以构造另一类基于.NET的插件对象用于加载文档模板。一个包含文档模板的.NET组件库与主窗口一样,也是一个基 于MFC框架的动态链接库。手工建立这个库的过程与主窗口工程的建立类似:第一,需要一个支持托管扩展的MFC动态链接库工程;第二,需要一个辅助的基于 文档的MFC程序工程;第三,将辅助工程中与文档相关的代码全部添加到动态链接库工程中。这个过程同样可以实现为一个集成的Wizard(在光盘中提 供)。

实现文档模板动态插入的代码如下:

void ActiveDocTemplate(String* strExtImpl)

{

if(!g_pDotNetExtImpl)

{

...

CWinApp* pApp = AfxGetApp();

AfxSetResourceHandle(theApp.m_hInstance);

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_DocumentXDocTemplate3TYPE,

RUNTIME_CLASS(

CDocumentXDocTemplate3Doc),

// custom MDI child frame:

RUNTIME_CLASS(CChildFrame),

RUNTIME_CLASS(

CDocumentXDocTemplate3View));

if (!pDocTemplate)

return;

CString strFileInfo,strFileExt;

pDocTemplate->GetDocString(strFileInfo,

CDocTemplate::filterName);

pDocTemplate->GetDocString(strFileExt,

CDocTemplate::filterExt);

g_pDotNetExtImpl->

m_DocInfoDictionary[strFileInfo] =

strFileExt;

g_pDotNetExtImpl->m_pCurDocTemplate =

pDocTemplate;

pApp->AddDocTemplate(pDocTemplate);

}

}

从上述代码中可以看出,文档模板插件的目的就是实现一个从外部构造的模板,自然地插入到应用程序对象的文档模板队列以使得程序支持新的文档类型。

构造基于插件的应用系统

回到最小的MFC程序构造环节,我们看到,我们并不是要刻意挖空一个程序的内容。这样做的目的是寻找一个解决MFC机制臃肿的方案。结果,我们得到的是一 些碎片。现在我们找到理由了,根据插件系统的一般原理,我们看到了这些碎片重新合成的可能性。有了一个最小的MFC程序,一类主窗口插件、一类文档模板插 件,问题就很清晰了。那就是:如何将这些东西拼装起来,以形成一个真正的程序。

现在,让我们将剥离后的碎片重新合成,以形成一个系统吧!首先:我们需要一个具有组织、 加载能力的应用程序。这样一个完整的例子可以参考我们提供的案例代码,彻底的方案是将上述机制形成一系列Wizard并与Visual Studio集成。为此我们提供了所有这些相关的工作(在随机光盘中提供);一旦有了程序,合成工作的基础就建立了。

第二步:我们需要一个配置文件以便指定特定应用需要的主窗口插件以及文档模板队列的检索位置。为此,一个最好的做法就是直接利用.NET程序的默认配置文 件。对.NET程序而言,配置文件相当于一个局部的注册表,不仅可以指定组件的局部检索路径,而且可以指定其他的定制化信息。为了合成工作的需要,在配置 文件中增加如下结点:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<program

MainAppType = ".net"

MainAssemblyLib = " testmainframe"

MainAssemblyCategory= "testmainframe"

MainFrameAssembly =

"testmainframe.AppObject"

>

</program>

</configuration>

经过上述调整后,程序的配置文件事实上为合成工作描述了一个合成方案。一个程序可以有任意多个描述方案,当面对不同需求的时候,用户可以形成不同的方案。 由于每个.NET程序可以存储在不同的本地目录中,因此,你可以为每个本地目录匹配一个不同的合成方案以此实现应用系统的多态性。

第三步:系统通过MainAssemblyLib = "testmainframe"指定主窗口插件所在的.NET程序集名称;MainFrameAssembly = "testmainframe.AppObject"指 定加载主窗口的.NET对象的ID值。这个方法与JavaBean采用的技术十分相似,给软件配置带来了十分灵活的机制。由于主窗口的实现已经与程序分离 了,因此,开发人员可以根据需要提供多个主窗口(理论上可以实现任意多个主窗口),甚至直接使用已存在的主窗口对象。而且,由于实现主窗口的动态链接库是 基于.NET框架的,因此,你甚至可以用其他.NET语言重载相关的对象。例如,如果负责加载主窗口的.NET对象中存在有针对性的“虚函数”,那么其他 开发者甚至用户就可以重载这个对象,以实现进一步的程序定制化。一般来说,我们可以将主窗口与对应负责加载主窗口的.NET对象匹配在一起形成一个对偶 对,就可以实现“通过相应的.NET对象驱动主窗口”这一驱动模型。一旦运行时可以将主窗口关联的对象模型委托给其他对象模型系统,你的程序主体就具备了 强大的“二次开发”能力。著名的VBA与Visual Studio for Application以及Visual Studio Tools for Office就是通过类似的原理实现的。

第四步:与主窗口对象不同的是,我们需要加载一个文档模板队列。因此,需要提供一个文档模板的检索机制。为此,可以在程序所在的目录中建立一个子目录 DocTemplate,我们希望相关的文档信息保存在这个目录中;并且每个模板通过一个XML文件进行描述,其中指定实现文档模板的组件库以及负责加载 模板的.NET对象ID值。当然,我们也可以建立一个全局目录,负责保存共享的模板对象库。这个实现在想法上很类似开源集成框架eclipse,此时每个 文档类型通过插件的形式集成到一个特定场合中的主窗口框架之中。例如,如果对象库DocumentXDocTemplate1.dll包含一个文档模型, 且加载模型的.NET对象的ID值是DocumentXDocTemplate1.DocTemplate,那么描述这个模型的XML文件就是象下面这样 的:

<?xml version="1.0" encoding="utf-8" ?>

<Template

DocViewID="DocumentXDocTemplate1#

DocumentXDocTemplate1.DocTemplate"

DocObjID = ""

ExtDocObjID = ""

>

</Template>

所有的描述文档模板的xml文件均保存在DocTemplate目录中,因此程序就具备了枚举文档模板的能力。在模板描述文件中,通过Template节 点的DocViewID属性的值指定模板对应得组件库。这个值的格式为“xxx#yyy”,其中“xxx”指定对象库,“yyy”指定加载模板的对象 ID。例如上面的XML文件显示组件库DocumentXDocTemplate1.dll中.NET对象DocTemplate负责加载文档模板的工 作,即对象DocTemplate中存在一个方法,这个方法将在运行时被宿主程序激活,并将(a)中描述的代码嵌入到宿主程序并执行。从这里我们可以看 到,程序支持的文档类型并不一定必须在初始化的环节插入文档模板队列,需要的时候插入即可。

完成上述步骤后,一个程序系统就形成了。那它可以做什么实际的工作呢?当它在不同的运行环境下加载了相应的主窗体和文档模板时,它的具体功能才体现了出 来。你可以在需要时加载两个完全不同的文档模板,表现为两个完全不同的运行态。换句话说,如果有一个通用的应用框架基础可以一致地实现异构编程框架支持、 功能逻辑的动态演化机制,那么开发人员就可以在这个基础之上随心所欲地把握各种可用的技术资源,专注于丰富功能的设计和构造,而不用从头做起。按照我们的 观点,基于文档的程序,是个无内容的程序,只有这样,这个程序所支持的内容才可能达到最大的饱和度。我们的软件哲学是:在程序的具体应用场合中赋予程序以 内容——首先我们希望形成一个描述方案,即制定程序的配置文件;其次,主窗口是外接的,即主窗口被实现成一个外接插件;不仅如此,所有的文档模板均被实现 成插件,这样就使得不同场合不同配置成为可能。