EMF介绍系列(六、自定义命令)

EMF生成的应用程序里,用户的发出的每一条命令都是可以撤销(Undo)的,例如修改了产品的价格,按一下撤销按钮就能恢复原来的价格,当然还可 以通过重做(Redo)再回到新的价格。为了实现这个功能,应用程序里维护了一个用于存放命令的类似栈的数据结构(CommandStack),每一条执 行过的命令都被存放在那里,需要撤销时取出最近一条命令进行撤销。这个数据结构是由EditingDomain对象负责维护的, EditingDomain相当于编辑模型时的环境。

在EMF里命令框架实际上可以分为两大部分,一部分是与模型无关的通用命令,另一部分是.Edit命令,后者是建立在前者的基础之上的。EMF对模 型的任何修改都是通过命令完成的,例如当用户在属性视图里修改一个对象的属性时,会生成一个新的SetCommand实例,然后执行它的execute ()方法,这个方法里对模型进行修改(实际上是通过doExecute()方法),成功执行完成后这个命令被放入命令栈以便撤销时使用。

通用命令可以完全脱离EMF使用,也就是说,这个命令框架可以应用到任何需要命令框架的应用程序中,包括非EMF应用程序。它位于 org.eclipse.emf.common.command包里,其中Command接口定义了什么是“命令“,一个命令具有execute()、 undo()和redo()等方法,还有canExecute()和canUndo()方法用于判断命令是否可被执行或撤销(考虑到资源消耗,有些命令可 能设计为不可撤销更合理)。另一个重要的接口是前面提到过的CommandStack,它的作用是保存所有命令,可以通过 addCommandStackListener()方法注册监听器来观察CommandStack的状态变化。CompoundCommand接口可以 把多个命令按顺序包装成一个组合命令,它具有原子性,类似数据库里事务(Transaction)的概念,只有所有命令都可执行时这个组合命令才可执行, 撤销也是如此。

EMF在.Edit框架提供了针对EMF模型编辑所需要的一些命令(位于org.eclipse.emf.edit.command包),例如 SetCommand用于修改对象的属性,CreateChildCommand的作用是创建一个子元素,还有MoveCommand、 CopyCommand和CutToClipboardCommand等等。这些命令都实现Command接口,并且大部分继承自 AbstractOverrideableCommand这个抽象类,它给我们带来的影响是在Command接口里的方法名前面都加了一个do,比如 execute()变为doExecute()、canUndo()变为doCanUndo()等等,我们在扩展这些.Edit命令时要覆盖doXXX方 法。.Edit命令是通过反射的方式来修改模型的。

EMF提供的这些命令为我们完成基本的模型编辑功能,多数情况下直接使用它们就可以了,但有时通过自定义的命令可以实现一些特别的需求。举个例子来 说,在网上商店的例子里,假设要求产品的价格只精确到小数点后两位,那么我们要在用户输入新价格以后立刻对这个数值进行四舍五入处理,这个操作就可以利用 自定义命令完成。因为利用了.Edit提供的类,所以一般我们应该扩展.Edit命令,具体来说就是SetCommand。

首先通过继承SetCommand创建我们的SetPriceCommand,在这个方法里覆盖doExecute()方法,SetCommand 里有很多可供利用的环境变量,我们要用到的是owner和value这两项,前者是要修改的对象,在这里是产品对象,后者是属性的新值,在这里也就是新价 格。所以我们的SetPriceCommand可以像下面这样写(为了使代码最简,我们直接把EObject类型转换为Product类型,这样就不需要 用反射的方式了):

public class SetPriceCommand extends SetCommand {
    public SetPriceCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value) {
        super(domain, owner, feature, value);
    }
    public void doExecute() {
        Product product = (Product) owner;
        double newPrice = ((Double) value).doubleValue();
        newPrice = Math.max(0, newPrice);//New price value must >= 0
        newPrice=Math.round(newPrice*100)/100d;//Max fraction digits is 2    
        product.setPrice(newPrice);
    }
}

要让这个自定义命令生效,必须在ProductItemProvider里覆盖createSetCommand()方法,因为这个方法缺省是返回SetCommand的,我们要在这里做一个判断:如果修改的是价格属性,就返回我们自定义的这个命令,如下所示:

protected Command createSetCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value,
        int index) {
    if (feature == ShopPackage.eINSTANCE.getProduct_Price())
        return new SetPriceCommand(domain, owner, feature, value);
    return super.createSetCommand(domain, owner, feature, value, index);
}

这样当用户在属性页里改价格属性时,就会调用我们的SetPriceCommand了。顺便说一句,在GEF里 也有类似的EditDomain和Command,只是GEF里的Command一般通过EditPolicy的createXXXCommand()方 法来创建。因为GEF和EMF的两套Command机制没有实现统一的接口,所以结合GEF和EMF的时候常会遇到一些问题,需要额外的代码帮助解决,请 参考这两处讨论(讨论1,讨论2)。

最后要说一句,CreateChildCommand有点特殊,它是.Edit命令但不继承 AbstractOverrideableCommand,而且如果想在创建子元素时自动完成一些工作,不应该通过扩展这个类完成,而应该在 XXXItemProvider的collectNewChildDescriptors()方法里处理,这个方法决定每个对象可以创建哪些子元素,你可 以修改它的代码以对新建的元素做一些处理。

参考资料

Eclipse Modeling Framework A Developers Guide,第3.3节、第14.1节。

代码下载

点击下载源代码

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/12/31/309182.html

