Funktioner, som kan fortrydes

Vi har set på mange forskellige måder at bidrage med funktioner til arbejdsbænken, men vi har ikke fokuseret på implementeringen af en funktions run()-metode. Metodens mekanismer afhænger af den aktuelle funktion, men hvis du strukturerer koden som en funktion, der kan fortrydes, kan funktionen deltage i platformens understøttelse af fortryd og gentag.

Platformen omfatter en ramme til fortryd-funktioner i pakken org.eclipse.core.commands.operations. Hvis koden implementeres i en run()-metode for at oprette en IUndoableOperation, kan funktionen gøres tilgængelig for fortrydelse og gentagelse. Konvertering af en funktion til at bruge funktioner er ligetil, når der ses bort fra implementeringen af selve fortrydelses- og gentagelsesfunktionerne.

Skriv en funktion, som kan fortrydes

Vi starter med at se på en meget enkelt eksempel. Husk den enkle ViewActionDelegate fra eksemplet på Readme-plugin'en. Når den startes, starter funktionen en dialogboks med en meddelelse om, at den er udført.

public void run(org.eclipse.jface.action.IAction action) {
	MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed")); 
}
Når der bruges funktioner, er run-metoden ansvarlig for at oprette en funktion, som udfører det arbejde, der tidligere er udført i run-metoden, og for at anmode om, at en funktionshistorik udfører funktionen, så den kan huskes til brug for fortrydelse og gentagelse.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
Funktionen indkapsler den gamle funktionsmåde fra run-metoden samt fortrydelse og gentagelse af funktionen.
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, IAdaptable info) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			"Undoing view action");   
		return Status.OK_STATUS;
	}
	public IStatus redo(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			"Redoing view action");   
		return Status.OK_STATUS;
	}
}

For simple funktioner kan det være muligt at flytte alt det grundlæggende arbejde til funktionsklassen. I det tilfælde kan det være relevant at skjule de tidligere funktionsklasser i en enkelt funktionsklasse, der er gjort parametrisk. Funktionen udfører derefter blot den leverede funktion, når det er tid for udførelse. Det er stort set et spørgsmål om programdesign.

Når en funktion starter en guide, oprettes funktionen typisk som del af guidens performFinish()-metode eller en guidesides finish()-metode. Konvertering af finish-metoden til at bruge funktioner ligner konverteringen til en run-metode. Metoden er ansvarlig for at oprette og udføre en funktion, som udfører det arbejde, som tidligere blev udført indvendigt.

Funktionshistorik

Vi har hidtil brugt en funktionshistorik uden virkelig at forklare den. Lad os igen se på den kode, der opretter funktionseksemplet.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
Hvad handler funktionshistorikken om? IOperationHistory definerer grænsefladen for det objekt, der overvåger alle de funktioner, som kan fortrydes. Når en funktionshistorik udfører en funktion, udfører den først funktionen og tilføjer den dernæst til fortrydelseshistorikken. Klienter, der vil bruge fortrydelses- og gentagelsesfunktioner, gør det ved at bruge IOperationHistory-protokol.

Den funktionshistorik, som et program bruger, kan hentes på flere måder. Den nemmeste måde er at bruge OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

Arbejdsbænken kan også bruges til at hente funktionshistorikken. Arbejdsbænken konfigurerer standardfunktionshistorikken og stiller også en protokol til rådighed, som bruges til at få adgang til den. Følgende kodestykke viser, hvordan du henter funktionshistorikken fra arbejdsbænken.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Når en funktionshistorik er hentet, kan den bruges til at forespørge fortrydelses- eller gentagelseshistorikken, finde ud af, hvilken funktion der er den næste i køen til fortrydelse eller gentagelse, eller til at fortryde eller gentage bestemte funktioner. Klienter kan tilføje en IOperationHistoryListener for at kunne modtage beskeder om ændringer i historikken. En anden protokol giver klienter mulighed for at angive grænser for historikken eller at give lyttere besked om ændringer i en bestemt funktion. Før vi ser nærmere på protokollen, skal vi kende til fortrydelseskonteksten.

