可復原的作業

我們已經看到有許多不同的方法可以對工作台執行動作, 但我們尚未將重點放在動作的 run() 方法的實作。方法的機制是根據有問題的特定動作而定, 但是如果將程式碼建構成可復原的作業,就能讓動作參與平台的復原和重做支援。

平台在 org.eclipse.core.commands.operations 套件中 提供一個可復原的作業組織架構。 藉由在 run() 方法內實作程式碼來建立 IUndoableOperation, 就可以在作業中執行復原和重做。將動作轉換成使用作業是直接明確的, 與實作復原和重做行為本身無關。

撰寫可復原的作業

我們將從一個非常簡單的範例開始。 在 Readme 範例外掛程式中曾經提供一個簡易的 ViewActionDelegate。在呼叫它時, 動作就會啟動一個對話框來宣告它已經執行。

public void run(org.eclipse.jface.action.IAction action) {      MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),
		MessageUtil.getString("View_Action_executed")); 
}
使用作業時,run 方法會負責建立一個作業(該作業會完成 run 方法之前所完成的工作), 並負責要求作業歷程執行作業,以記得復原和重做。
public void run(org.eclipse.jface.action.IAction action) {	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
作業會從 run 方法來封裝舊的行為,同時也會封裝作業的復原和重做。
class ReadmeOperation extends AbstractOperation {
	Shell shell;
	public ReadmeOperation(Shell shell) {
		super("Readme Operation");
		this.shell = shell;
	}
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Readme_Editor"),
		MessageUtil.getString("View_Action_executed")); 
		return Status.OK_STATUS;
	}
	public IStatus undo(IProgressMonitor monitor) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Readme_Editor"),
			"Undoing view action"));   
		return Status.OK_STATUS;
	}
	public IStatus redo(IProgressMonitor monitor) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Readme_Editor"),
			"Redoing view action"));   
		return Status.OK_STATUS;
	}
}

對於簡單的動作,可能會將所有具體細節工作移至作業類別。 在此情況下,可能適合把先前的動作類別收合到已參數化的單一動作類別中。動作要執行時,只會執行所提供的作業。 這大部分是應用程式設計決策。

當動作啟動精靈時,通常會將作業建立成精靈 performFinish() 方法或精靈頁面 finish() 方法的一部分。將 finish 方法轉換成使用作業與轉換 run 方法很類似。 方法會負責建立和執行一個作業,該作業會執行先前在行內完成的工作。

作業歷程

目前,我們已經使用過作業歷程,但並未加以詳細說明。 且讓我們再看一次用來建立範例作業的程式碼。