[Eclipse]使用osgi.util.NLS简化资源文件访问

Eclipse 3.1提供了新的资源文件访问方式,就是通过osgi.util.NLS类。具体方法是构造一个NLS的子类,如下:

public class MyMessages extends NLS {
  private static final String BUNDLE_NAME = "gr.scharf.MyMessages"; //$NON-NLS-1$

  public static String HELLO_WORLD;
  public static String HELLO_SOMETHING;

  static {
  // initialize resource bundles
  NLS.initializeMessages(BUNDLE_NAME, MyMessages.class);
  }
}

资源文件的内容还是和以前一样:

HELLO_WORLD=Hello world!  
HELLO_SOMETHING=Hello {0}!

在程序里使用资源的方式如下:

System.out.println(MyMessages.HELLO_WORLD);  
System.out.println(MyMessages.bind(MyMessages.HELLO_SOMETHING,"world"));

这样一来代码简单了不少,但eclipse jdt提供的externalize strings功能生成的类还是以前的方式,而不是生成这样的NLS子类。

小Tip:在.java文件编辑器里输入nls,再自动完成(ctrl+space),这样可以快速生成"//$NON-NLS-N$"标记。

参考资料

原文地址(访问不到,似乎被删除了)

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/12/31/308718.html

EMF介绍系列(五、定制应用程序界面)

第三篇帖子介绍了定制一个EMF应用程序的基本方法,这一篇让我们来看看怎样定制应用程序的使用界面。没有任何一个界面是万能的,所以定制工作不可避免,而大多数定制都是通过修改代码来实现的。在实际应用中,同一个需求可能有多种修改方式可以实现,我认为修改涉及的地方(类,方法)越少越有利于 发挥EMF的优势,因此我们应该对EMF生成的代码有一定的了解,这是发挥自己创造力的基础。

下面有几个常见的需求,通过对这些需求的实现,相信你会对EMF应用程序的开发过程有一个更具体的认识。

一、简化模型创建向导

EMF帮我们生成的模型创建向导(菜单File->New->Other->Shop Model)分为两步,第一步要用户输入文件名,对于商店的例子文件名是*.shop格式;第二步用户要选择以哪个对象作为根节点,同时要指定XML文件 的编码方式,商店例子里显然要以商店对象为根节点,所以其实第二步可以省去,以免造成使用者的困扰。

生成的向导类是ShopModelWizard,比起增加一个步骤来,去掉一个步骤要简单得多。首先找到addPages()方法,把最后四句关于initialObjectCreationPage最的语句都注释掉;

/**
 * The framework calls this to create the contents of the wizard.
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @generated NOT
 */
public void addPages() {
    ...
    //initialObjectCreationPage = new ShopModelWizardInitialObjectCreationPage("Whatever2");
    //initialObjectCreationPage.setTitle(ShopEditorPlugin.INSTANCE.getString("_UI_ShopModelWizard_label"));
    //initialObjectCreationPage.setDescription(ShopEditorPlugin.INSTANCE.getString("_UI_Wizard_initial_object_description"));
    //addPage(initialObjectCreationPage);
}

现在因为没有了这个向导页,原来由它提供的信息我们要改为在程序里提供,所以要修改另外两个方法:第一,createInitialModel()方法本来是建立一个用户选择的对象作为根节点的模型,我们把它改为直接建立一个Shop对象;

protected EObject createInitialModel() {
//    EClass eClass = (EClass)shopPackage.getEClassifier(initialObjectCreationPage.getInitialObjectName());
//    EObject rootObject = shopFactory.create(eClass);
    EObject rootObject=shopFactory.createShop();
    return rootObject;
}

第二,在performFinish()方法里设置文件编码的地方,改为使用UTF-8编码,当然你也可以规定使用其他编码,只是用户不能选择了:

public boolean performFinish() {
    ...
    options.put(XMLResource.OPTION_ENCODING, "UTF-8"/*initialObjectCreationPage.getEncoding()*/);
    ...
}

因此,这个类里我们总共修改了三个方法,一定记得要把每个方法前的@generated标记删除或修改。现在,用户只要简单的指定文件名后就可以Finish了,如图1所示。

file
图1 向导的最后一页

二、改造大纲视图的显示

对于EMF来说,在应用程序模型的根节点上还有两层,分别是Resource和ResourceSet,在商店的例子里,Category的父节点 是Shop,Shop的父节点是Resource(具体来说是XMLResource),Resource的父节点是ResourceSet,它们之间都 是多对一的关系。缺省情况下,大纲视图里显示的是完整的ResourceSet树(根节点不显示),显示出的最上层节点是“platform: /resource/Project3/My.shop”,它代表一个Resource,这个Resource(通过这个URI)指向保存着模型信息的 XML文件My.shop。对于使用者来说,这个节点显示在这里没有什么意义,用户看到的根节点应该是.shop文件里保存的Shop对象,见图2的对 比。

file
图2 在大纲视图里隐藏最上层节点

那么该修改哪些代码来实现这个需求呢?我们想到大纲视图里的内容是从ShopEditor的getAdapter()方法里得到的,通过查看 ShopEditor的getAdapter()方法发现名为getContentOutlinePage()的方法负责产生大纲模型,在这个方法里,变 量contentOutlineViewer是对大纲视图里的树控件的包装对象,它的输入(Input)是 editingDomain.getResourceSet(),我们要把它的输入改成ResourceSet的第一个Resource,修改后的代码如 下:

