使用.NET Framework 2.0创建自定义设计器

本文讨论了:

理解设计器和服务
创建一个主机窗体
构造和使用工具箱
加载和管理设计器
本文使用了如下技术:

Windows窗体技术和Visual Studio 2005开发工具
目录

基于服务的扩展
DesignSurface和DesignSurfaceManager 类
The Hosting Form
工具箱
多个DesignSurface
DesignerLoaders
CodeDomDesignerLoader
总结
微软的.NET Framework 1.0版中提供了一个非常灵活的设计时架构,但实际上并没有提供用于创建和承载设计器的代码。所有的承载(hosting)逻辑都在Visual Studio.NET中执行,需要第三方支持才能够重写这些复杂的逻辑代码。然而所有这些现在已经变了。 .NET Framework 2.0引入了一套类的集合,用于承载设计器。

              

图 1 Runtime

要理解.NET框架的设计器是如何工作的,掌握设计器是如何使用的很重要。设计器仅在设计期间存在,且能够关联通常在运行期存在的对象。.NET框架关联这两个对象(设计器对象和其关联的对象)并为设计器对象提供管道用于增加运行期对象的行为。在运行期,一个窗体和一个按钮通过父子关系关联起来(如图1所示)。没有其他对象控制这些控件(窗体和按钮)的生命周期。

在设计期间,这张图片看起来稍稍有些复杂。窗体和按钮两个控件都有与之对应的设计器。这两个对象也被关联到一个包含它们的主机容器里(如图2所示)。主机容器也提供一些服务—例如,在设计期用于选择一个或多个对象的选择服务,用于显示消息、调用帮助的用户界面服务,而且它还与对象或设计器使用的开发环境进行交互。

主机容器有许多职责,包括创建组件、把它们绑定到设计器、给由它维护的组件和设计器提供相关服务等。它从某种稳定的状态加载设计器并保存它们回到该状态(saves them back to that state)。设计器主机还提供了用于取消操作和剪贴板功能的逻辑以及其他许多设计器依赖的服务,用于提供一个完备的设计期运行环境。

图 2


图3 设计器主机

主机容器也能够维持设计器的状态。要做到这一点,它要使用设计器加载器。如图3所示,设计器加载器使用串行化机制依次串行化组件。

带服务的扩展

.NET框架下的设计器架构是可扩展的。对于这一扩展性,其关键在于,上文提到的服务提供了一种能力,用于增强设计器架构适用不同设计器的功能。服务是任何一个可以通过使用类型信息查询得到的对象。特别地,你可以定义一些抽象类或接口来表示服务,然后再提供该服务的实现。你可以从一个叫做服务容器的对象中添加或删除服务。设计器的主要主机接口IDesignerHost就是一个服务容器。一个服务就是一个特征,可以在不同场合编写的组件之间共享。鉴于此,当使用或创建服务的时候,你必须遵循特定的规则。

服务是没有保证的。无论何时你通过调用GetService方法请求一个服务,你都必须检查方法返回的是不是一个有效的对象。并不是所有的服务适用于所有平台,而且,服务一旦可用,在将来的某个时刻可能会不可用。因此,你的代码要适当地减少功能,通常是使需要服务的特征不可用,以防止服务确实变得不可用。

如果添加了一个服务,就要记得当你的设计器不可用时删除它。设计器主机可能会不时地销毁和重建你的设计器。如果你没有适时删除服务,那么以前通过该服务创建的设计器就会一直占用内存。

DesignSurface 和 DesignSurfaceManager

.NET 框架2.0引入了两个类用于承载(hosting)设计器和为设计器提供相关服务,即DesignerSurface和DesignSurfaceManager。DesignSurface是一个让用户感觉像一个设计器的东西;它就是用户操作的用于改变设计期行为的用户界面。DesignSurface可以用作一个单独的设计器,也可以和DesignSurfaceManager共同提供一个通用的承载多个DesignSurface对象的应用程序的实现。

如图4所示,DesignSurface对象自动地提供一些设计期服务。大多数服务可以在服务容器中重写。替换一个不可被替换的服务是不合规定的,因为它们的实现都是互相依赖的。注意,所有添加到服务容器的实现IDisposable接口的服务,在设计器服务DesignSurface对象销毁的时候也将会被销毁。

