Vi har sett på en rekke ulike måter å bidra med handlinger på for arbeidsbenken, men vi ikke fokusert på implementeringen av run()-metoden for en handling. Mekanismen i denne metoden avhenger av den bestemte handlingen, men gjennom å strukturere koden som en operasjon som kan angres, kan handlingen delta i plattformens støtte for angring og omgjøring.
Plattformen oppgir et rammeverk for handlinger som kan angres i pakken org.eclipse.core.commands.operations. Ved å implementere koden i en run()-metode og opprette en IUndoableOperation, kan operasjonen gjøres tilgjengelig for angring og omgjøring. Det er enkelt å konvertere en handling til å bruke operasjoner, bortsett fra implementering av selve angrings- og omgjøringsfunksjonaliteten.
La oss begynne med å se på et svært enkelt eksempel. I eksempelet med readme-plugin-modulen oppgav vi en enkel ViewActionDelegate. Ved aktivering starter handlingen helt enkelt en dialogboks som angir at den er kjørt.
public void run(org.eclipse.jface.action.IAction action) { MessageDialog.openInformation(view.getSite().getShell(), MessageUtil.getString("Readme_Editor"), MessageUtil.getString("View_Action_executed")); }Gjennom bruk av operasjoner oppretter run-metoden en operasjon som utfører det arbeid som tidligere ble utført i run-metoden, og ber om at en operasjonshistorikk utfører operasjonen slik at den kan lagres for angring og omgjøring.
public void run(org.eclipse.jface.action.IAction action) { IUndoableOperation operation = new ReadmeOperation( view.getSite().getShell()); ... operationHistory.execute(operation, null, null); }Operasjonen kapsler inn gammel funksjonalitet fra run-metoden samt angring og omgjøring av operasjonen.
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 enkle handlinger er det mulig å flytte alt funksjonelt arbeid til operasjonsklassen. I så fall kan det være hensiktsmessig å komprimere de tidligere handlingsklassene til en enkelt handlingsklasse som parameteriseres. Handlingen utfører helt enkelt den oppgitte operasjonen når det er på tide å kjøre den. Dette er hovedsakelig en utformingsavgjørelse om applikasjonen.
Når handlingen starter en veiviser, opprettes operasjonen vanligvis som en del av veiviserens performFinish()-metode eller veivisersidens finish()-metode. Det å konvertere finish-metoden til å bruke operasjoner, likner på konvertering av en run-metode. Metoden oppretter og utfører en operasjon som utfører det arbeidet som tidligere ble utført internt.
Hittil har vi brukt en operasjonshistorikk uten å egentlig forklare dette noe nærmere. La oss nok en gang se på koden som oppretter eksempeloperasjonen.
public void run(org.eclipse.jface.action.IAction action) { IUndoableOperation operation = new ReadmeOperation( view.getSite().getShell()); ... operationHistory.execute(operation, null, null); }Hva handler operasjonshistorikk egentlig om? IOperationHistory definerer grensesnittet for objektet som har oversikt over alle operasjoner som kan angres. Når en operasjonshistorikk utfører en operasjon, utføres først operasjonen før den legges til i angringshistorikken. Klienter som vil angre eller gjøre om operasjoner, kan gjøre dette ved hjelp av IOperationHistory-protokollen.
Operasjonshistorikken som brukes av en applikasjon, kan hentes på flere måter. Det enkleste er å bruke OperationHistoryFactory.
IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();
Arbeidsbenken kan også brukes til å hente operasjonshistorikken. Arbeidsbenken konfigurerer standard operasjonshistorikk og oppgir dessuten protokoll for å få tilgang til historikken. Følgende snutt viser hvordan du henter operasjonshistorikken fra arbeidsbenken.
IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench(); IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();Når operasjonshistorikken er skaffet til veie, kan den brukes til å utføre spørringer etter historikk for angring eller omgjøring, finne ut hvilken operasjon som står for tur for angring eller omgjøring eller til å angre eller gjøre om bestemte operasjoner. Klienter kan legge til en IOperationHistoryListener for å bli underrettet om endringer i historikken. Andre protokoller lar klienter definere grenser for historikk eller underrette lyttere om endringer i en bestemt operasjon. Før vi ser nærmere på protokollen, må vi forstå angrekonteksten.
Når det opprettes en operasjon, tilordnes den en angrekontekst som beskriver brukerkonteksten som den opprinnelige operasjonen ble utført i. Denne konteksten avhenger vanligvis av visningen eller redigeringsprogrammet som utførte handlingen som kan angres. For eksempel er endringer som utføres i et redigeringsprogram, ofte lokale for det redigeringsprogrammet. I så fall skal redigeringsprogrammet opprette sin egen angrekontekst og tilordne konteksten til operasjoner som legges til i historikken. På denne måten betraktes alle operasjoner som utføres i redigeringsprogrammet, som lokale og halvprivate. Redigeringsprogrammer eller visninger som opererer i en delt modell, bruker ofte en angrekontekst som er tilknyttet modellen som de manipulerer. Ved å bruke en mer generell angrekontekst kan operasjoner som utføres av en visning eller et redigeringsprogram, være tilgjengelig for angring i en annen visning eller redigeringsprogram som fungerer på samme modell.
Angrekontekster er relativt enkle i sin funksjonalitet. Protokollen for IUndoContext er ganske minimal. Kontekstens viktigste rolle er å "kode" en bestemt operasjon som en operasjon som tilhører en angrekontekst, for å skille den fra operasjoner som er opprettet i andre angrekontekster. Dette betyr at operasjonshistorikken kan holde oversikt over den globale historikken for alle operasjoner som kan angres og som er utført, mens visninger og redigeringsprogrammer kan bruke angrekonteksten til å kan filtrere historikken for et bestemt ståsted.
Angrekontekster kan opprettes av plugin-modulen som oppretter operasjoner som kan angres, eller åpnes gjennom programmeringsgrensesnittet. For eksempel gir arbeidsbenken tilgang til en angrekontekst som kan brukes for operasjoner i hele arbeidsbenken. Uavhengig av hvordan de hentes må angrekontekster tilordnes når det opprettes en operasjon. Følgende snutt viser hvordan readme-pluginens ViewActionDelegate kan tilordne operasjonene en kontekst i hele arbeidsbenken.
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 bruke angrekontekster i det hele tatt? Hvorfor ikke bare bruke separat operasjonshistorikk for separate visninger og redigeringsprogrammer? Med separate operasjonshistorikk antas det at bestemte visninger eller redigeringsprogrammer vedlikeholder sin egen private angrehistorikk, og at angring ikke har global innvirkning på applikasjonen. Dette kan være hensiktsmessig for noen applikasjoner, og i så fall må hver enkelt visning eller redigeringsprogram opprette sin egen separate angrekontekst. Andre applikasjoner vil kanskje implementere en global angring som gjelder alle brukeroperasjoner, uavhengig av visningen eller redigeringsprogrammet de kommer fra. I så fall skal arbeidsbenkkonteksten brukes av alle plugin-moduler som legger til operasjoner i historikken.
I mer kompliserte applikasjoner er angring verken bare lokalt eller bare globalt. I stedet er det noe overlapping mellom angrekontekster. Dette kan oppnås ved å tildele flere kontekster til en operasjon. For eksempel kan en IDE-arbeidsbenkvisning manipulere hele arbeidsområdet og betrakte arbeidsområdet som sin angrekontekst. Et redigeringsprogram som er åpent i en bestemt ressurs i arbeidsområdet, kan betrakte operasjonene hovedsakelig som lokale. Operasjoner som utføres i redigeringsprogrammet, kan imidlertid påvirke både den bestemte ressursen og arbeidsområdet generelt. (Et godt eksempel på dette er JDTs støtte for refaktorisering som tillater strukturelle endringer i et Java-element som oppstår mens kildefilen redigeres.) I disse tilfellene er det nyttig å kunne legge til både angrekontekster i operasjonen slik at angring kan utføres fra selve redigeringsprogrammet samt fra de visningene som manipulerer arbeidsområdet.
Nå som vi vet hva en angrekontekst gjør, kan vi på nytt se på protokollen for IOperationHistory. I følgende snutt utføres en angring for en bestemt kontekst:
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory(); try { IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo); } catch (ExecutionException e) { // handle the exception }Historikken henter den sist utførte operasjonen med gitt kontekst, og ber den om å angre seg selv. Det kan brukes annen protokoll for å hente hele angre- eller omgjøringshistorikken for en kontekst eller for å finne operasjonen som skal angres eller gjøres om i en bestemt kontekst. Følgende snutt henter etiketten for operasjonen som skal angres i en bestemt kontekst.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory(); String label = history.getUndoOperation(myContext).getLabel();
Den globale angrekonteksten IOperationHistory.GLOBAL_UNDO_CONTEXT kan brukes for å referere til den globale angrehistorikken. Det vil si til alle operasjonene i historikken, uavhengig av den spesifikke konteksten. Følgende snutt henter den globale angrehistorikken.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory(); IUndoableOperation [] undoHistory = operationHistory.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);
Når en operasjon utføres, angres eller gjøres om ved hjelp av operasjonshistorikkprotokoll, kan klienter oppgi en fremdriftsovervåker og annen brukergrensesnittinformasjon som trengs for å utføre operasjonen. Informasjonen sendes til selve operasjonen. I det opprinnelige eksempelet opprettet readme-handlingen en operasjon med et skallparameter som kan brukes til å åpne dialogboksen. I stedet for å lagre skallet i operasjonen, er det bedre å sende parameterne til utførings-, angrings- og omgjøringsmetodene som oppgir den brukergrensesnittinformasjonen som trengs for å kjøre operasjonen. Disse parameterne sendes til selve operasjonen.
public void run(org.eclipse.jface.action.IAction action) { IUndoableOperation operation = new ReadmeOperation(); ... operationHistory.execute(operation, null, infoAdapter); }infoAdapter er en IAdaptable som minimalt kan oppgi det skallet (Shell) som kan brukes når dialogbokser startes. I operasjonseksempelet vårt brukes denne parameteren på følgende måte:
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... }
Plattformen oppgir standard målskiftende (retargetable) handlingsbehandlere for angring og omgjøring som kan konfigureres av visninger og redigeringsprogrammer for å oppgi støtte for angring og omgjøring for deres bestemte kontekst. Når handlingsbehandleren opprettes, tildeles den en kontekst slik at operasjonshistorikken filtreres på en måte som er hensiktsmessig for den bestemte visningen. Handlingsbehandlerne håndterer oppdatering av etiketter for angring og omgjøring slik at de viser den aktuelle operasjonen, oppgir riktig fremdriftsovervåker og brukergrensesnittinformasjon til operasjonshistorikken og eventuelt rensker historikken når den gjeldende operasjonen er ugyldig. En handlingsgruppe som oppretter handlingsbehandlere og tildeler dem til de globale angrings- og omgjøringshandlingene er oppgitt for enkelhets skyld.
new UndoRedoActionGroup(this.getSite(), undoContext, true);Den siste parameteren er en boolsk verdi som angir om historikken for angring og omgjøring for den angitte konteksten skal slettes når operasjonen som er tilgjengelig for angring eller omgjøring, ikke er gyldig. Innstillingen for denne parameteren er knyttet til angrekonteksten som er oppgitt, og valideringsstrategien som brukes av operasjoner med den konteksten.
Vi har tidligere sett på hvordan angrekontekster kan brukes til å implementere ulike typer angremodeller for applikasjoner. Muligheten for å tildele en eller flere kontekster til operasjoner gjør det mulig for applikasjoner å implementere angrestrategier som er bare lokale for hver visning eller hvert redigeringsprogram, eller bare globale på tvers av alle plugin-modulene eller en modell som er noe midt imellom. En annen utformingsavgjørelse som omfatter angring og omgjøring, er om en operasjon når som helst skal kunne angres eller gjøres om eller om det skal brukes en lineær modell der bare den siste operasjonen anses som tilgjengelig for angring eller omgjøring.
IOperationHistory definerer protokoll for fleksible angremodeller, der det er opp til hver enkelt implementering å fastsette hva som er tillatt. Den protokollen for angring og omgjøring som vi har sett til nå, forutsetter at det bare er en antatt operasjon som er tilgjengelig for angring eller omgjøring i en bestemt angrekontekst. Ytterligere protokoll oppgis slik at klienter kan utføre en bestemt operasjon uavhengig av plasseringen i historikken. Operasjonshistorikken kan konfigureres slik at modellen som passer for en applikasjon, kan implementeres. Dette gjøres med et grensesnitt som brukes til å forhåndsgodkjenne forespørsler om angring og omgjøring før operasjonen angres eller gjøres om.
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 tilfellet installeres en operasjonsgodkjenner i historikken. Denne er angitt av rammeverket LinearUndoEnforcer for å unngå angring eller omgjøring av en operasjon som ikke er den nyeste operasjonen som er utført eller angret, i alle angrekontekstene.
En annen operasjonsgodkjenner, LinearUndoViolationUserApprover, oppdager den samme tilstanden og underretter brukeren om operasjonen skal få fortsette. Operasjonsgodkjenneren kan installeres i en bestemt arbeidsbenkdel.
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-utviklere kan fritt utvikle og installere sine egne operasjonsgodkjennere for implementering av applikasjonsspesifikke angremodeller og godkjenningsstrategier. I plugin-modulen kan det være ønskelig å søke godkjenning for den opprinnelige utføringen av en operasjon, i tillegg til angring og omgjøring av operasjonen. Hvis dette er tilfellet bør operasjonsgodkjenneren også implementere IOperationApprover2, som godkjenner utføringen av operasjonen. Ved spørsmål om å utføre en operasjon vil plattformhistorikken søke godkjenning fra en operasjonsgodkjenner som implementerer dette grensesnittet.
Vi har sett at kodesnutter bruker arbeidsbenkprotokoll til å få tilgang til operasjonshistorikk og arbeidsbenkens angrekontekst. Dette gjøres med IWorkbenchOperationSupport, som kan hentes fra arbeidsbenken. En angrekontekst for hele arbeidsbenken er nokså generell. Det er opp til arbeidsbenkapplikasjonen å fastsette hvilket bestemt omfang som omfattes av arbeidsbenkens angrekontekst, og hvilke visninger eller redigeringsprogrammer som bruker arbeidsbenkkonteksten når de oppgir støtte for angring.
I tilfellet med Eclipse IDE-arbeidsbenken skal arbeidsbenkens angrekontekst tildeles enhver operasjon som påvirker IDE-arbeidsområdet i sin helhet. Konteksten brukes av visninger som manipulerer arbeidsområdet, for eksempel ressursnavigatoren. IDE-arbeidsbenken installerer en adapter i arbeidsområdet for IUndoContext, som returnerer arbeidsbenkens angrekontekst. Med en slik modellbasert registrering kan plugin-moduler som manipulerer arbeidsområdet, hente riktig angrekontekst selv om de er i et kommandogrensesnitt og ikke refererer til noen arbeidsbenkklasser.
// 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 plugin-moduler bør også bruke denne teknikken ved registrering av modellbaserte angrekontekster.