public IContentOutlinePage getContentOutlinePage() {
    ...
    //contentOutlineViewer.setInput(editingDomain.getResourceSet());
    contentOutlineViewer.setInput(editingDomain.getResourceSet().getResources().get(0));
    ...
}

你可能会问,那么从Resource怎样得到Shop对象呢?很简单,(Shop)yourResource.getContents().get (0)即可,有兴趣的话你可以试试把大纲视图的输入设为Shop对象会看到什么。最后说一次不要忘记修改@generated,以后不再提醒了。

三、移除编辑器里多余的Tab页

EMF生成的Editor为我们提供了六个Tab页,其主要目的是向我们演示如何以各种方式展示数据(例如在大纲视图里选择一个Category对 象,通过Parent页里可以很容易的看到前面说过的Category->Shop->Resource->ResourceSet关 系),在实际的应用里一般不会用到全部这些页,下面我们就只保留Table页而移除其他五页,利用大纲和Table页的组合,实现类似Windows资源 管理器的界面。

编辑器里的页面在createPages()方法里被添加,它虽然很长但EMF在这个方法里生成了不少注释,每段代码的作用都很明显,只要把我们不 需要的那五段注释掉即可。现在把程序运行起来,打开一个模型文件,稍微调整一下布局把大纲视图放在编辑器的旁边,如图3所示,有点资源管理器的样子了吧。

file
图3 和资源管理器类似的布局

但是很遗憾,现在的表格里只有两列,而且两列里显示的内容是相同的。按照资源管理器的设计,当用户在大纲视图里选择一个对象时,表格中应该显示该对象的子对象详细信息的列表,现在子对象列表已经有了(表格里每一行就是一个子对象),让我们做一些修改以显示详细信息。

首先增加一个表格列,还是在ShopEditor的createPages()方法里修改,搜索一下TableColumn很容易找到应该修改的位 置。新的三列标题分别为“Name”、“Children”和“Description”,其中Children列里显示子对象的数目。

public void createPages() {

    TableColumn objectColumn = new TableColumn(table, SWT.NONE);
    layout.addColumnData(new ColumnWeightData(3, 100, true));
    objectColumn.setText("Name");
    objectColumn.setResizable(true);

    TableColumn childrenColumn = new TableColumn(table, SWT.NONE);
    layout.addColumnData(new ColumnWeightData(2, 100, true));
    childrenColumn.setText("Children");
    childrenColumn.setResizable(true);

    TableColumn descColumn = new TableColumn(table, SWT.NONE);
    layout.addColumnData(new ColumnWeightData(2, 100, true));
    descColumn.setText("Description");
    descColumn.setResizable(true);
    ...
}

如果现在运行程序,看到的将是三列,但内容仍然是相同的。表格里显示的内容是由生成的XXXItemProvider类决定的,例如对于一个 Category对象在表格或树控件里怎样展示是由CategoryItemProvider来负责,你可以把它看作是JFace里的 ContentProvider加上LabelProvider,这些XXXItemProvider都被放在.edit项目里了。EMF生成的 CategoryItemProvider没有实现ITableItemLabelProvider接口,所以缺省情况下不能支持表格的展示(能够显示, 但每列的内容相同),所以我们要对代码进行一些修改,在CategoryItemProvider实现的接口列表里增加 ITableItemLabelProvider,并实现它的两个方法,修改后的代码如下:

public class CategoryItemProvider
    extends ItemProviderAdapter
    implements    
        IEditingDomainItemProvider,    
        IStructuredItemContentProvider,    
        ITreeItemContentProvider,    
        IItemLabelProvider,    
        IItemPropertySource,
        ITableItemLabelProvider{

    public Object getColumnImage(Object object, int columnIndex) {
        return null;
    }

    public String getColumnText(Object object, int columnIndex) {
        Category category=(Category)object;
        switch (columnIndex) {
        case 0:
            return category.getName();
        case 1:
            return category.getChildren().size()+"";
        case 2:
            return "";//Categories don't own descriptions
        default:
            return "";
        }
    }
    ...
}

现在只差一步就完成了,如果你注意看过ShopEditor的createPages()方法里定义TableViewer的代码,会发现这个 TableViewer的ContentProvider和LabelProvider都是一个 AdapterFactoryContentProvider对象,这个对象会把TableViewer对getText()、getElements ()的请求转发到XXXItemProvider上;转发之前它要得到这个XXXItemProvider,这是通过 ShopItemProviderAdapterFactory的adapt()方法实现的,而 ShopItemProviderAdapterFactory维护了一个supportTypes列表,只有注册到这个列表中的类型才能被adapt。 这里出现了不少新内容,可能不那么容易理解,没有关系,因为在以后的帖子里会专门介绍到它们,现在只要记住需要把我们新实现的接口类型注册到 ShopItemProviderAdapterFactory的supportTypes里即可,具体的方法是修改它的构造方法,如下所示:

public ShopItemProviderAdapterFactory() {
    supportedTypes.add(IEditingDomainItemProvider.class);
    supportedTypes.add(IStructuredItemContentProvider.class);
    supportedTypes.add(ITreeItemContentProvider.class);
    supportedTypes.add(IItemLabelProvider.class);
    supportedTypes.add(IItemPropertySource.class);    
    supportedTypes.add(ITableItemLabelProvider.class);//Added to support table
}