public void run(org.eclipse.jface.action.IAction action) {	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
作業歷程到底是什麼? IOperationHistory 定義了物件的介面來追蹤所有可以復原的作業。當作業歷程執行作業時, 它會先執行作業,然後將它新增至復原歷程中。 想要復原和重做作業的用戶端可以利用 IOperationHistory 通訊協定來執行這些作業。

應用程式所使用的作業歷程可以用幾種方式來擷取。最簡單的方法是使用 OperationHistoryFactory

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

工作台也可以用來擷取作業歷程。工作台可配置預設的作業歷程,同時提供通訊協定來存取它。下列片段示範如何從工作台取得作業歷程。

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
一旦取得作業歷程,就可以用它來查詢復原或重做歷程、找出下一個要復原或重做的作業, 或用來復原或重做特定作業。用戶端可以新增一個 IOperationHistoryListener 來接收關於歷程變更的通知。其他通訊協定可讓用戶端設定歷程限制,或通知接聽器關於特定作業的變更。在仔細介紹通訊協定之前, 我們需要瞭解復原環境定義

復原環境定義

建立作業時,會為該作業指派一個復原環境定義,以說明執行原始作業的使用者環境定義。復原環境定義通常是根據產生可復原作業的視圖或編輯器而定。例如,編輯器的內部變更通常都位於該編輯器。在此情況下,編輯器應該建立自己的復原環境定義,並將該環境定義指派到它加至歷程中的作業。因此,所有在編輯器中執行的作業都會被視為本端,而且是半私密的。操作共用模型的編輯器或視圖通常會使用與所操作的模型相關的復原環境定義。如果使用更普遍的復原環境定義,則某個視圖或編輯器所執行的作業,就可以在另一個操作相同模型的視圖或編輯器中執行復原。

復原環境定義的行為非常簡單;IUndoContext 的通訊協定相當小。 環境定義的主要角色是將特定的作業「標示」為隸屬於該復原環境定義,以區別該作業以及在不同的復原環境定義中建立的其他作業。 這樣就能讓作業歷程追蹤所有已經執行的可復原作業的廣域歷程, 而視圖和編輯器可以過濾歷程,以獲得使用復原環境定義的特定觀點。

復原環境定義可以由負責建立可復原作業的外掛程式來建立,或者透過 API 來存取。例如, 工作台對可用於整體工作台作業之復原環境定義提供存取權。然而,您必須在建立作業時指派復原環境定義才能將之取得。 下列片段顯示 Readme 外掛程式的 ViewActionDelegate 如何指派工作台環境定義給它的作業。

public void run(org.eclipse.jface.action.IAction action) {	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
	IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
	IUndoContext undoContext = workbench.getOperationSupport().getUndoContext();
	operation.addContext(undoContext);
	operationHistory.execute(operation, null, null);
}

為何要使用復原環境定義?為何不使用個別的作業歷程來處理個別的視圖和編輯器?使用個別的作業歷程時, 會假設任何特定視圖或編輯器都會維護自己的專用復原歷程,而復原在應用程式中並沒有整體意義。這對某些應用程式來說是適當的, 而且在這些情況下,每一個視圖或編輯器都應該建立自己的個別復原環境定義。而其他的應用程式可能會實作適用於所有使用者作業的廣域復原, 而不管原先產生作業的視圖或編輯器為何。在此情況下,工作台環境定義應該由所有新增作業至歷程的外掛程式來使用。

在更複雜的應用程式中,復原並不是全然本端或廣域。相反的,這兩種復原環境定義之間仍有部分交集。這可以藉由指派多個環境定義給一個應用程式來達成。例如, IDE 工作台視圖可以操作整個工作區,並且將工作區視為其復原環境定義。在工作區中特定資源開啟的編輯器可能會將其大部分的作業視為本端。然而, 在編輯器內執行的作業事實上可能會同時影響特定的資源和整個工作區。(這個狀況的一個典型範例是 JDT 重構支援, 它能在編輯程式檔時,容許對 Java 元素進行結構變更)。在這些狀況下,如果能夠新增復原環境定義至作業(以便從編輯器本身執行復原), 以及新增至用來操作工作區的視圖,就非常有用。

現在我們已經瞭解復原環境定義的作用,讓我們再回頭來說明 IOperationHistory 的通訊協定。 下列片段是用來對某些環境定義執行復原:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception 
}
歷程將取得最近執行的作業中具有給定環境定義的作業,然後要求它自行復原。 其他通訊協定也可以用來取得環境定義的整個復原或重做歷程,或者用來尋找將在特殊環境定義中復原或重做的作業。下列片段可取得將在特殊環境定義中復原的作業的標籤。
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

廣域的復原環境定義 (IOperationHistory.GLOBAL_UNDO_CONTEXT) 可用來參照廣域復原歷程。亦即,參照歷程中的所有作業(不論作業的環境定義是否特殊)。下列片段可取得廣域的復原歷程。

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
IUndoableOperation [] undoHistory = operationHistory.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);

每次使用作業歷程通訊協定來執行、復原或重做作業時,用戶端都可以提供一個進度監視器, 以及執行作業可能需要的其他 UI 資訊。 這項資訊會傳送到作業本身。在我們的原始範例中, Readme 動作已建構一個具備 Shell 參數的作業,這個參數可用來開啟對話框。您可以不要在作業中儲存 Shell, 更好的方法是將參數傳送到可提供執行作業所需之任何 UI 資訊的執行、復原和重做方法。這些參數將傳送到作業本身。

public void run(org.eclipse.jface.action.IAction action) {	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
infoAdapter 是一個 IAdaptable, 它至少能提供啟動對話框時可用的 Shell。 我們的範例作業將以下列方式來使用這個參數:
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
		if (info != null) {
			Shell shell = (Shell)info.getAdapter(Shell.class);
			if (shell != null) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Readme_Editor"),
		MessageUtil.getString("View_Action_executed")); 
		return Status.OK_STATUS;
		}
	}
		// 執行其他事項...
}

