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的時候常會遇到一些問題,需要額外的代碼幫助解決,請參考這兩處討論。
最後要說一句,CreateChildCommand有點特殊,它是.Edit命令但不繼承 AbstractOverrideableCommand,而且如果想在創建子元素時自動完成一些工作,不應該通過 擴展這個類完成,而應該在 XXXItemProvider的collectNewChildDescriptors()方法裡處理 ,這個方法決定每個對象可以創建哪些子元素,你可以修改它的代碼以對新建的元素做一些 處理。