现在,表格里显示的Category对象已经按我们的要求列出其他信息了,如图4所示,Description列是空白因为Category没有这 个属性。我们还应该修改ProductItemProvider以展示产品的详细信息,方法和修改Category是类似的,而且增加 supportTypes的步骤不须要重复做,所以更加简单了,不妨就留作练习。

file
图4 经过定制的表格

经过上面的这些定制,我们就实现了应用程序从EMF缺省界面到资源管理器风格界面的转换,虽然文字比较多,但掌握以后这个过程是相当快速的,而且其他的定制也是同样的思路。

代码下载

点此下载代码

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/12/23/303508.html

使用工作集(Working Set)整理项目

Eclipse鼓励将不同的功能模块划分为独立的项目存在,这样不但结构清晰,组织起来还非常灵活,因为我们可以用feature对这些项目进行不同的组合,输出后得到具有不同功能的产品。

不过这样一来Package Explorer里的项目会以更快的速度增加,当你面对几十上百个项目时,工作效率必然大打折扣。幸好Eclipse提供了工作集(Working Set)的功能,它可以用来划分这些项目。

在Package Explorer视图的下拉菜单里选择Show->Working Sets,然后还是在它的菜单里选择Select Working Sets,在对话框里定义你的工作集,每个工作集要对应至少一个项目,按OK后你的项目就分好类了,没有归类的项目都放在名为Other Projects的工作集里。

赶快试试吧!

file

补充(2006-4-11):另一个整理项目的办法是使用多个Workspace,似乎也不错,见Tips for using Eclipse effectively

EMF介绍系列(四、枚举类型、自定义类型和Map)

除了普通的类(接口)以外,在类图里可以定义一些特殊的元素,比较常见的是枚举类型、自定义类型,它们对于一个完整可用的模型也是必不可 少的,这篇帖子主要介绍EMF里它们的使用方法。另外,由于EMF对Map的支持比较特别,所以在这里也简要介绍一下Map类型的定义方法。

枚举类型

继续前面帖子的 例子,现在要为产品增加一个评分属性,评分值可以是好中差之一,像这样属性值只能是有限几个值之一的属性就应该定义为枚举类型 (Enumeration)。在类图里首先创建一个名为Score的枚举类型,然后为它增加三个可选值,每个值对应一个唯一的整数值作为标识;然后给 Product类型添加一个名为score的属性,这时的类型列表里已经比原来多了Score类型,我们就选择它作为score属性的类型。重新生成一遍 代码,你会发现增加了Score类(不是接口),运行新生成的编辑器会看到,产品对象的属性里增加了评级,见图1。

file
图1 枚举类型的属性以下拉列表方式编辑

自定义类型

EMF虽然对大多数java类型做了包装,但是有些情况需要我们使用没有被包含的类型,例如在设计图形化的编辑器(例如类图编辑器)时,图形节点一 般允许选择背景颜色,这就需要一个org.eclipse.swt.graphics.RGB类型的成员变量,而RGB类是SWT提供的类,所以不能通过 创建一个同名类的方式实现,这时就要使用自定义类型。类似的道理,在必须利用遗产项目(Legacy)代码的时候,自定义类型也是必须的。

现在为Product节点增加这样一个名为background的成员变量,步骤如下:首先在类图上新建一个名为RGB的自定义类型(data- type,见图2),将它的Instance Class属性设置为org.eclipse.swt.graphics.RGB;然后给Product类添加一个成员变量background,类型选 择为刚建立的RGB;现在重新生成一遍代码,可以看到Product.java里已经多了这个成员变量,其类型为 org.eclipse.swt.graphics.RGB(因为org.eclipse.swt.graphics.RGB是属于 org.eclipse.swt这个插件的,所以要为com.my.shop项目增加对org.eclipse.swt的依赖才能正确编译)。

file
图2 新建自定义类型

不过到这里还没有完成全部工作。以为EMF对这个RGB类一无所知,所以它不可能为我们实现RGB对象的持久化,即将RGB类型的数据保存到(缺省 格式的)xml文件中。要解决这个问题得在生成的ShopFactoryImpl里做一些定制,具体来说是修改createRGBFromString ()和convertRGBToString()这两个方法,很明显它们的作用分别是从字符串创建RGB对象以及将RGB对象转换成字符串,我们把它们改为下面这样:

/**
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @generated NOT
 */
public RGB createRGBFromString(EDataType eDataType, String initialValue) {
    String[] values = initialValue.split(",");
    int red=Integer.parseInt(values[0]);
    int green=Integer.parseInt(values[1]);
    int blue=Integer.parseInt(values[2]);
    RGB rgb = new RGB(red,green,blue);
    return rgb;
}

/**
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @generated NOT
 */
public String convertRGBToString(EDataType eDataType, Object instanceValue) {
    RGB rgb = (RGB) instanceValue;
    return rgb.red + "," + rgb.green + "," + rgb.blue;
}

这些代码可以把RGB对象转换为形如“255,90,150”的格式,EMF在持久化时遇到RGB类型的对象就按这样的格式写到文件里。

使用EMap

在类图里可以指定各种类型的成员变量,但使用Map类型则有点特别,直接定义一个EMap类型的成员变量是不可以的,因为EMap并不是继承自 java.util.Map而是EList,也就是说EMF使用EList来模拟实现Map的功能(为什么要这样实现我还没弄清楚)。要实现一个EMap 类型的成员变量,在类图里要参考下面的方式进行定义。