除了默认提供的服务,DesignSurface还提供了IDictionaryService接口,通过一个组件的站点来使用。这种服务对每个组件都是唯一的,它提供了一个通用的名/值对字典,用于存储有关组件的任何数据信息。它不可能取代这些服务,因为在每个站点基础上无法取代这些服务。 

DesignSurfaceManager 规定为设计器的容器。它提供了一个通用的服务,用于处理设计器之间的事件路由、属性窗口和其他全局的对象。使用DesignSurfaceManager不是必须的,但是如果你想使用多个设计器窗口,那么推荐使用。

DesignSurfaceManager也会自动提供一些设计期服务,如图5所示。在受保护的服务容器特性中,每个服务可以通过替换其值来重写。正如DesignSurface,所有添加到服务容器中的实现IDisposable接口的DesignSurfaceManager服务对象,在设计器应用程序销毁的时候也将会被销毁。

IDesignerEventService接口是一个特别有用的服务。它能够让应用程序知道窗体何时激活。IDesignerEventService接口提供了一组设计器的集合,而且是全局对象,如属性窗口,能够侦听到选择事件的单一场合。

承载窗体

为了展示承载一个设计器是多么简单,我编写了如下代码,用于创建一个基本的Windows窗体设计器并显示它。

// Create the DesignSurface and load it with a form
DesignSurface ds = new DesignSurface();
ds.BeginLoad(typeof(Form));
// Get the View of the DesignSurface, host it in a form, and show it
Control c = ds.View as Control;
Form f = new Form();
c.Parent = f;
c.Dock = DockStyle.Fill;
f.Show();


在这个代码片断里,我加载了一个带有Form的DesignSurface。同样地,你可以为DesignSurface加载任何组件,只要这些组件包含一个根设计器可用于它(DesignSurface)。例如,你可以加载一个用户控件或者一个组件来代替。

用于该文章的下载代码提供的示例中承载了四个不同的根组件:窗体、用户控件、组件、和MyTopLevelComponent(一个图形设计器)。当你运行这个示例的时候,一个主机界面会打开。该接口包含一个工具箱、一个属性浏览器、一个用于承载设计器的Tab控件、一个输出窗口和一个解决方案管理浏览器,如图6所示。从菜单选择文件|新建|窗体会打开一个带有Window窗体设计器的设计器主机。这个基本上使用了我刚才展示你大家的用于加载设计器的代码。不仅仅加载了一个窗体,示例应用程序还示范了如何加载一个用户控件或组件。 


图6 承载Window窗体设计器

要创建一个根组件,就要创建一个实现IRootDesigner接口的设计器,紧接着把组件和设计器关联起来。根组件的View属性指定了即将呈现给用户的视图。

由DesignSurface提供的主要服务之一就IDesignerHost接口。它是主要的接口,用于提供设计器和控件对类型信息、相关服务和相关事务的访问。它还用于创建和销毁相关组件。要向我之前创建的Windows窗体中添加按钮控件,我需要做的就是从DesignSurface中得到IDesignerHost服务,并使用它来创建按钮,如图7所示。

IToolboxUser接口指定设计器支持从工具箱中添加控件。这意味着如果你拥有一个继承自ToolboxService类的工具箱,你就可以使用IToolboxUser接口来向根组件中添加控件。例如:

/* Add a Button to the Form using IToolboxUser */
IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost));
IToolboxUser itu = (IToolboxUser)idh.GetDesigner(idh.RootComponent);
itu.ToolPicked(new ToolboxItem(typeof(Button)));

示例程序中,当通过双击工具箱中的项使控件添加到自定义根设计器中时,根设计器视图更新以显示一个饼图,如图8所示。单击超链接GraphStyle会使视图变成一个柱状图。

图8 自定义根设计器更新

 

工具箱

MyRootDesigner类继承自IToolboxUser接口。它包括两个方法:GetToolSupported和ToolPicked。你可以使用GetToolSupported方法来筛选能添加到设计器的项。ToolPicked方法最终要调用ToolboxItem的CreateComponents方法(正如其名称所暗示的,它负责创建组件)。

