Operazioni annullabili

Esistono molti modi diversi di fornire azioni al workbench, ma finora non è stata posta attenzione sull'implementazione del metodo run() di un'azione. Il meccanismo del metodo dipende dall'azione specifica, ma una struttura del codice del tipo operazione annullabile consente all'azione di partecipare al supporto di annullamento/ripetizione delle operazioni della piattaforma.

La piattaforma fornisce un framework delle operazioni annullabili nel pacchetto org.eclipse.core.commands.operations. Attraverso l'implementazione del codice all'interno di un metodo run() per creare una IUndoableOperation, l'operazione viene resa disponibile per l'annullamento/ripetizione. La conversione di un'azione per l'utilizzo delle operazioni risulta semplice, tranne per l'implementazione del comportamento di annullamento/ripetizione.

Scrittura di un'operazione annullabile

Come primo esempio, consideriamo il semplice ViewActionDelegate fornito nel plugin di esempio del readme. Quando viene richiamata, l'azione avvia semplicemente una finestra di dialogo nella quale viene segnalata la sua esecuzione.

public void run(org.eclipse.jface.action.IAction action) {
MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed")); 
}
Utilizzando le operazioni, il metodo run è responsabile della creazione di un'operazione che esegue le attività precedentemente eseguite nel metodo run e della richiesta ad una cronologia operazioni di eseguire l'operazione, in modo che sia registrata per l'annullamento e la ripetizione.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
L'operazione racchiude il precedente comportamento del metodo run, oltre all'annullamento/ripetizione dell'operazione.
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;
	}
}

Per azioni semplici, è possibile spostare tutto il funzionamento nella classe di operazioni. In questo caso, può essere opportuno ridurre le precedenti classi di azioni in un'unica classe parametrizzata. L'azione eseguirà semplicemente l'operazione fornita quando richiesto. Questa è fondamentalmente una decisione di progettazione dell'applicazione.

Quando un'azione avvia una procedura guidata, l'operazione viene generalmente creata come parte del metodo performFinish() della procedura guidata oppure del metodo finish() della pagina della procedura guidata. La conversione del metodo finish per l'utilizzo delle operazioni è simile alla conversione di un metodo run. Il metodo è responsabile per la creazione ed esecuzione di un'operazione che effettua le attività precedentemente svolte in modo incorporato.

Cronologia operazioni

Ora verrà illustrata la funzione di una cronologia operazioni. Di seguito viene riportato il codice che crea l'operazione di esempio.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
Quale è lo scopo della cronologia operazioni? IOperationHistory definisce l'interfaccia per l'oggetto che tiene traccia di tutte le operazioni annullabili. Quando una cronologia operazioni esegue un'operazione, prima esegue l'operazione e poi la aggiunge alla cronologia dell'annullamento operazioni. I client che effettuano l'annullamento/ripetizione delle operazioni, utilizzano il protocollo IOperationHistory.

La cronologia operazioni utilizzata da un'applicazione può essere richiamata in diversi modi. Il modo più semplice è utilizzare OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

Anche il workbench può essere utilizzato per richiamare la cronologia operazioni. Il workbench configura la cronologia operazioni predefinita e fornisce anche il protocollo per accedervi. Il frammento seguente mostra come ottenere la cronologia operazioni dal workbench.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Una volta ottenuta la cronologia operazioni, questa può essere utilizzata per interrogare la cronologia dell'annullamento/ripetizione delle operazioni, per individuare le operazioni successive nell'elenco di annullamento/ripetizione o per annullare/ripetere particolari operazioni. I client possono aggiungere IOperationHistoryListener allo scopo di ricevere le notifiche sulla modifica della cronologia. Un altro protocollo consente ai client di impostare i limiti della cronologia o inviare notifiche ai listener sulle modifiche di una particolare operazione. Prima di analizzare il dettaglio del protocollo, è necessario comprendere il contesto di annullamento operazione.