现在我们要为产品类增加一个EMap类型的属性,用这个属性来记录产品每月的销售数量,它维护一个日期字符串(如"2005-09")到当月销售数 量的映射。首先,定义一个名为StringToIntegerMapEntry的新类,这个类将作为Map里的项(Entry);为这个类增加两个属性: 字符串类型的key和整数类型(Integer或int均可)的value;在属性视图里把这个类的Instance Class Name属性设置为java.util.Map$Entry;这样MapEntry类就定义好了,在需要EMap类型变量的类里引用它就可以了,注 意对它的引用必须是包含关系(即“组合”而非“聚合”,类图上连接线用黑色菱形修饰),并且是一对多的。

重新生成一遍代码,在Product接口里salesMap成员变量是这样定义的(因为EMF会检测到我们正在定义一个Map):

/**
 * Returns the value of the '<em><b>Sales Map</b></em>' map.
 * The key is of type {@link java.lang.String},
 * and the value is of type {@link java.lang.Integer},
 * <!-- begin-user-doc -->
 * <p>
 * If the meaning of the '<em>Sales Map</em>' containment reference isn't clear,
 * there really should be more of a description here
 * </p>
 * <!-- end-user-doc -->
 * @return the value of the '<em>Sales Map</em>' map.
 * @see com.my.shop.ShopPackage#getProduct_SalesMap()
 * @model mapType="com.my.shop.StringToIntegerMapEntry" keyType="java.lang.String" valueType="java.lang.Integer"
 * @generated
 */
EMap getSalesMap();

这样,在生成的编辑器里可以对每个产品增加新的项,每项包含key和value两个值;在程序里,则应该使用 Product#getSalesMap().put()方法向这个Map里增加数据,因为我们定义的Map项是字符串到整数类型的,所以put()的时 候value参数必须是整数对象(java.lang.Integer),否则会抛出ClassCastException。请记住,不要直接指定 EMap类型,包含MapEntry即可,如果key或value不是普通类型,则使用名为key或value的引用。

file
图3 编辑器里的EMap类型属性

file
图4 修改后的模型

代码下载

经过上面几处改动,现在我们的商店模型如下图所示,点此下载项目打包

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/11/28/286078.html

EMF介绍系列(三、定制应用程序的基本方法)

借助EMF的帮助,不用亲自编写一行代码就可以生成一个完整的应用程序,你是不是对EMF有些感兴趣了?不过生成的应用程序看起来都像是从同一个模子里出来的,即 一个多页编辑器,一个大纲视图和属性页,这当然无法满足所有人的需求。不用耽心,只要了解EMF的机制,按照我们的要求修改这个应用程序并不是一件很困难的 事情。

首先大概的看一下EMF为我们生成了哪些东西吧。按照前文的操作,EMF应该一共生成了四个插件项目:com.my.shop、 com.my.shop.edit、com.my.shop.editor和com.my.shop.tests,其中最后一个项目是方便我们编写单元测 试的框架代码,这里我们先不管它,暂时把注意力集中在前三个项目上。

第一个项目是模型部分,主要包含你定义的ecore模型里各类型(EClass,在ecore元模型里类型称为EClass,属性称为 EAttribute)对应的java接口和缺省的实现类代码,例如Product.java和ProductImpl.java,它们分别被放置在 com.my.shop和com.my.shop.impl包里;一个工厂类(ShopFactory)使用工厂模式创建新的模型实例;一个 Package类(ShopPackage)维护关于元模型的信息,提供了一堆静态成员变量;此外还生成了ShopAdapterFactory和 ShopSwitch这两个类,它们是可选的,它们俩的作用这里卖个关子暂时先不说。

第二个项目是.edit部分,这里面包含了一系列ItemProvider类,用来为在jface的各种查看器(Viewer)里显示这些模型对象 提供便利,以CategoryItemProvider为例,它实现了IStructuredItemContentProvider、 ITreeItemContentProvider和IItemLabelProvider这些接口,注意把这些接口名字中的"Item"去掉就是 jface里需要的Provider,可以把这些带有"Item"字样的Provider想象成对jface相应Provider的包装。有了这些 Provider,在应用程序里使用jface的TreeViewer、TableViewer等查看器就很方便了。(前面讲GEF的一个帖子里曾利用 EMF构造模型,当时使用的就仅仅是模型部分,因为并未用到jface查看器。所以视你的应用程序而定,可以只生成模型部分来用,.edit部分依赖模型部分,而反之不然。)

第三个项目是编辑器,这个部分依赖.edit部分,主要包含了几个UI方面的类。EMF为我们生成的这个编辑器有两个用途:一是让我们可以不用从零开始,而是在这个编辑器的基础上进行修改得到自己的编辑器; 二是通过这些代码展示怎样在应用程序里使用前两个项目里的那些类,编辑器包含那么多Tab正是为了演示各种查看器的用法。下面来说一下怎样定制生成的应用 程序。

一、修改ecore模型和genmodel模型

在ecore模型和genmodel模型里我们可以通过修改一些属性改变所生成的代码,例如希望新创建的类别和产品的名称不是空字符串而是" (Unnamed)",就可以在类图里修改NamedElement类的name属性的"Default Value Literal"属性(没错,属性的属性。见图1)。修改ecore模型后,必须更新genmodel模型,方法是在Package Explorer里右键单击shop.genmodel文件,在弹出菜单里选择"Reload...",这样genmodel会从修改后的ecore模型 里获得修改过的信息。之后,再次从genmodel模型生成一遍代码,这样得到的程序运行后,类别和产品的名称缺省就是"(Unnamed)"了。在 genmodel模型里则可以定制更多属性,例如所生成的每个项目的id、生成类所在的包名(本例中为com.my)、类名前缀(本例中为Shop)、是否生成图标、标准属性页里各属性的分类等等。