既然我已经向设计器添加了一些控件和组件,那么让我们仔细研究一下如何应用一个工具箱。首先,工具箱类要实现IToolboxService接口——该服务被添加到服务容器中,能够被任何使用者调用。IToolboxServiceru接口的主要函数如图9所示。

为了允许使用鼠标或键盘来向设计器中添加工具箱中的项,示例中的工具箱委托了KeyDown和MouseDown事件。按Enter键或双击鼠标时,将调用IToolboxUser接口的ToolPicked方法。示例展示了在鼠标单击事件发生时如何把工具箱项序列化到数据对象(DataObject)和进行拖曳操作。当鼠标键释放时IToolboxUser接口的SerializeToolboxItem方法会被调用,对应的工具箱项会添加到设计器中。

当一个新的控件或组件添加到设计器中时,你可以通过实现INameCreationService接口为控件提供一个自定义名称。示例应用程序展示了一个该服务使用CreateName、ValidateName和IsValidName方法的例子(in action)。

多个DesignSurface

 当进行管理多个DesignSurface时,使用DesignSurfaceManager是一个不错的主意。它使得管理管理这些DesignSurface更加容易。(注意:DesignSurfaceManager服务也对DesignSurface可用)

调用DesignSurfaceManager的CreateDesignSurface方法会转而调用CreateDesignSurfaceCore方法。你可以重载相应函数来创建一个自定义的DesignSurface和相关服务。通过在HostSurfaceManager类中重写相应函数,示例应用程序创建了一个自定义的主机界面(HostSurface):

protected override DesignSurface CreateDesignSurfaceCore(
IServiceProvider parentProvider)
{
  return new HostSurface(parentProvider);
}

然后,你可以在HostSurfaceManager类中实现ActiveDesignSurfaceChanged事件以更新输出窗口,事件代码如下所示:

void HostSurfaceManager_ActiveDesignSurfaceChanged(
object sender, ActiveDesignSurfaceChangedEventArgs e)
{
ToolWindows.OutputWindow o =
this.GetService(typeof(ToolWindows.OutputWindow)) as
ToolWindows.OutputWindow;
o.RichTextBox.Text += "New host added./n";
}

DesignerLoaders 至此我已经创建了诸多DesignSurface对象、承载的设计器(hosted designers)、添加的控件,实现了一个工具箱并添加和访问了相关服务,如输出窗口。下一步工作是持久化设计器。正如你期望的那样,设计器加载器负责从某种持久的状态加载设计器。简单灵活的设计器加载器需要非常少的要求便可以使用。实际上,使用一个one-line设计器加载器创建System.Windows.Forms.Form的一个实例很简单,因此你可以使用它创建Windows窗体的一个实例。

除了加载一个窗体设计器,设计器加载器还负责保存设计器。因为保存是一个可选择的行为,所以设计器加载器侦听来来自设计机主机的变化事件并自动根据这些事件来保存设计器的状态。

.NET框架2.0引入了两个用于编写自定义加载器的新类:BasicDesignerLoader和CodeDomDesignerLoader。示例应用程序示范这两种加载器的实现方式。早些时间我举例说明了通过组件的类型来加载带有根组件的DesignSurface。然而,如果使用一个加载器,那么它应当用于加载设计界面(design surface)。BeginLoad代码片断,就像下面所示,当你使用加载器时会需要的:

// Load it using a Loader
ds.BeginLoad(new MyLoader());
 设计器加载器负责加载DesignSurface中的根组件并且创建任意组件。当创建一个新窗体或其他任何根组件时,加载器很简单地加载它。相比较,当从一个代码文件或某种存储对象中加载时,加载器负责解析文件或存储对象并重新创建根组件及其他任何必要的组件。

.NET框架定义了一个叫做DesignerLoader抽象基类,用于从持久性存储中加载和保存设计器。该基类是抽象的以使持久性模型的任何类型都能够使用。然而,这增加了实现该类的复杂性。

