GEF 的設計並未對模型層施加任何限制,開發者可以自由構建自己的模型,唯一的要求是模型必須具備某种通知機制,以便在數據變更時通知 GEF(透過 EditPart)。在先前的範例中,我們使用 java.beans.PropertyChangeSupport 和 PropertyChangeListener 實現此機制。本文將介紹如何讓 GEF 使用基於 EMF(Eclipse Modeling Framework)構建的模型。
EMF 以自定義的 Ecore 作為元模型,其中定義了 EPackage、EClassifier、EFeature 等概念。我們定義的模型皆基於這些概念,且 Ecore 中的所有概念均可用自身概念循環定義,故 Ecore 也是自身的元模型(即元元模型)。關於 Ecore 的詳細說明,請參考 EMF 官方文檔。
EMF 支援多種模型代碼生成途徑:XML Schema、帶註解的 Java 介面、Rose 的 .mdl 檔案以及 .ecore 檔案等。代碼生成器需要一個擴展名為 .genmodel 的檔案提供配置資訊,該檔案可由上述方式生成。推薦使用 Omondo 公司的 EclipseUML 外掛來構建 .ecore 檔案,其免費版本可從官網下載(可能需要國外代理才能訪問)。

圖1 示例模型
為節省篇幅,不詳細描述建立 EMF 專案的步驟,重點說明使用 EMF 與非 EMF 模型的差異。圖1 展示範例模型,其中 Dimension 和 Point 為外部 Java 類型,EMF 不了解它們,故定義為 datatype 類型。
為使模型與編輯器更好分離,可將 EMF 模型置於獨立 Plugin(如 SubjectModel),編輯器 Plugin(SubjectEditor)依賴它。這樣修改模型後,可輕易刪除舊生成代碼並完全重新生成。
先前 EditPart 實現 java.beans.PropertyChangeListener 介面;改用 EMF 後,應改為實現 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;
}
}
activate()/deactivate() 方法內容也需調整,作用仍是將 EditPart 作為 Adapter(而非 PropertyChangeListener)加入模型的監聽器列表。以下為 SubjectPart 實現,eAdapters() 取得監聽器列表:
public void activate() {
super.activate();
((Subject)getModel().eAdapters()).add(this);
}
可見,EditPart 的修改實質是在兩種通知機制間的轉換,若熟悉舊機制,理解起來不會有困難。
此類負責根據 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;
}
原先 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 包含 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 模型部分至少有以下好處:
此外,EMF.Edit 框架能為模型編輯提供極大幫助,因目前不熟悉故範例未用到,未來會修改範例以利用 EMF.Edit。