Contesti di annullamento operazione

Quando si crea un'operazione, viene assegnata ad un contesto di annullamento operazione che descrive il contesto utente in cui è stata eseguita l'operazione originale. Il contesto di annullamento operazione generalmente dipende dalla vista o editor che ha originato l'operazione annullabile. Ad esempio, le modifiche effettuate all'interno di un editor sono spesso locali per l'editor. In questo caso, l'editor dovrebbe creare il proprio contesto di annullamento operazione ed assegnare tale contesto alle operazioni che aggiunge alla cronologia. In questo modo, tutte le operazioni eseguite nell'editor sono considerate locali e semi-private. Gli editor e le viste che operano su un modello condiviso spesso utilizzano un contesto di annullamento operazione relativo al modello che viene gestito. Utilizzando un contesto di annullamento operazione più generale, le operazioni eseguite da una vista o editor possono essere rese disponibili per l'annullamento operazione in un'altra vista o editor che agisce sullo stesso modello.

I contesti di annullamento operazione presentano un comportamento piuttosto semplice; il protocollo per IUndoContext è molto ristretto. Il ruolo principale di un contesto è assegnare "tag" ad una particolare operazione che appartiene a quel contesto di annullamento operazione, per distinguerla da operazioni create in altri contesti di annullamento operazione. Questo consente alla cronologia operazioni di tenere traccia della cronologia globale di tutte le operazioni annullabili eseguite, mentre le viste e gli editor possono filtrare la cronologia di propria competenza utilizzando il contesto di annullamento operazione.

I contesti di annullamento operazione possono essere creati dal plugin che crea le operazioni annullabili, oppure vi si può accedere tramite API. Ad esempio, il workbench fornisce accesso ad un contesto di annullamento operazione che può essere utilizzato per le operazioni dell'intero workbench. In qualsiasi modo siano ottenuti, i contesti di annullamento operazione devono essere assegnati quando si crea un'operazione. Il seguente frammento mostra come ViewActionDelegate del plugin readme assegna un contesto generale di workbench alle proprie operazioni.

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);
}

Perché utilizzare i contesti di annullamento operazione? Perché non si utilizzano cronologie operazioni separate per le diverse viste e editor? L'uso di cronologie operazioni separate presuppone che ogni singola vista o editor conservi la propria cronologia di annullamento operazioni privata, e che l'annullamento operazioni non abbia un significato globale all'interno dell'applicazione. Questo può essere opportuno per alcune applicazioni, nella quali ciascuna vista o editor dovrebbe creare un proprio contesto di annullamento operazione separato. Altre applicazioni possono avere l'esigenza di implementare un annullamento operazioni globale, che si applichi a tutte le operazioni dell'utente, qualsiasi sia la vista o editor in cui sono state effettuate. In questo caso, il contesto del workbench dovrebbe essere utilizzato per tutti i plugin che aggiungono operazioni alla cronologia.

In applicazioni più complesse, l'annullamento operazioni non è né strettamente locale né strettamente globale. Al contrario esiste una sovrapposizione tra i contesti di annullamento operazione. Per ottenere ciò, si assegnano più contesti ad un'operazione. Ad esempio, una vista di workbench IDE può gestire l'intero spazio di lavoro e considerare lo spazio di lavoro il proprio contesto di annullamento operazione. Un editor che apre una particolare risorsa nello spazio di lavoro può considerare le proprie operazioni principalmente locali. Tuttavia, le operazioni eseguite all'interno dell'editor possono in realtà avere effetti sia sulla risorsa che sullo spazio di lavoro nel suo insieme. (Un buon esempio di questa situazione è il supporto del refactoring JDT, che consente di effettuare modifiche strutturali ad un elemento Java mentre si modifica il file di origine). In questi caso, risulta utile riuscire ad aggiungere entrambi i contesti di annullamento all'operazione, in modo che l'annullamento possa essere eseguito sia dall'editor stesso che dalle viste che gestiscono lo spazio di lavoro.