復原和重做動作處理常式

平台提供了標準復原和重做的可重新設定目標動作處理常式, 它們可以由視圖和編輯器來配置,以便為其特定環境定義提供復原和重做支援。建立動作處理常式時, 會為其指派環境定義,以使用適合該特定視圖的過濾方式來過濾作業歷程。 動作處理常式可處理復原和重做標籤的更新作業來顯示有問題的現行作業、 提供適當的進度監視器和 UI 資訊給作業歷程, 以及當現行作業無效時選擇性刪改歷程。為了方便,在此提供一個動作群組, 它可以建立動作處理常式,並且將它們指派給廣域的復原和重做動作。

new UndoRedoActionGroup(this.getSite(), undoContext, true);
最後的參數是一個 Boolean,它指出當目前可執行復原或重做的作業無效時, 是否應該刪除指定環境定義的復原和重做歷程。這個參數的設定與該環境定義的作業提供的復原環境以及使用的驗證策略有關。

應用程式復原模型

稍早,我們說明了如何使用復原環境定義來實作不同類型的應用程式復原模型。 如果可以指派一或多個環境定義給作業,就可以允許應用程式實作每一個視圖或編輯器本端的復原策略、 或實作所有外掛程式廣域的復原策略,或實作介於以上兩者之間的部分模型。 另一個與復原和重做有關的設計決策是關於任何作業是否都能隨時復原或重做, 或者模型是不是完全線性,而且只考量復原或重做最近的作業。

IOperationHistory 會定義一個通訊協定,以容許用彈性復原模型,並且由個別的實作來決定是否允許這個通訊協定。 目前,我們所看到的復原和重做通訊協定都是假設在特定復原環境定義中, 只有一個隱含的作業可用於復原或重做。其他提供的通訊協定都是為了容許用戶端執行特定作業, 而作業在歷程中的位置則無關緊要。您可以配置作業歷程來實作適合應用程式的模型。您可以透過一個介面來執行這個步驟, 該介面可以在復原或重做作業之前,預先核准任何復原或重做要求。

作業核准器

IOperationApprover 可定義一個通訊協定來核准特定作業的復原和重做。作業核准器安裝在作業歷程中。特定的作業核准器會輪流檢查所有作業的有效性、僅檢查某些環境定義的作業, 或在作業發生非預期的狀況時,提示使用者。 下列片段顯示應用程式如何配置作業歷程,以為所有作業執行線性復原模型。
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// 在歷程中設定核准器,以禁止不是最近作業的任何復原作業
history.addOperationApprover(new LinearUndoEnforcer());

在此情況下,組織架構 LinearUndoEnforcer 所提供的作業核准器 會安裝在歷程中,防止執行不是最近完成之作業或復原環境定義中所有復原作業的復原或重做作業。

另一個作業核准器 (LinearUndoViolationUserApprover) 則偵測相同的情況,並提示使用者是否要允許作業繼續執行。這個作業核准器可安裝在特定的工作台組件中。

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// 在這個組件中設定核准器,以在作業不是最近的作業時,提示使用者。
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

外掛程式開發人員可自由開發和安裝自己的作業核准器,以實作特定應用程式專用的復原模型和核准策略。

復原和 IDE 工作台

我們看到程式碼片段使用工作台通訊協定來存取作業歷程和工作台復原環境定義。這是利用 IWorkbenchOperationSupport 來達成, 您可以從工作台取得它。工作台復原環境定義的概念很籠統。工作台應用程式會決定工作台復原環境定義要隱含哪些特定範圍, 以及哪些視圖或編輯器在提供復原支援時,會使用工作台環境定義。

如果是 Eclipse IDE 工作台,則應該將工作台復原環境定義指派給任何會影響整個 IDE 工作區的作業。 負責操作工作區的視圖會使用這個環境定義,例如「資源導覽器」。IDE 工作台會在 IUndoContext 的工作區中安裝一個配接器, 以傳回工作台復原環境定義。這個模型式登錄作業可允許負責操作工作區的外掛程式取得適當的復原環境定義, 即便他們是由遠端控制,而且沒有參照任何工作台類別。

// 取得作業歷程
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// 取得我的模型的適當復原環境定義
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// 建立作業並且為它指派環境定義
}

其他外掛程式也歡迎使用相同的技巧來登錄模型式復原環境定義。