Eclipse 重复/撤销框架浅析
在 IBM Bluemix 云平台上开发并部署您的下一个应用。
前言
现今日益强调软件产品可用性,支持重复和撤销操作以方便用户使用已经成为软件必不可少的功能。Eclipse作为一个成功的软件开发台,对软件CoP(Consumability of Product)同样很重视,它在核心插件org.eclipse.core.commands 的org.eclipse.core.commands.operations 包中实现了一套可撤销操作框架。利用这个框架,一般开发人员只需要实现三个必要方法(执行execute,重复redo和撤销undo)就能在基于Eclipse平台的应用程序中实现对重复和撤销操作的支持。
请访问 Eclipse 技术资源中心,获取 Eclipse 相关信息,包括大量 Eclipse 技术文章、教程、下载和相关技术资源。
Eclipse 重复撤销操作框架技术原理及其实现
在Eclipse重复撤销操作框架中有几个重要的概念,对这些概念的实现就构成了整个重复撤销操作框架:
可重复撤销操作(Operation): 通过实现IUndoableOperation接口来实现的类,这是框架执行的内容。这个类指定了操作的实际行为,定义的行为可以被执行,撤销和重复,IUndoableOperation接口的实现相对比较复杂,但Eclipse为开发人员提供了一个抽象类AbstractOperation:
public abstract class AbstractOperation implements IUndoableOperation { …… public AbstractOperation(String label) { this.label = label; } public abstract IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException; public abstract IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException;
这个抽象类实现了IUndoableOperation接口中的大部分方法,继承AbstractOperation的类只需要实现三个的关键方法就可以了, 三个方法如下:
- public abstract IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException; 操作首次运行时被调用
- public abstract IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException; 重复操作时被调用
- public abstract IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException; 撤销操作时被调用
这三个方法需要实现包括开发者将要提供的逻辑,每个方法传入的参数都是相同的,有如下两个:
- IProgressMonitor:对于IProgressMonitor 说大家比较熟悉,是用来告诉用户目前方法的执行进度的,
- IAdaptable 主要是用来提供UI相关的对象,使得操作在执行时能够与用户进行交互。使用时,如果这个参数不为空,则在这个参数中至少需要包含一个Shell(org.eclipse.swt.widgets.Shell)适配器。
操作历史记录(Operation History): 也是这个框架的运行引擎,在使用这个框架时,所有的操作的调用执行都要通过OperationHistory.execute()来执行,同样的,重复操作是通过OperationHistory.redo() 来完成,撤销操作要通过调用OperationHistory.undo()来完成。OperationHistory 对象是可以通过OperationHistoryFactory来创建,这个类是用来维护OperationHistory的实例的,用户可以通过setOperationHistory()方法来设置一个特殊实现的OperationHistory实例,但是这个设置操作必须在调用getOperationHistory()之前完成,否则将没有任何效果。如果用户没有为OperationHistoryFactory设置任何特殊实现的OperationHistory实例,在调用getOperationHistory()时,将缺省生成一个DefaultOperationHistory的实例,这个实例是在整个Eclipse环境中共享的,大部分情况下,使用这个实例就足够用了。DefaultOperationHistory 维护了两个堆栈,一个堆栈负责记录刚刚运行过的操作,称为Undo Operation Stack,而另外一个堆栈负责记录刚刚被撤销过的操作,称为Redo Operation Stack,为重复该操作提供支持。被执行过的操作根据不同的状态会被置于不同的堆栈中。下图描述了操作在DefaultOperationHistory执行过程中operation对象状态变化。
图 1 Operation 对象状态变化图
如上图所示,在使用DefaultOperationHistory.execute()方法运行Operation成功后,操作将被加入到撤销操作堆栈(Undo Operation Stack),当调用DefaultOperationHistory.undo()的时候,DefaultOperationHistory会从撤销操作堆栈(undo operation stack) 中取得需要撤销的操作,执行操作中的undo()方法,如果undo()方法执行成功,该操作会被从撤销操作堆栈中除去,根据堆栈的特性,最后被成功执行的操作将被撤销,通过DefaultOpeationHistory撤销的操作会被放入另外一个堆栈,重复操作堆栈(redo operation stack),当调用DefaultOperationHistor.redo()时,最近一次被撤销的操作的redo()方法会被调用,如果成功,该操作会被重复操作堆栈,并被重新加入到撤销操作堆栈中。
运行上下文(UndoContext): 这套框架同时支持在Eclipse环境下所有视图(View)或编辑器(Editor)中使用。显然,仅仅靠这两个堆栈来完成整个Eclipse集成平台的重复撤销操作是不够的,需要区分不同的情况下应该使用不同的操作,这就需要对不同编辑器或视图中使用的操作需要进行区分,运行上下文(UndoContext)就是这样的一种标记。运行上下文是在其应用环境中定义的,如特殊的某个视图或者编辑器,在这个环境中运行的操作都将被应用环境标记上它所定义的撤销上下文,当在这个特殊的环境中,也就是这个特定的视图或是编辑器中进行重复撤销操作时,只有与它所定义的撤销上下文相匹配的操作才能从堆栈中弹出。所有的运行上下文必须实现接口IUndoContext,如下所示:
public interface IUndoContext { public String getLabel(); public boolean matches(IUndoContext context); }
在IUndoContext接口中只提供了两个方法:
- getLabel(),定义用来描述Context的字符串;
- mathes(IUndoContext context)方法是用来标示给定的一个Context是否与本身相匹配,只有相匹配的Operation 对象才能从OperationHistory中维护的堆栈弹出。
在OperationHistory对象调用redo() 和undo()方法时都会使用IUndoContext对象作为参数。IUndoContext能够实现特殊的macthes方法,将其定义成与需要的任意上下文匹配,这样就能够扩大相匹配操作的范围。为了方便开发人员,Eclipse中提供了3种IUndoContext 的实现,可以满足大部分的需求,重用这些定义好的Cotnext可以减少开发者的工作:
- GlobalUndoContext:之所以称之为GlobalUndoContext,是因为它可以与任何其它UndoContext相匹配,能够得到所有UndoContext下的操作。他的matches(IundoContext context)方法实现很简单,就是直接返回true。
- UndoContext:一个简单,轻量级的UndoContext,可以被用来标记任意一个Operation。这个Context 可以被任意客户端初始化也可以被继承。使用同一UndoContext对象的Operation才能相匹配。
- ObjectUndoContext:这是用这个运行上下文时,可以通过其自身实现的一个特殊方法addMatch(IUndoContext)添加任意与之匹配的运行上下文,方便实现跨编辑器或视图的重复撤销操作之间的共享。
重复操作句柄和撤销操作句柄(UndoActionHandler 和RedoActionHandler):Eclipse提供了两个类RedoActionHandler和UndoActionHandler作为重复撤销操作的可重定位操作句柄(Retargetable action handler),这个句柄被使用它的视图或编辑器初始化,来支持自身的重复和撤销操作。这个句柄提供了重复和撤销操作的一般行为,也包括重复撤销操作的菜单显示。在句柄被创建时,视图或编辑器会分配给他一个运行是上下文,这样,该句柄在为视图或编辑器提供支持的时候,就可以根据这个上下文在操作历史记录中进行过滤。 包括更新重复撤销操作的菜单的显示,为操作历史提供正确的进度监控和界面信息并在当前的重复撤销操作无效时,将其从操作历史中除去。伴随这两个句柄,Eclipse同时提供了类UndoRedoActionGroup,这个类为编辑器或视图初始化重复操作句柄和撤销操作句柄提供了方便,当视图或编辑器需要初始化这两个句柄时,只要根据构造函数要求,构建一个UndoRedoActionGroup对象就可以了。
Approver:Approver在重复撤销框架内负责对重复撤销操作的验证,它在重复撤销操作之前执行,如果重复或撤销操作不能通过Approver的验证,将不能执行,Approver需要实现接口IOperationApprover,接口也很简单,如下所示:
public interface IOperationApprover { IStatus proceedRedoing(IUndoableOperation operation, IOperationHistory history, IAdaptable info); IStatus proceedUndoing(IUndoableOperation operation, IOperationHistory history, IAdaptable info); }
接口中的两个方法分别对将要进行的重复撤销的操作进行验证。
重复撤销框架应用
本节通过一个简单的例子来说明如何使用Eclipse3.2所提供的重复撤销框架。这里将开发一个视图,视图包含一个仅有一列的表,通过上下文菜单来为来为表增加行,然后可以在Eclipse的工作台菜单中通过重复撤销操作来完成该动作的撤销,并且在重复操作时,将询问用户来确认是否真的要重复该操作。
第一步:创建视图(view),这部分就是一个视图扩展点的实现,不是本文关心的核心内容,不再赘述。下面是创建好的视图:
图 2 实例视图
在创建视图时需要在视图的ContentProvider加一些方法来对视图进行操作,代码如下所示:
public class ViewContentProvider implements IStructuredContentProvider { public String[] strs = { "One", "Two", "Three" }; public void inputChanged(Viewer v, Object oldInput, Object newInput) { } public void dispose() { } public Object[] getElements(Object parent) { return strs; } public void addElement(String itemName){ ArrayList list = new ArrayList(); List strsList = Arrays.asList(strs); list.addAll(strsList); list.add(itemName); strs = new String[list.size()]; for(int i=0;i<strs.length;i++) strs[i]= (String)list.get(i);
} public void removeElemment(String itemName){ if(itemName == null) return; ArrayList list = new ArrayList(); for(int i=0;i<strs.length;i++){ if(itemName.equals(strs[i])){ } else { list.add(strs[i]); } } strs = new String[list.size()]; for(int i=0;i<strs.length;i++) strs[i]= (String)list.get(i); } }
其中addElement(String itemName) 和 removeElement(String itemName) 负责为视图增加和删除行。
第二步:创建IUndoableOperation, 如前所述,为避免实现过多的方法,这里将直接继承AbstractOperation,代码如下所示:
public class AddTableItemOperation extends AbstractOperation { private SampleView sampleViewer; private String addingStr; public AddTableItemOperation(String label, SampleView viewer,String addingItem) { super(label); this.sampleViewer = viewer; this.addingStr = addingItem; } public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { sampleViewer.getContentProvider().addElement(addingStr); sampleViewer.getTableViewer().setContentProvider( sampleViewer.getContentProvider()); sampleViewer.getTableViewer().setInput(sampleViewer.getViewSite()); return Status.OK_STATUS; } public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor,info); } public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { sampleViewer.getContentProvider().removeElemment(addingStr); sampleViewer.getTableViewer(). setContentProvider(sampleViewer.getContentProvider()); sampleViewer.getTableViewer().setInput(sampleViewer.getViewSite()); return Status.OK_STATUS; } }
这个Operation 利用传入的视图对象,获取视图的ContentProvider,然后对视图内容进行操作,其中redo 方法和execute执行相同的方法。
第三步:获取Eclipse平台内部的operationHistory,只有取得了Eclipse平台内提供的OperationHistory,才能利用重复撤销框架的功能,Eclipse提供了两种API来获取operatoinHistory对象,获取结果是一样的,如下面代码所示:
private IOperationHistory getOperationHistory() { //This script can also be used to retrive operationHistory //IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory(); return PlatformUI.getWorkbench().getOperationSupport() .getOperationHistory(); }
第四步:实现Approver类,这里只在重复操作时加入代码,第9-10行用来过滤operation, 只有在operation的Context与Approver相匹配时才去确认是否要执行重复操作。11-13行代码弹出窗口询问是否需要重复进行操作,如果确认需要,返回OK_Status,重复操作将被执行。
public class AskUserApprover implements IOperationApprover { private IUndoContext undoContext; public AskUserApprover(IUndoContext context) { super(); this.undoContext = context; } public IStatus proceedRedoing(IUndoableOperation operation, IOperationHistory history, IAdaptable info) { if (!operation.hasContext(undoContext)) return Status.CANCEL_STATUS; boolean isOK = MessageDialog.openConfirm(PlatformUI.getWorkbench() .getActiveWorkbenchWindow().getShell(), "Confirm", "Proceed Redo?"); if (isOK) return Status.OK_STATUS; else return Status.CANCEL_STATUS; } public IStatus proceedUndoing(IUndoableOperation operation, IOperationHistory history, IAdaptable info) { return Status.OK_STATUS; } }
第四步:利用Context特性化operationHistory,下面的代码包含几部分内容,第2行代码使用ObjectUndoContext为视图创建一个UndoContext,下面调用可重复撤销操作都需要用到这个UndoContext,第3-4行代码以前面创建的UndoContext为构造参数构建了一个AskUserApprover的Approver对象,并把该对象加入到operationHistory中,这样在重复和撤销操作执行之前都会首先执行approver对象中的方法,来确定是否可以执行重复或撤销操作;5-6行代码为符合当前context的操作堆栈来设定深度,超过这个深度的操作会被覆盖;7-9行代码把实现的重复撤销操作集成到Eclipse环境中。
private void initializeOperationHistory() { undoContext = new ObjectUndoContext(this); approver = new AskUserApprover(undoContext); getOperationHistory().addOperationApprover(approver); int limit = 10; getOperationHistory().setLimit(undoContext, limit); UndoRedoActionGroup undoRedoGroup = new UndoRedoActionGroup(getSite(), undoContext, true); IActionBars actionBars = getViewSite().getActionBars(); undoRedoGroup.fillActionBars(actionBars); }
第五步:在视图的Action中加入执行上面所说operation的代码,每次通过视图的Action来执行操作的时候都是通OperationHistory来执行的,只有这样执行的重复撤销操作才能被放到重复操作堆栈中,代码如下:
addTableItemAction = new Action() { public void run() { AddTableItemOperation operation = new AddTableItemOperation("hahah",SampleView.this,"test11"); try { operation.addContext(undoContext); getOperationHistory().execute(operation, null, null); } catch (ExecutionException e) { e.printStackTrace(); } } };
第六步:运行前面创建的插件,在Eclipse中点击Run->Run..,在弹出的窗口中创建并运行一个Eclipse Application, 在Samle View中右键点击Add Item ,在Eclipse 的操作台上的撤销操作菜单会变成如下所示:
图 3 撤销菜单变化
然后点击Undo操作,撤销操作会变灰,重复操作如下所示:
图 4 重复操作菜单变化
选择重复操作之后,根据我们前面的Approver代码,会有窗口弹出来确认是否要重复操作。
转载自IBM