Dopo aver analizzato il funzionamento del contesto di annullamento operazione, analizziamo di nuovo il protocollo per IOperationHistory. Il seguente frammento viene utilizzato per eseguire un annullamento operazione in un contesto:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception 
}
La cronologia riceverà l'ultima operazione eseguita nel contesto fornito e richiederà l'annullamento di questa operazione. È possibile utilizzare un altro protocollo per richiamare tutta la cronologia di annullamento/ripetizione operazioni per un contesto, oppure per trovare l'operazione che verrà annullata/ripetuta in un particolare contesto. Il seguente frammento ottiene l'etichetta dell'operazione da annullare in un determinato contesto.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

Il contesto di annullamento operazione globale, IOperationHistory.GLOBAL_UNDO_CONTEXT, può essere utilizzato per fare riferimento alla cronologia di annullamento operazione globale, ovvero a tutte le operazioni presenti nella cronologia indipendentemente dal contesto specifico. Il seguente frammento riceve la cronologia di annullamento operazione globale.

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

In qualsiasi momento un'operazione venga eseguita, annullata o ripetuta, utilizzando il protocollo della cronologia operazioni, i client possono fornire un controllo dello stato di avanzamento e ulteriori informazioni UI eventualmente richieste per eseguire l'operazione. Queste informazioni sono passate direttamente all'operazione. Nell'esempio originale, l'azione readme genera un'operazione con un parametro shell, che può essere utilizzato per aprire la finestra di dialogo. Invece di mantenere la shell nell'operazione, una soluzione migliore è di passare i parametri ai metodi execute, undo e redo che forniscono le informazioni UI richieste all'esecuzione dell'operazione. Questi parametri sono passati direttamente all'operazione.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
infoAdapter è un IAdaptable che può fornire in maniera minima la Shell da utilizzare quando si avviano le finestre di dialogo. Nell'operazione di esempio, i parametri sarebbero utilizzati nel modo seguente:
	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;
		}
	}
		// do something else...
}

Gestori delle azioni di annullamento/ripetizione operazioni

La piattaforma fornisce gestori di azioni ridestinabili per l'annullamento/ripetizione operazioni standard, che possono essere configurati da viste e editor per fornire il supporto di annullamento/ripetizione operazioni per i rispettivi contesti specifici. Quando viene creato un gestore dell'azione, ad esso viene assegnato un contesto, in modo che la cronologia operazioni sia filtrata in modo appropriato per la specifica vista. I gestori delle azioni si occupano dell'aggiornamento delle etichette di annullamento/ripetizione operazioni per indicare l'operazione corrente, forniscono il controllo dello stato di avanzamento opportuno e le informazioni UI alla cronologia operazioni e facoltativamente eliminano le voci dalla cronologia quando l'operazione corrente non è valida. Viene fornito un gruppo di azioni che crea i gestori delle azioni e li assegna alle azioni globali di annullamento/ripetizione operazioni.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
L'ultimo parametro è un parametro booleano che indica se le cronologie di annullamento/ripetizione operazioni per il contesto specificato devono essere eliminate quando l'operazione attualmente disponibile per l'annullamento/ripetizione non è valida. L'impostazione di questo parametro è correlata al contesto di annullamento/ripetizione operazioni fornito ed alla strategia di convalida utilizzata dalle operazioni appartenenti al contesto.

Modelli di annullamento operazione delle applicazioni

In precedenza è stato illustrato l'utilizzo dei contesti di annullamento operazione per l'implementazione dei diversi tipi di modelli di annullamento operazione delle applicazioni. La capacità di assegnare uno o più contesti alle operazioni consente alle applicazioni di implementare strategie di annullamento operazione che sono locali per ciascuna vista o editor, globali per tutti i plugin, o un modello misto. Un'altra decisione che deve essere presa in fase di progettazione è se un'operazione può essere annullata/ripetuta in qualsiasi momento, oppure se il modello è rigidamente lineare, considerando solo l'operazione più recente per l'annullamento/ripetizione.