file
图1 在EclipseUML类图里修改属性的缺省值

有人对 ecore模型和genmodel模型各自的用途搞不清楚,其实因为JET是直接通过genmodel生成代码的,所以像是否生成图标这些信息放在 genmodel里是比较合适的,而ecore模型里定义的是各个类型以及之间的关系,所以像属性缺省值这样的信息是应该放在ecore里没错的。

二、直接修改生成的代码

前面的方式是让EMF生成我们想要的程序,好处是非常直观和方便,缺点是只有有限的信息可被定制,而一个复杂的应用程序是不太可能仅仅通过填写一些 属性就被生成出来的,所以我们需要代码级的定制。有一个事实你需要知道:无论对ecore还是genmodel模型定制,产生的结果最终都要反映到生成的 代码。让我们对比一下修改名称缺省值前后的代码(因为类图里NamedElement被定义为抽象类,所以这段代码的位置在 CategoryImpl.java和ProductImpl.java里),这是之前生成的代码:

/**
 * The default value of the '{@link #getName() <em>Name</em>}' attribute.
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @see #getName()
 * @generated
 * @ordered
 */
protected static final String NAME_EDEFAULT = null;

下面是定制之后的代码:

/**
 * The default value of the '{@link #getName() <em>Name</em>}' attribute.
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @see #getName()
 * @generated
 * @ordered
 */
protected static final String NAME_EDEFAULT = "(Unnamed)";

可以看到只是NAME_EDEFAULT的值发生了变化。也就是说,如果我们不修改ecore模型,而是手工修改这段代码也能达到同样的目的。对于熟悉 EMF的开发人员来讲,修改代码很多时候甚至比修改ecore/genmodel模型更加快速(因为省去了reload genmodel和生成代码这两步),但一定要记住,在修改代码的同时修改这部分代码前的javadoc。因为在每次重新生成代码的时候,JET通过原有 代码前的javadoc判断是否覆盖这段代码,如果javadoc里包含一行"@generated"则覆盖,否则保持这部分代码不变。为了让我们手工的 修改不在以后的生成中消失(这可是很大的损失),可以直接删除@generated这一行,更好的办法是将其改为一个统一的字符串,例如 "@generated NOT",以便将来通过javadoc搜索找到自己都在哪些地方做了手工修改。

下面再通过一个例子演示怎样修改EMF生成的代码。假设现在有个需求是让所有价格大于等于100元的产品自动打95折,而小于5元的商品一律按5元算,那么我们需要修改ProductImpl的getPrice()方法,修改前的代码如下:

/**
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @generated
 */
public double getPrice() {
    return price;
}

这是修改后的代码,再次提醒一定别忘记修改@generated那行注释:

/**
 * <!-- begin-user-doc -->
 * 所有价格大于等于100元的产品自动打95折,而小于5元的商品一律按5元算。
 * <!-- end-user-doc -->
 * @generated NOT
 */
public double getPrice() {
    if (price >= 100)
        return price * 0.95;
    if (price < 5)
        return 5d;
    return price;
}

至于使用哪种方式进行定制,我个人建议能通过修改ecore/genmodel模型解决的问题就不要直接修改代码,尽量减少手工修改的地方有利于保持思路清晰,其实在实际使用中大部分定制还是需要修改代码才能实现的。

代码下载

点此下载修改后的项目

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/11/19/280499.html

EMF介绍系列(二、从模型生成应用程序)

还是从一个例子里看看EMF的使用方法和作用吧。假设我们的应用是一个网上商店,在系统里有这些对象:商店Shop、类别Category、 商品Product,其中类别可以包含子类别。现在我们用EMF从头到尾生成一个可以管理类别和商品的应用程序。如果你的Eclipse里还没有安装 EMF,在eclipse.org/emf 下载适合你Eclipse版本的EMF SDK,建议你下载全部包含的那种。安装后,在新建对话框里会增加EMF的类别,如图1所示:

file
图1 新建向导里的EMF类别

其中“EMF Model”是从已有的模型文件创建genmodel模型,这个模型是专门用来生成代码的;如果你手里已经有一个模型(比如一个.mdl文件或是一个.xsd文件等等),可以选择新建“EMF Project”,这样在向导的后面部分里会要求提供已有的模型文件;因为我们打算自己从头开始建立这样的模型,所以选择“Empty EMF Project”,和建立普通插件一样,要提供一个插件名称,我们为这个商店项目起名为com.my.shop,然后按Finish即完成向导。目前这个新建立的 项目里还没有包含任何代码,只是在META-INF/MANIFEST.MF文件里定义了对EMF相关插件的依赖。

下面开始定义ecore模型,我比较喜欢图形化的方式,因为看起来很直观,所以我使用Omondo公司的EclipseUML插件来画类图,这个插件的免费版本在 它们网站下载,注意下载适合你的Eclipse的版本。当然rose也不错而且更加稳定,但它 不是免费的,而且只能在Windows里使用。和Rose不同,EclipseUML对EMF有特别的支 持,安装这个插件后,我就们可以在项目里新建一个“EMF Class Diagram”,这样会同时创建一个.ecd文件和一个.ecore文件,EclipseUML编辑的类图信息会保存在这两个文件里,前者主要是图形方 面的内容,而后者是真正的模型信息,注意这两个文件中的任何一个都不要搞丢了,最好能经常备份一下。