Fortrydelseskontekster

Når en funktion oprettes, tildeles den en fortrydelseskontekst, som beskriver den brugerkontekst, som den oprindelige funktion blev udført i. Fortrydelseskonteksten afhænger normalt af den oversigt eller editor, som den funktion, der kan fortrydes, stammer fra. Ændringer i en editor er f.eks. ofte lokale for editoren. I dette tilfælde skal editoren oprette sin egen fortrydelseskontekst og tildele konteksten til de funktioner, den tilføjer til historikken. På den måde anses alle funktioner, der udføres i editoren, for lokale og halvprivate. Editorer eller oversigter, der benytter en fælles model, bruger ofte en fortrydelseskontekst, som er relateret til den model, de arbejder med. Hvis der bruges en mere generel fortrydelseskontekst, vil de funktioner, der udføres af en oversigt eller en editor, være tilgængelige for fortrydelse i en anden oversigt eller editor, som bruger den samme model.

Fortrydelseskontekster har en relativ enkel funktionsmåde: protokollen for IUndoContext er temmelig lille. En konteksts overordnede rolle er at "mærke" en bestemt funktion som hørende til denne fortrydelseskontekst, så det er muligt at skelne den fra funktioner, der er oprettet i andre fortrydelseskontekster. Det giver funktionshistorikken mulighed for at overvåge den globale historik for alle udførte funktioner, der kan fortrydes, mens oversigter og editorer kan filtrere historikken for en bestemt synsvinkel ved hjælp af fortrydelseskonteksten.

Fortrydelseskontekster kan oprettes af den plugin, som opretter de funktioner, der kan fortrydes, eller der kan oprettes adgang til dem via API. Arbejdsbænken giver f.eks. adgang til en fortrydelseskontekst, der kan bruges til funktioner på tværs af arbejdsbænken. Uanset hvordan fortrydelseskonteksterne opnås, skal de tildeles, når en funktion oprettes. Følgende kodestykke viser, hvordan Readme-plugin'ens ViewActionDelegate kan tildele en kontekst til sine funktioner på tværs af arbejdsbænken.

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

Hvorfor skal der bruges fortrydelseskontekster? Hvorfor ikke bruge separate funktionshistorikker for separate oversigter og editorer? Hvis der bruges separate funktionshistorikker antages det, at hver oversigt eller editor har sin egen private fortrydelseshistorik, og at fortrydelsen ikke har nogen global betydning i programmet. Det kan være relevant for nogen programmer, og i de tilfælde skal hver oversigt eller editor oprette sin egen separate fortrydelseskontekst. Andre programmer kan have brug for at implementere en global fortrydelse, som gælder for alle brugerfunktioner, uanset den oversigt eller editor de stammer fra. I det tilfælde skal arbejdsbænkskonteksten bruges af alle de plugins, der tilføjer funktioner til historikken.

I mere komplicerede programmer er fortrydelsen ikke strengt lokal eller strengt global. Der er i stedet nogle overlap mellem fortrydelseskonteksterne. Det opnås ved at tildele flere kontekster til en funktion. En IDE-arbejdsbænksoversigt kan f.eks. arbejde med hele arbejdsområdet og betragte arbejdsområdet som sin fortrydelseskontekst. En editor, som åbnes for en bestemt ressource i arbejdsområdet, kan betragte sine funktioner som overvejende lokale. De funktioner, som udføres inde i en editor, kan imidlertid påvirke både den enkelte ressource og arbejdsområdet i det hele taget. Et godt eksempel på et sådan tilfælde er støtten til JDT-refactoring, som tillader, at der sker strukturændringer i et Java-element, når kildefilen redigeres. I disse tilfælde er det nyttigt at kunne tilføje begge fortrydelseskontekster til funktionen, så fortrydelsen kan udføres fra selve editoren og fra de oversigter, som arbejder med arbejdsområdet.

