在《使用 Eclipse 插件开发环境构建模板》一文中,作者就如何创建一个 Eclipse 模板进行了介绍,笔者在作项目的过程中也恰巧用到了 Eclipse 的这个功能,对这个功能给用户带来的方便性有着深刻的体会,令笔者体会更深的是一个指导性的模板还能够为用户的操作提供正确性的指导,避免用户由于错误理解对于工具作任何错误的操作。本文就 Eclipse 插件模板的一些高级功能进行了说明并通过一个例子讲解了如何为您的产品创建一个复杂的插件模板。
背景介绍
当您的产品功能非常复杂的时候,您还在花费精力写大量的用户指南,并且指望着用户会严格遵循用户指南不烦任何错误的应用您的产品么?笔者不是否定用户指南的重要性,一个产品一定要有指导性的用户指南,但是凭借笔者多年的经验,绝大多数用户都会一接触到工具就先凭借自己的想像和理解“试探性的使用”一番,只有当他们遇到问题的时候才会去尝试查看用户手册,然而一个能够“带着” 用户一步一步按着正确的方法朝着正确的方向走的“导游”就不一样了,“他”能够用“行动”来带领你走向成功。
Eclipse提供的Wizard, Cheat Sheet[1]和Template(模板)等等功能都是为了最大化的减小用户的使用难度,引导用户正确的使用工具所提供的向导。在《使用Eclipse插件开发环境构建模》[2] 一篇文章中,该作者就如何创建一个Eclipse模板进行了介绍,笔者在作项目的过程中也恰巧用到了Eclipse的这个功能,对这个功能给用户带来的方便性有着深刻的体会,令笔者体会更深的是一个指导性的模板还能够为用户的操作提供正确性的指导,避免用户由于错误理解对于工具作任何错误的操作。本文就Eclipse插件模板的一些高级功能进行了说明并通过一个例子讲解了如何为您的产品创建一个复杂的插件模板。
例子介绍
为了使本文所讲解的Eclipse插件模板技术点更容易理解,笔者提供了一个简单的模板实例,该实例为用户提供了一个多页定制模板,用户可以利用这个模板来实现:
- 为新建的插件程序绑定一个特定的配置文件,指定配置文件的名称和路径;
- 扩展Action类,实现一个简单的可以弹出对话窗口的Action;
- 将2中的对话窗口中的输出文字与1中的配置文件的内容绑定过来的,使得对话框中的输出文字为配置文件的内容。
在后面的文章中我们将逐步的指导读者如何来实现这个‘定制的模板’,为了实现这个模板,我们先创造出一个“产品”,该产品由两个Project组成(您可以在附件中下载例子项目并导入到您自己的Eclipse环境),他们是:
- sample.base:它定义了两个扩展点configFile.xsd用于动态的指定一个配置文件,以及taskDef.xsd将用户动态绑定到一个实现了ITask的类,该类将configFile.xsd所绑定的配置文件的内容绑定到弹出对话窗口的输出界面。相关技术点(不在本文研究范畴之内,您可以直接下载项目得到已经定义好的两个扩展点):
- 定义扩展点,本例的扩展点为configFile和taskDef;
- 利用configFile定义配置文件,并从配置文件中读出数据进行执行;
- 利用taskDef定义扩展类必须实现一个预定义的接口;
- sample.template:它是我们实现插件模版的项目,利用它可以生成一个插件,这个插件扩展了sample.base中定义的扩展点configFile和 taskDef,同时还扩展了eclipse提供的actionSet。技术点:
- 如何实现一个多页的插件模板;
- 对原有的TemplateOption进行扩展,实现一个带有提示功能的Light Bulb Option;
- 对插件模板进行验证;
模板高级配置详解
关于如何创建一个基础的模板的知识请参见《使用 Eclipse 插件开发环境构建模板》[2],本文将就实例重点的分析一些模板创建的高级配置。
创建包含多个模板页面的模板 Wizard
Eclipse的绝大部分内置模板Wizard都只包括一个配置页,例如,Sample Editor Wizard:
图 1. Eclipse PDE 中生成 Editor 的模板页面
然而,当您的产品很复杂时,仅有一页的 Wizard 就不能将众多的配置根据商业需求明确的分离开来,这时您就需要一个多页的Wizard来处理您的配置。实际上,Eclipse是支持创建含有多个可配置模板页的Wizard的。那么下面的问题就来了,当我需要为本例设计一个多页的Wizard时,我需要怎样分离商业逻辑,Eclipse模板机制又是如何支持多页的Wizard的呢?
首先来回答第一个问题,细心的读者可能发现过,当您在模版选择时,选择Eclipse的”自定义模板”模板类型时,然后单击Next的时候您会发现所有的页面都是由Extension Point(扩展点)划分的,这正好也恰恰是问题的答案,多页模板Wizard是根据扩展点来划分页面边界的,每个扩展点都是一个独立的逻辑单元,根据扩展点来分页刚好把商务逻辑以及技术逻辑的逻辑单元清楚的分离开。
图2. 自定义模板的模板分区
那么,Eclipse 模板框架到底是如何支持将模板 Wizard 按着扩展点分页的呢?您可以在您定义的 NewPluginTemplateWizard[2] 的实现类中的 createTemplateSections 方法内,按照您想要的顺序定义一个 ITemplateSection 数组,模板被调用的时候会自动按顺序调用您在这里定义的 TemplateSection,每个 Template Section 作为一个单独的 Template 页面处理一个扩展点。
清单1:为模板 Wizard 添加多个模板页面
Public class SampleDefinitionPluginWizard extends NewPluginTemplateWizard{
public SampleDefinitionPluginWizard() {
super();
}
public void init(IFieldData data) {
super.init(data);
setWindowTitle(Messages.PLUGIN_CONTENT_WIZARD_TITLE);
}
/**
* register all the related templates
*/
public ITemplateSection[] createTemplateSections() {
return new ITemplateSection [] {
new SampleConfigFileTemplate(),
new SampleMenuTemplate(),
new SampleTaskTemplate(),
};
}
}
|
模板页面高级配置之一:Light Bulb Option(域)
利用模版生成插件时,每个模板页面都会有至少一个域值(Option)需要被用户选择或填写,这些域值就是模板为用户提供的可以定制的内容。如下图3中所示,模板的域可以有多种形式:名值对,单选列表,多选列表,等等;为了达到对这些域的排版,取值和校验的统一管理,Eclipse模板机制要求用户在扩展这些域时必须使用Template Option的子类。
图 3. 利用模版生成一个简单视图
Eclipse 模板机制提供了一些已经实现了的标准常用的Template Option的实现类,比如字符串选项(StringOption)、单选选项(ChoiceOption)、布尔选项(BooleanOption)等等。多数情况下直接使用它们都可以达到我们预期的效果。
图 4. Eclipse PDE 内置的 Template Option 扩展实现
下表列出了PDE预先定义的几个常用的 Template Option 实现:
表 1. 常用的 Template Option 扩展
选项(类名) |
说明 |
BlankField |
用于在模板分区向导页面中创建空白空间 |
BooleanOption |
用于在模板分区向导页面中收集来自用户的布尔型选项 |
ChoiceOption |
不建议使用 —— 请使用 RadioChoiceOption 或 ComboChoiceOption |
StringOption |
用于在模板分区向导页面中收集来自用户的字符串 |
RadioChoiceOption |
用于在模板分区向导页面中收集来自用户的一组单选选项 |
ComboChoiceOption |
用于在模板分区向导页面中收集来自用户的一组组合选项 |
虽然Eclipse模板机制给出了以上诸多实现,但是,有时我们可能还是会觉得这些选项不能够满足我们的一些特定的要求,这时,我们就需要自己定义一下Template Option的扩展。在本例中,我们实现了一个扩展的域定义,叫做LightBulbOption,它可以显示的提示用户某些域值的使用说明,实现的效果如下图5。当你将鼠标放在File的输入框中(或其他方法如Tab等,使该输入框得到焦点)时,用户将得到一条简单的关于该输入值用途的提示。这样的小提示可以使用户更加清晰明了的理解自己所添的值的用途,从而避免一些理解歧义引发的错误。
图 5. Light Bulb Option 的演示
实现上述的Light Bulb Option的步骤如下:
- 创建一个扩展自org.eclipse.pde.ui.templates.TemplateOption的类,本例中我们命名它为LightBulbOption。
- 复写createControl方法,该方法实现了对Template Option的控制。在该方法中利用org.eclipse.jface.fieldassist.DecoratedField实现这种提示的效果,(注:DecoratedField类如其名,是用来负责提供对于域的装饰的,您可以利用它来设置装饰域的图片等,这部分的知识不是本文的重点,请参考Eclipse的其他文章来学习相关知识。):
- 设置DecoratedField的图片,如上图5所示,我们这里用了一个小灯泡来表示注释图标;
- 将新建的DecorateField注册到FieldDecorationRegistry,并在注册时指定注释时输出的语句(本例中用的是一个传进来的bulbDescription变量);
清单2: Light Bulb Option的实现方法
public void createControl(Composite parent, int span) {
………
DecoratedField field = new DecoratedField(
parent, SWT.BORDER | SWT.SINGLE, new TextControlCreator());
text = (Text) field.getControl();
Image image = ImageUtil.CONTENT_ASSIT_IMAGE;
FieldDecorationRegistry registry = FieldDecorationRegistry.getDefault();
registry.registerFieldDecoration(entryId, bulbDescription, image); //$NON-NLS-1$
field.addFieldDecoration(
registry.getFieldDecoration(entryId), SWT.TOP | SWT.LEFT, true); //$NON-NLS-1$
if (getValue() != null)
text.setText(getValue().toString());
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = span - 1;
gd.horizontalIndent = -5;
field.getLayoutControl().setLayoutData(gd);
text.setEnabled(isEnabled());
text.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
if (ignoreListener)
return;
LightBulbOption.super.setValue(text.getText());
getSection().validateOptions(LightBulbOption.this);
}
});
}
|
清单3: getStringOption对于getReplacementString的调用源码
public String getReplacementString(String fileName, String key) {
String value = getStringOption(key);
if (value != null)
return value;
return super.getReplacementString(fileName, key);
}
public String getStringOption(String name) {
TemplateOption option = (TemplateOption) options.get(name);
if (option != null) {
if (option instanceof StringOption)
return ((StringOption) option).getText();
else if (option instanceof ComboChoiceOption) {
// Added by DG: selection of the choice option
// should also be considered if the selected
// value is a String.
ComboChoiceOption ccoption = (ComboChoiceOption)option;
Object value = ccoption.getValue();
if (value!=null && value instanceof String)
return (String)value;
}
}
return null;
}
|
![](http://pubimage.360doc.com/wz/default.gif) |
注意事项:
在自己扩展Template Option时,您一定要留意它的一个小陷阱:当 PDE 从模版中复制文件的时候,它会调用 getReplacementString 方法,找到在模版向导中输入的值,参见 org.eclipse.pde.ui.templates.BaseOptionTemplateSection。
|
|
我们应该覆盖getStringOption方法来取得用户在Wizard的模板页面中输入的值,但在真正的Template Option实现的过程中,笔者发现您根本没有途径去在自己实现的Template Section中调用options.get(name)。 所以,一个权衡的办法就是如下表所示的方法,根据Option的Key值来抽取出您想要的客户的输入, 见表5,CONFIGFILE_FILENAME代表的就是图5 中LightBulbOption的File值。
清单4: 复写getReplacementString
public String getReplacementString(String fileName, String key) {
if (key.equals(CONFIGFILE_FILENAME)) {
return (String)fileOption.getValue() ;
} else{
return super.getReplacementString(fileName, key);
}
}
|
模板页面高级配置之二:输入值的校验
有输入就离不开校验。比如文件名不能为空或者某些输入的值是只能为数字等等。
图 6. File 输入名字为空时的报错信息
Template Option的子类是通过调用所在容器/部分(section)的validateOptions方法来实现校验的,当我们有特殊的校验需求时,可以覆盖这个方法。
例如:
清单5: 校验源码实例
/**
* Validate your options here!
*/
public void validateOptions(TemplateOption source) {
if (source.isRequired() && source.isEmpty()) {
flagMissingRequiredOption(source);
} else {
validateContainerPage(source);
}
}
/*
* Validate Container Page
*/
private void validateContainerPage(TemplateOption source) {
TemplateOption[] allPageOptions = getOptions(0);
for (int i = 0; i < allPageOptions.length; i++) {
TemplateOption nextOption = allPageOptions[i];
if (nextOption.isRequired() && nextOption.isEmpty()) {
flagMissingRequiredOption(nextOption);
return;
}
}
resetPageState();
}
|
Template Option中定义了一个required属性,它标识了该域值是否为必需的。当该属性被设为true时,如果域值没有被设置,Wizard将自动报错,这是一个最简单的校验过程。当我们对于校验没有其他的特别要求是,我们可以直接通过调用setRequired来设置空校验。
![](http://pubimage.360doc.com/wz/default.gif) |
注意事项:
在Template Option的所有预实现子类中,只有StringOption是默认要校验的。StringOption 的构造函数如下:
public StringOption(BaseOptionTemplateSection section, String name, String label) {
super(section, name, label);
setRequired(true);
}
|
|
模板页面高级配置之三:为插件添加依赖关系(Dependency)
经常使用Eclipse扩展点机制的用户应该都知道,要想实现带扩展的Eclipse插件项目必须首先将定义扩展点的插件作为您所创建的插件的依赖关系(Dependency)声明在您的插件文件(plugin.xml)中。通常情况下用模板创建的类都会依赖于一些其他的插件,例如本例中所创建的模板就必须依赖于4个其他的插件项目。
这时候您需要对所有您模板生成的插件的依赖插件进行声明,Eclipse的模板机制对添加依赖关系也提供了支持。您可以在您的OptionTemplateSection实现类中设置该实现类相关联的依赖关系插件组。如下表8所示,SampleTaskTemplate所定义的模板的实现将要依赖于3个插件项目:
- "org.eclipse.core.runtime"
- "org.eclipse.ui"
- "com.sample.template"
您需要作的事情就是,将这些插件的id在getDependencies方法内声明出来:
清单 6: 声明依赖关系
public IPluginReference[] getDependencies(String schemaVersion) {
if (schemaVersion != null) {
IPluginReference[] dep = new IPluginReference[3];
dep[0] = new SamplePluginReference(
SamplePluginReference.eclipse_Core_Runtime_ID, null, 0);
dep[1] = new SamplePluginReference(
SamplePluginReference.eclipse_UI_ID, null, 0);
dep[2] = new SamplePluginReference(
SamplePluginReference.sample_template_ID, null, 0);
return dep;
}
return super.getDependencies(schemaVersion);
}
|
上清单6中的SamplePluginReference相当于各个IPluginReference的一个简单的Adapter,它实现了IPluginReference接口,通过Plugin id来指定具体的插件。
清单7:SamplePluginReference根据id来选取插件的实现
public class SamplePluginReference implements IPluginReference {
……
public boolean equals(Object object) {
if (object instanceof IPluginReference) {
IPluginReference source = (IPluginReference)object;
if (id==null) return false;
if (id.equals(source.getId())==false) return false;
if (version==null && source.getVersion()==null) return true;
return version.equals(source.getVersion());
}
return false;
}
……
}
|
总结
好的,通过本文的介绍,模板实例已经成功的创建了,您可以导入我们的实例源码到您的Workspace里面进行试验。通过本实例您应该对Eclipse的模板功能有了进一步的了解,您已经学会了如何创建一个多页的模板,如何利用模板来绑定配置文件,如何利用模板来自动为用户生成需要扩展的类框架,如何为用户自动的添加所需的依赖关系等等,那么您还在等什么,还不为您现在的项目也添加一个这样的模板,我坚信您的用户一定会对它好评如潮的。
参考资料
作者简介
![](http://pubimage.360doc.com/wz/default.gif) |
|
![](http://pubimage.360doc.com/wz/default.gif) |
李东兵,IBM 中国软件开发实验室 SOA 设计中心,软件工程师
|
![](http://pubimage.360doc.com/wz/default.gif) |
|
![](http://pubimage.360doc.com/wz/default.gif) |
李彦宁,IBM 中国软件开发实验室 SOA 设计中心,软件工程师
|
|