在EclipseUML里编辑类图很简单,需要注意的是,两个对象之间如果有关联,要仔细考虑关联是否为“包含”关系(一般在UML中以黑色菱形表 示),如果一个类没有包含在任何其它类里,则这个类的实例不会被保存到文件。例如图2中Category包含在Shop中,Product包含在 Category中。这样,所有的对象都直接或间接的被Shop对象包含。换句话说,如果以Shop作为“根”,所有的对象都可以被保存到文件里。

EMF对java基本类型和一些常用类做了包装,例如int->EInt,java.lang.Integer->EInteger以 及java.util.List->EList等等,所以在定义类的属性时要使用这些EMF的类型,当然也有办法使用自定义类型(以后会用到)。我 们例子里的模型相当简单,一共只有三种业务对象(为了更加直观,我们增加了一个NamedElement接口),现在网上 商店类图的第一个版本如图2所示。

file
图2 网上商店类图(版本1.0)

接下来就要生成代码了。EMF使用JET利用模板生成代码(前面曾介绍过JET),所以要把ecore模型转换为可以被JET利用的genmodel模型,具体的操作是按 ctrl+n新建一个EMF Model,在这个向导的第一步指定名称shop.genmodel,第二步选择从ecore模型导入,第三步选择ecore模型文件 (shop.ecore),这样就建立了缺省的genmodel模型,在这个模型的基础上还可以做一些定制工作,例如每个属性的描述信息等等。

有了genmodel模型,离得到可用的java代码就只有一步之遥了。打开shop.genmodel文件,在根节点上点开右键菜单(见图3),如果只想生 成模型代码选择“Generate Model Code”,如果需要.edit的代码(EMF提供的一些ItemProvider和AdapterFactory,帮助实现编辑器)和可用的编辑器,选 择“Generate All”最方便,这也是例子里选择的方式。EMF的代码生成器为模型、.edit、编辑器和测试代码各生成一个插件项目 (com.my.shop、com.my.shop.edit、com.my.shop.editor和com.my.shop.tests),前三个是后者依赖前者的关系。这里插上一句,即使没有 用EMF的项目,也建议把模型和界面使用不同的插件项目分开,这样做有很多好处,主要是灵活性大大提高了。

file
图3 从菜单里选择生成部分或全部代码

现在可以运行起来看看效果了,注意我们甚至连一句代码也没有写呢。EMF为我们生成了一个新建向导(New Wizard),利用这个向导可以生成新的Shop实例,注意在第三步要选择以Shop类为根类型。编辑器的运行界面如图4所示,它的外观虽然有待改进,但功能已 经足够我们对网上商店里的类别和产品进行编辑了。在以后的帖子里,我们要对网上商店的ecore模型和编辑器的界面做一些修改。

file
图4 缺省的编辑器界面

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/11/13/275054.html

EMF介绍系列(一、EMF与MDA)

接触Eclipse一段时间的朋友应该都听说过EMF这个名字,EMF是Eclipse Modeling Framework的缩写,它是Eclipse的一个重要的子项目,如果翻译成中文就是“Eclipse建模框架”。其实只从这个名字还真是难以确定它的 作用是什么,我认为要完全掌握EMF应该对模型驱动开发(MDA)有一定的了解,而EMF可以看作是Eclipse上的MDA一个实现(代码生成是MDA 的重要组成部分之一),它能够生成在Eclipse上执行的代码。可惜我对MDA没有系统研究过,对EMF的应用大多是为了减少模型修改带来的影响,所以 也希望EMF能带我进入MDA的世界。

MDA讲究的是把模型和应用系统实现分开,模型是最重要的部分,可以说有了清楚的模型,就完成了一半的工作。模型是由元模型(Meta Model)定义的,例如UML里“类”和“属性”这些概念是在UML的元模型里定义的,而元模型又是由“元元模型”来定义,后者多是自描述的,也就是能 够自己定义自己,所以很少见到“元元元模型”的概念。在MOF规范里, 元元模型处于M3层,元模型处于M2层,往下的M1层是模型,而M0层是实例。EMF定义了一套Ecore元模型,该模型是EMOF(MOF的一个子集, MOF是Meta-Object Facility的缩写)的一个实现,这是一个自描述的模型,可以认为它处于MOF中的M2层,即与UML元模型相同的位置。用Ecore元模型可以定义 ecore模型,也就是.ecore文件,这个模型处于M1层,而ecore模型的实例处于M0层。关于MOF的更多概念请参考MOF规范和相关文档, MDA的各种概念是相当多的,研究它的人也很多,我认为EMF算是比较务实的一派。如果以后有机会深入研究Ecore元模型,我也会把心得写在这里供大家 参考。

EMF自发布以来一直受到Eclipse社区的热情拥护,目前很多Eclipse的子项目都是基于它开发的,可见EMF确实能给开发者带来好处。随着EMF的成长,出现了越来越多的文档,在eclipse.org/emf上就可以找到不少,最全面和权威的当属这本Eclipse Modeling Framework A Developers Guide, 完整的讲解了EMF,虽然针对的版本较早,但绝大部分内容还是适用的;作为入门读物,网站上Documents里列出的一些教程也是不错的选择;EMF的 新闻组更是一个很好的交流场所,Ed Merks(EMF设计师之一)和其它几位开发人员可以说是有问必答,感谢他们的认真态度。