IOperationHistory definisce il protocollo che consente modelli di annullamento operazione flessibili, lasciando alle implementazioni la facoltà di stabilire cosa è consentito. Il protocollo dell'annullamento/ripetizione delle operazioni presuppone che esista una sola operazione disponibile per l'annullamento/ripetizione in un determinato contesto di annullamento operazione. Viene fornito un ulteriore protocollo per consentire ai client di eseguire un'operazione specifica, qualsiasi sia la sua posizione nella cronologia. La cronologia operazioni può essere configurata in modo da implementare il modello appropriato per un'applicazione. Questo si realizza con un'interfaccia utilizzata per la pre-approvazione delle richieste di annullamento/ripetizione prima che siano effettuate.

Revisori dell'operazione

IOperationApprover definisce il protocollo per l'approvazione dell'annullamento/ripetizione di una particolare operazione. Un revisore dell'operazione viene installato in una cronologia operazioni. Revisori di operazioni specifici possono verificare la validità di tutte le operazioni, verificare solo le operazioni di determinati contesti oppure richiedere l'intervento dell'utente quando si verificano condizioni impreviste per un'operazione. Il frammento seguente mostra come un'applicazione può configurare la cronologia operazioni per applicare un modello di annullamento lineare per tutte le operazioni.
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// set an approver on the history that will disallow any undo that is not the most recent operation
history.addOperationApprover(new LinearUndoEnforcer());

In questo caso, nella cronologia viene installato un revisore dell'operazione fornito dal framework, LinearUndoEnforcer per evitare l'annullamento/ripetizione di operazioni che non siano le ultime effettuate/annullate in tutti i contesti di annullamento operazioni.

Un altro revisore di operazioni, LinearUndoViolationUserApprover, rileva le stesse condizioni e richiede all'utente se si deve procedere con l'operazione. Questo revisore di operazioni può essere installato in una specifica parte del workbench.

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// set an approver on this part that will prompt the user when the operation is not the most recent.
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

Gli sviluppatori di plugin sono liberi di sviluppare ed installare i propri revisori di operazioni per l'implementazione di modelli di annullamento e strategie di approvazione specifiche dell'applicazione.

Annullamento operazioni e workbench IDE

In precedenza sono stati mostrati i frammenti di codice che utilizzano il protocollo del workbench per accedere alla cronologia operazioni e al contesto di annullamento operazione del workbench. Ciò avviene utilizzando IWorkbenchOperationSupport, che può essere ottenuto dal workbench. Il concetto di contesto di annullamento operazione dell'intero workbench è abbastanza generico. È compito dell'applicazione workbench determinare l'ambito specifico connesso al contesto di annullamento operazione del workbench, e quali viste o editor devono utilizzare il contesto del workbench quando viene fornito il supporto per l'annullamento operazione.

Nel caso del workbench IDE di Eclipse, il contesto di annullamento operazione del workbench deve essere assegnato a qualsiasi operazione che ha effetto sullo spazio di lavoro nel suo complesso. Questo contesto viene utilizzato dalle viste per gestire lo spazio di lavoro, ad esempio il pannello di selezione. Il workbench IDE installa un adattatore nello spazio di lavoro per IUndoContext che restituisce il contesto di annullamento operazione del workbench. Questa registrazione basata su modelli consente ai plugin di gestire lo spazio di lavoro per ottenere il contesto di annullamento operazione appropriato, anche se sono di tipo headless e privi di riferimento alle classi del workbench.

// get the operation history
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// obtain the appropriate undo context for my model
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// create an operation and assign it the context
}

Si consiglia di utilizzare la stessa tecnica di registrazione dei contesti di annullamento operazione basata su modelli per gli altri plugin.