BasicDesignerLoader类提供一个完整且通用的设计器实现,精简了任何与持久性格式化相关的信息。类似DesignerLoader,它也是抽象的要,不规定任何关于持久性格式化信息。然而,BasicDesignerLoader能够处理标准的工作,如知道何时保存,知道如何重新加载及跟踪设计器的改变通知。它的特点还包括对多加载依赖性的支持、修改一点点就要指示需要保存改变的跟踪以及推迟停机时间以加载相关支持服务。

图10所示的相关服务,通过BasicDesignerLoader添加到设计器主机的服务容器中。至于其他服务,你可以通过在受保护的LoaderHost属性中编辑它们的值来改变可替换的服务。示例程序实现了一个持久化XML格式的BasicDesignerLoader。要查看它是如何工作的,选择文件|类型|BasicDesignerLoader。然后通过选择文件|新建|窗体来创建一个新窗体。要查看生成的XML代码,选择视图|代码|XML。由应用程序生成的XML代码和下面类似:

<Object type="System.Windows.Forms.Form, System.Windows.Forms,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="Form1" children="Controls">
<Property name="Name">Form1</Property>
<Property name="DataBindings">
<Property name="DefaultDataSourceUpdateMode">OnValidation</Property>
</Property>
<Property name="ClientSize">292, 273</Property>
</Object>
PerformFlush和PerformLoad是BasicDesignerLoader的两个抽象方法,你需要分别实现它们以用于序列化和反序列化。

CodeDomDesignerLoader

设计期序列化机制通过生成源代码来实现。任何代码生成方案的挑战之一就是多语言的处理。由于.NET框架被设计成支持多语言一起工作,所以我也想让设计器支持几种语言。有两种方式来处理。第一种方式就是需要每种语言的卖主为它们的语言编写代码生成引擎。不幸的是,没有哪家语言卖主能够预见大量的第三方组件卖主可能需要的代码生成要求。第二种方式就是需要每个组件卖主为每种他们想要支持的语言提供代码生成引擎。这也是相当糟糕的,因为所支持语言的数目是不固定的。

为了解决这个问题,.NET框架定义了一个叫做代码文档对象模型(Code Document Object Model,简称CodeDOM),所有源代码本质上可分解成基本的元素,而CodeDOM就是这些元素的对象模型。当代码关联于CodeDOM时,产生的对象模型可以延迟发送到用于特定语言的代码生成器,以生成相应的代码。

.NET框架2.0引入了CodeDomDesignerLoader类,它继承自BasicDesignerLoader类。CodeDomDesignerLoader是一个完整的加载器,其通过读写CodeDOM来进行工作。它是一个一切齐全即可使用的设计器加载器,所以你所需做的就是提供CodeDOM。

在示例应用程序中,你可以选择文件|类型|CodeDomDesigner-Loader来查看使用CodeDOM的示例(an example of the CodeDOM in action)。通过选择文件|新建|窗体来创建一个新窗体——这创建了一个DesignSurface并使用CodeDomDesignerLoader来加载它。要检查生成的代码,选择视图|代码|C#来查看窗体代码的C#版本,或者能过视图|代码|VB来查看Visual Basic版本的代码。

为了生成代码,示例应用程序使用了CSharpCodeProvider和VBCodeProvider类。它也使用了代码提供者来编译代码并且运行可执行文件(见图11)。

ITypeResolutionService服务,在使用CodeDomDesignerLoader时需要,负责解析一个类型。一个情景,例如,在这里调用一个服务用于解析类型是当从工具箱添加一个控件到设计器中的时候发生的。(One scenario, for example, where this service is called to resolve types is when adding a control from the toolbox into the designer.)示例应用程序从System.Windows.Forms程序集中解析了所有的类型以使你能够从工具箱的Windows窗体标签中添加控件。

总结

正如你所见,.NET框架2.0提供了一个强大而灵活的设计器主机基础架构。,设计器提供了直接的扩展,有利于满足特定的需求或更加高级的场合(scenarios),相比Visual Studio的先前版本。当然,设计器也能够在Visual Studio之外进行承载。确保下载示例应用程序,因此你就可以好好享受这些代码并实现一个自己自定义设计器了。

posted @ 2020-12-17 15:54  许宝  阅读(298)  评论(0编辑  收藏  举报