通过在一些项目里使用EMF,我也逐渐感觉了到它起到的作用,特别是当模型里各种元素和关系比较多时,EMF的代码生成功能会节省不少工作量,对于 我们开发人员来讲,这不就是最大的好处吗。其实用EMF构造一个应用的步骤很简单:1、构造模型,2、生成代码,前者可以通过UML类图、Java接口、 XML Schema等多种方式定义,后者可以选择只生成模型部分的代码,也可以同时生成编辑器部分的代码,对这个编辑器做一些定制就可以得到符合需求的应用程 序。

类图可以帮助我们直观的了解应用系统里各对象的关系,但在开发过程中,类图里的定义很可能被修改,如果这一修改没有及时反映回类图,类图就会逐渐变 得不准确而失去作用。但是保持代码和类图的一致是一件很烦琐的工作,有时由于项目管理的需要,类图又必须保证能够反映系统的真实结构。比较好的解决方法是 让代码由类图直接生成,模型需要修改时也在类图上做改动,并且重新生成代码,这正是EMF的专长。

说了这么多,你可能还是没弄明白EMF到底能为我们带来哪些好处,是怎样为我们节省工作量的,从下个帖子开始我们将一步步了解怎样使用EMF构造应用程序。因为是边用边写,所以这个系列的帖子都不会太长,相信后面部分会以心得和技巧等内容为主。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/11/12/275024.html

EMF+GEF的属性页问题

最近有朋友问使用EMF作为GEF模型时,如何在选中editpart时在属性页里显示属性的问题。是的,因为GEF是这样判断是否填充属性 页的:

public Object getAdapter(Class key) {
    if (IPropertySource.class == key) {
        if (getModel() instanceof IPropertySource)
            return getModel();
        if (getModel() instanceof IAdaptable)
            return ((IAdaptable)getModel()).getAdapter(key);
    }
    if (AccessibleEditPart.class == key)
        return getAccessibleEditPart();
    return null;
}

所以,一般(不使用EMF)我们让模型类实现IPropertySource接口即可看到属性。而用EMF生成的模型类是不实现这个接口的,因此用户在界面上选中 editpart时属性页里只能是空白。

要解决这个问题,一种方式是覆盖editpart的getAdapter()方法,返回一个自定义的PropertySource, 这个办法比较直接,但那么多属性写起来很麻烦,更重要的是当ecore模型改变后这些属性是不会跟着变的;另一种方式是在editor类里作文章,工作量 比较小,具体办法如下:

ModelItemProviderAdapterFactory adapterFactory;
AdapterFactoryContentProvider adapterFactoryConentProvider;

//Constructor of the editor
public TobeEditor() {
    setEditDomain(new DefaultEditDomain(this));
    //For getting propertysource from emf.edit
    adapterFactory = new ModelItemProviderAdapterFactory();
    adapterFactoryConentProvider = new AdapterFactoryContentProvider(adapterFactory);
}

public Object getAdapter(Class type) {
    if (type == IContentOutlinePage.class)
        return new OutlinePage();
    if (type == org.eclipse.ui.views.properties.IPropertySheetPage.class) {
        PropertySheetPage page = new PropertySheetPage();
        UndoablePropertySheetEntry root = new UndoablePropertySheetEntry(getCommandStack());
        root.setPropertySourceProvider(new IPropertySourceProvider() {
            public IPropertySource getPropertySource(Object object) {
                if (object instanceof EditPart) {
                    Object model = ((EditPart) object).getModel();
                    return new PropertySource(model, (IItemPropertySource) adapterFactory.adapt(model,
                            IItemPropertySource.class));
                } else {
                    return adapterFactoryConentProvider.getPropertySource(object);
                }
            }
        });
        page.setRootEntry(root);
        return page;
    }
    return super.getAdapter(type);
}

也就是对UndoablePropertySheetEntry做一些处理,让它能够适应editpart的选择(GEF里选中元素的都是 editpart而非model本身)。这个方法在显示属性方面没有什么问题,但在属性页里修改属性值后,是不能undo的,而且不会显示表示dirty 的*号,所以还有待改进。

EMF+GEF里像这种别扭的地方还远不只这一处,不过我相信大部分都是可以适当修改一些代码解决的,希望它们之间增加一些合作,同时继续期待GMF

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/10/24/261106.html

把GEF放在ViewPart里

其实GEF可以放在任何Composite上,当然也就可以放在视图里了。关键任务是创建GraphicalViewer、RootEditPart、EditDomain和EditPartFactory这些对象,下面的代码是我从别处拷来的,稍微修改了一下。

public class TestView extends ViewPart {
    ScrollingGraphicalViewer graphicalViewer;
    FigureCanvas canvas;
    Diagram diagram;

    public void createPartControl(Composite parent) {
        graphicalViewer = new ScrollingGraphicalViewer();
        canvas = (FigureCanvas) graphicalViewer.createControl(parent);
        ScalableFreeformRootEditPart root = new ScalableFreeformRootEditPart();
        graphicalViewer.setRootEditPart(root);
        graphicalViewer.setEditDomain(new EditDomain());
        graphicalViewer.setEditPartFactory(new PartFactory());
        graphicalViewer.setContents(diagram);
    }
}

运行结果如下,这个基本上只有视图的功能,也可以增加编辑功能,例如对GraphicalViewer加一个DropTargetListener就可以从调色板里拉对象上来了,等等。这个代码有点问题,就是打开View后要调整一下大小才能显示出图形,该怎么解决呢……

file

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/10/21/259500.html