GEF的设计没有对模型部分做任何限制,也就是说,我们可以任意构造自己的模型,唯一 须要保证的就是模型具有某种消息机制,以便在发生变化时能够通知GEF(通过EditPart)。 在以前的几个例子里,我们都是利用java.beans包中的PropertyChangeSupport和 PropertyChangeListener来实现消息机制的,这里将介绍一下如何让GEF利用EMF构造的模 型.
EMF使用自己定义的Ecore作为元模型,在这个元模型里定义了EPackage、EClassifier、 EFeature等等概念,我们要定义的模型都是使用这些概念来定义的。同时因为ecore中的所有 概念都可以用本身的概念循环定义,所以ecore又是自己的元模型,也就是元元模型。关于 ecore的详细概念,请参考EMF网站上的有关资料。
利用EMF为我们生成模型代码可以有多种方式,例如通过XML Schema、带有注释的Java接 口、Rose的mdl文件以及.ecore文件等,EMF的代码生成器需要一个扩展名为.genmodel的文件 提供信息,这个文件可以通过上面说的几种方式生成,我推荐使用Omondo公司的EclipseUML 插件来构造.ecore文件,该插件的免费版本可以从这里下载。(也许需要使用国外代理才能 访问omondo网站)
图1 示例模型
为了节约篇幅和时间,我就不详细描述构造EMF项目的步骤了,这里主要把使用EMF与非 EMF模型的区别做一个说明。图1是例子中使用的模型,其中Dimension和Point是两个外部 java类型,由于EMF并不了解它们,所以定义为datatype类型。
使用两个Plugins
为了让模型与编辑器更好的分离,可以让EMF模型单独位于一个Plugin中(名为 SubjectModel),而让编辑器Plugin (SubjectEdiTor)依赖于它。这样做的另一个好处是 ,当修改模型后,如果你愿意,可以很容易的删除以前生成的代码,然后全部重新生成。
EditPart中的修改
在以前我们的EditPart是实现java.beans.PropertyChangeListener接口的,当模型改用 EMF实现后, EditPart应改为实现org.eclipse.emf.common.notify.Adapter接口,因为EMF 的每个模型对象都是 Notifier,它维护了一个Adapter列表,可以把Adapter作为监听器加入 到模型的这个列表中。
实现Adapter接口时须要实现getTarget()和setTarget()方法,target代表发出消息的那 个模型对象。我的实现方式是在EditPart里维护一个Notifier类型的target变量,这两个方 法分别返回和设置该变量即可。
还要实现isAdapterForType()方法,该方法返回一个布尔值,表示这个Adapter是否应响 应指定类型的消息,我的实现一律为”return type.equals(getModel().getClass());”。
另外,propertyChanged()方法的名称应改为notifyChanged()方法,其实现的功能和以前 是一样的,但代码有所不同,下面是NodePart中的实现,看一下就应该明白了:
public void notifyChanged(Notification notification) { int featureId = notification.getFeatureID(ModelPackage.class); switch (featureId) { case ModelPackage.NODE__LOCATION: case ModelPackage.NODE__SIZE: refreshVisuals(); break; case ModelPackage.NODE__INCOMING_CONNECTIONS: refreshTargetConnections(); break; case ModelPackage.NODE__OUTGOING_CONNECTIONS: refreshSourceConnections(); break; }}
还有active()/deactive()方法中的内容需要修改,作用还是把EditPart自己作为Adapter (不是 PropertyChangeListener了)加入模型的监听器列表,下面是SubjectPart的实现, 其中eAdapters()得到监听器列表:
public void activate() { super.activate(); ((Subject)getModel().eAdapters()).add(this);}
可以看到,我们对EditPart所做的修改实际是在两种消息机制之间的转换,如果你对以前 的那套机制很熟悉的话,这里理解起来不应该有任何困难。
ElementFacTory的修改
这个类的作用是根据template创建新的模型对象实例,以前的实现都是”new XXX()”这样 ,用了EMF以后应改为”ModelFacTory.eINSTANCE.createXXX()”,EMF里的每个模型对象实例 都应该是使用工厂创建的。
public Object getNewObject() { if (template.equals(Diagram.class)) return ModelFacTory.eINSTANCE.createDiagram(); else if (template.equals(Subject.class)) return ModelFacTory.eINSTANCE.createSubject(); else if (template.equals(Attribute.class)) return ModelFacTory.eINSTANCE.createAttribute(); else if (template.equals(Connection.class)) return ModelFacTory.eINSTANCE.createConnection(); return null;}
使用自定义CreationFacTory代替SimpleFacTory
在原先的PaletteFacTory里定义CreationEntry时都是指定SimpleFacTory作为工厂,这个 类是使用 Class.newInstance()创建新的对象实例,而用EMF作为模型后,创建实例的工作应 该交给ModelFacTory来完成,所以必须定义自己的CreationFacTory。(注意,示例代码里没 有包含这个修改。)
处理自定义数据类型
我们的Node类里有两个非标准数据类型:Point和Dimension,要让EMF能够正确的将它们 保存,必须提供序列化和反序列化它们的方法。在EMF为我们生成的代码里,找到 ModelFacToryImpl类,这里有形如convertXXXToString()和 createXXXFromString()的几个 方法,分别用来序列化和反序列化这种外部数据类型。我们要把它的缺省实现改为自己的方 式,下面是我对 Point的实现方式:
public String convertPointToString(EDataType eDataType, Object instanceValue) { Point p = (Point) instanceValue; return p.x + "," + p.y;}public Point createPointFromString(EDataType eDataType, String initialValue) { Point p = new Point(); String[] values = initialValue.split(","); p.x = Integer.parseInt(values[0]); p.y = Integer.parseInt(values[1]); return p;}
注意,修改后要将方法前面的@generated注释删除,这样在重新生成代码时才不会被覆盖 掉。要设置使用这些类型的变量的缺省值会有点问题(例如设置Node类的location属性的缺 省值),在EMF自带的Sample Ecore Model EdiTor里设置它的defaultValueLiteral 为”100,100″(这是我们通过convertPointToString()方法定义的序列化形式)会报一个错, 但不管它就可以了,在生成的代码里会得到这个缺省值。
保存和载入模型
EMF通过Resource管理模型数据,几个Resource放在一起称为ResourceSet。前面说过,要 想正常保存模型,必须保证每个模型对象都被包含在Resource里,当然间接包含也是可以的 。比如例子这个模型,Diagram是被包含在Resource里的(创建新Diagram 时即被加入),而 Diagram包含Subject,Subject包含Attribute,所以它们都在Resource里。在图1中可以看到 , Diagram和Connection之间存在一对多的包含关系,这个关系的主要作用就是确保在保存 模型时不会出现 DanglingHREFException,因为如果没有这个包含关系,则Connection对象 不会被包含在任何Resource里。
在删除一个对象的时候,一定要保证它不再包含在Resource里,否则保存后的文件中会出 现很多空元素。比较容易犯错的地方是对 Connection的处理,在删除连接的时候,只是从源 节点和目标节点里删除对这个连接的引用是不够的,因为这样只是在界面上消除了两个节点 间的连接线,而这个连接对象还是包含在Diagram里的,所以还要调用从Diagram对象里删除 它才对,DeleteConnectionCommand中的代码如下:
public void execute() { source.getOutgoingConnections().remove(connection); target.getIncomingConnections().remove(connection); connection.getDiagram().getConnections().remove(connection);}
当然,新建连接时也不要忘记将连接添加在Diagram对象里(代码见 CreateConnectionCommand)。保存和载入模型的代码请看SubjectEdiTor的init()方法和 doSave()方法,都是很标准的EMF访问资源的方法,以下是载入的代码(如果是新创建的文件 ,则在Resource中新建Diagram对象):
public void init(IEdiTorSite site, IEdiTorInput input) throws PartInitException { super.init(site, input); IFile file = ((FileEdiTorInput) getEdiTorInput()).getFile(); URI fileURI = URI.createPlatformResourceURI(file.getFullPath().toString ()); resource = new XMIResourceImpl(fileURI); //注意要区分XMIResource和 XMLResource try { resource.load(null); diagram = (Diagram) resource.getContents().get(0); } catch (IOException e) { diagram = ModelFacTory.eINSTANCE.createDiagram(); resource.getContents().add(diagram); }}
虽然到目前为止我还没有机会体会EMF在模型交互引用方面的优势,但经过进一步的了解 和在这个例子的应用,我对EMF的印象已有所改观。据我目前所知,使用EMF模型作为GEF的模 型部分至少有以下几个好处:
只需要定义一次模型,而不是类图、设计文档、Java代码等等好几处;
EMF为模型提供了完整的消息机制,不用我们手动实现了;
EMF提供了缺省的模型持久化功能(xmi),并且允许修改持久化方式;
EMF的模型便于交叉引用,因为拥有足够的元信息,等等。
此外,EMF.Edit框架能够为模型的编辑提供了很大的帮助,由于我现在对它还不熟悉,所 以例子里也没有用到,今后我会修改这个例子以利用EMF.Edit。
本文配套源码
无论何时何地,只要创造就有收获,只有不息的奋进,才能证明生命的存在。