Efter denne gennemgang af fortrydelseskontekster kan vi igen se på protokollen for IOperationHistory. Følgende kodestykke bruges til at udføre en fortrydelse for noget kontekst:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
  try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception 
}
Historikken henter den senest udførte funktion, som indeholder den givne kontekst, og beder den om at fortryde sig selv. Der kan bruges en anden protokol til at hente hele fortrydelses- eller gentagelseshistorikken for en kontekst eller til at finde den funktion, som skal fortrydes eller gentages i en bestemt kontekst. Følgende kodestykke henter etiketten for den funktion, der skal fortrydes i en bestemt kontekst.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

Den globale fortrydelseskontekst, IOperationHistory.GLOBAL_UNDO_CONTEXT, kan bruges til at referere til den globale fortrydelseshistorik. Det vil sige til alle funktionerne i historikken uanset deres specifikke kontekst. Følgende kodestykke henter den globale fortrydelseshistorik.

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

Når en funktion udføres, fortrydes eller gentages ved hjælp af funktionshistorikprotokol, kan klienter stille en statusovervågning og eventuelle yderligere brugergrænsefladeoplysninger til rådighed, som behøves til at udføre funktionen. Oplysningerne sendes til selve funktionen. I det oprindelige eksempel konstruerede Readme-funktionen en funktion med en shell-parameter, der kunne bruges til at åbne dialogboksen. I stedet for at gemme denne shell i funktionen, er det bedre at sende parametrene til de metoder for udførelse, fortrydelse og gentagelse, som sørger for de brugergrænsefladeoplysninger, der kræves til at udføre funktionen. Parametrene sendes videre til selve funktionen.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
infoAdapter er en IAdaptable, der som minimum kan stille den shell til rådighed, der kan bruges ved start af dialogbokse. Funktionseksemplet bruger parameteren sådan:
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...
}

Funktionsbehandlere til fortryd og gentag

Platformen omfatter standard funktionsbehandlere, som kan ændre mål, for fortryd og gentag, som oversigter og editorer kan konfigurere, så der gives understøttelse af fortrydelse og gentagelse af deres bestemte kontekst. Når der oprettes en funktionsbehandler, tildeles den en kontekst, så funktionshistorikken filtreres på en måde, der passer til denne bestemte oversigt. Funktionsbehandlerne sørger for at opdatere fortrydelses- og gentagelsesetiketterne for at vise den aktuelle funktion, sørger for at stille den relevante statusovervågning og de relevante brugergrænsefladeoplysninger til rådighed for funktionshistorikken og sørger for eventuel beskæring af historikken, hvis den aktuelle funktion er ugyldig. Der stilles for nemheds skyld også en funktionsgruppe til rådighed, som opretter funktionsbehandlere og tildeler dem til de globale fortrydelses- og gentagelsesfunktioner.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
Den sidste parameter er en boolesk parameter, som angiver, om fortrydelses- og gentagelseshistorikkerne for den angivne kontekst skal fjernes, hvis den funktion, som i øjeblikket kan fortrydes eller gentages, er ugyldig. Indstillingen af parameteren er relateret til den angivne fortrydelseskontekst og den valideringsstrategi, som bruges af funktioner med denne kontekst.

Programfortrydelsesmodeller

Det er tidligere beskrevet, hvordan fortrydelseskontekster kan bruges til at implementere forskellige typer programfortrydelsesmodeller. Muligheden for at tildele en eller flere kontekster til funktioner giver programmer mulighed for at implementere fortrydelsesstrategier, som er strengt lokale for hver oversigt eller editor, strengt globale på tværs af alle plugins eller en model midtimellem. En anden designbeslutning angående fortrydelse og gentagelse er, om en funktion kan fortrydes eller gentages når som helst, eller om modellen er strengt lineær, så kun den nyeste funktion tages i betragtning til fortrydelse eller gentagelse.

IOperationHistory definerer en protokol, som tillader fleksible fortrydelsesmodeller og lader det være op til de enkelte implementeringer at bestemme, hvad der er tilladt. Den protokol for fortrydelse og gentagelse, der hidtil er beskrevet, antager, at der kun findes én underforstået funktion, som kan fortrydes eller gentages i en bestemt fortrydelseskontekst. Der stilles en anden protokol til rådighed, som tillader klienter at udføre en bestemt funktion, uanset dens plads i historikken. Funktionshistorikken kan konfigureres, så det er muligt at implementere den model, der passer til et program. Det gøres med en grænseflade, som bruges til at forhåndsgodkende alle fortrydelses- og gentagelsesanmodninger, før funktionen fortrydes eller gentages.

Funktionsgodkendere

IOperationApprover definerer protokollen for godkendelse af fortrydelse eller gentagelse af en bestemt funktion. En funktionsgodkender installeres i en funktionshistorik. Bestemte funktionsgodkendere kan efter tur kontrollere alle funktionernes gyldighed, kontrollere funktioner med udelukkende bestemte kontekster eller spørge brugeren, hvis der findes uventede tilstande i en funktion. Følgende kodestykke viser, hvordan et program kan konfigurere funktionshistorikken til at overholde en lineær fortrydelsesmodel for alle funktioner.
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());

I dette tilfælde installeres funktionsgodkenderen LinearUndoEnforcer, som stilles til rådighed af rammen, i historikken, så det er muligt at forhindre fortrydelse eller gentagelse af en funktion, som ikke er den seneste fortrudte eller gentagede funktion i alle dens fortrydelseskontekster.

En anden funktionsgodkender, LinearUndoViolationUserApprover, registrerer den samme tilstand og spørger brugeren, om funktionen må fortsætte. Denne funktionsgodkender kan installeres i en bestemt del af arbejdsbænken.

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

Plugin-udviklere kan frit udvikle og installere deres egne funktionsgodkendere for at implementere programspecifikke fortrydelsesmodeller og godkendelsesstrategier. I plugin'en kan det være relevant at finde godkendelsen for den oprindelige udførelse af funktionen som supplement til fortrydelse og gentagelse af funktionen. Hvis det er tilfældet, skal funktionsgodkenderen også implementere IOperationApprover2, som godkender udførelsen af funktionen. Når platformsfunktionshistorikken bliver bedt om at udføre en funktion, vil den søge godkendelse af enhver funktionsgodkender, der implementerer denne grænseflade.

Fortryd og IDE-arbejdsbænken

Der er tidligere vist kodestykker, som bruger arbejdsbænksprotokol til at få adgang til funktionshistorikken og arbejdsbænkens fortrydelseskontekst. Det opnås med IWorkbenchOperationSupport, som kan hentes fra arbejdsbænken. Ideen med en fortrydelseskontekst på tværs af arbejdsbænken er temmelig generel. Det er overladt til arbejdsbænksprogrammet at bestemme det specifikke omfang af arbejdsbænkens fortrydelseskontekst og at bestemme, hvilke oversigter og editorer der bruger arbejdsbænkskonteksten, når der gives understøttelse af fortrydelse.

Hvad angår Eclipse IDE-arbejdsbænken, skal arbejdsbænkens fortrydelseskontekst tildeles til de funktioner, der påvirker IDE-arbejdsområdet som sådan. Konteksten bruges af oversigter, der arbejder med arbejdsområdet, som f.eks. ressourcenavigatoren. IDE-arbejdsbænken installerer en adapter i arbejdsområdet for IUndoContext, som returnerer arbejdsbænkens fortrydelseskontekst. Denne modelbaserede registrering tillader plugins, som arbejder med arbejdsområdet, at hente den relevante fortrydelseskontekst, selvom de er hovedløse og ikke refererer til nogen arbejdsbænksklasser.

// 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
}

Andre plugins opfordres til at bruge den samme teknik til at registrere modelbaserede fortrydelseskontekster.