http://www.developer.com/

Back to article

Programming the Eclipse Workbench


March 7, 2006

What makes Eclipse applications most easily recognizable is the Workbench. It is the foundation of applications such as the Eclipse IDE — the main window contains menus, toolbars, and a variety of smaller windows, some tiled, some stacked behind one another. What makes each Workbench application unique is what these windows are and what functionality they provide.

In the previous installment of the Eclipse series an Eclipse Workbench "view" was used as the host of a JFace and SWT components, but I did not go into much detail about its structure or function. In this article, the Eclipse Workbench will be explored in more detail. Specifically, the Workbench "parts" — the views and editors that make up most Workbench applications — will be reviewed.

Introduction

When you run a typical Eclipse Workbench application, such as your Eclipse IDE, you get the familiar application window with menu and toolbars at the top, status bar at the bottom, and one or more views in between. Many of these views allow you to manipulate the information they present, and perhaps edit it in specialized editors. While some tasks may be implemented as step-by-step wizards, the telltale sign of a workbench application is the ability to view and manipulate information in a variety of ways.

In this article, a simple editor is developed that lets you edit lists of strings stored in plain-text files. A JFace viewer is used to display the model. Menus are provided to allow for the addition of new entries into the list and deletion of selected entries. Also provided is support for undoing, redoing, and saving of these modifications.

To compile and run the example, you will need Eclipse SDK 3.1, available at http://www.eclipse.org/downloads. Some familiarity with the Plug-in Development Environment, JFace, and SWT is required.

Eclipse Workbench

From a user's point of view, the Workbench is a top-level application window (or several of them, in fact) with menus, toolbars, views, and editors. While this is by no means a requirement, many users have actually come to expect certain menus and views to be available in Workbench applications; for example, there is usually a File menu that allows for creation of new and opening of existing files, an Edit menu with editing actions such as Cut, Copy, and Paste, as well as a Window menu for customizing some aspects of the user interface.

From a developer's perspective, the Workbench serves as the foundation for applications that contribute their views, editors, wizards, and other components as plug-ins. You can look at the Workbench as the infrastructure that you can use to build a certain class of desktop applications. In fact, Eclipse started out as an IDE workbench suitable for developing integrated development tools, but over the years it evolved into a generic platform for Rich Client Applications (RCP). The central architectural element of these applications is precisely the Workbench (also called the Generic Workbench to differentiate it from the IDE).

Programmatically, the Workbench is represented by interface IWorkbench, which can be obtained as a singleton instance by calling PlatformUI.getWorkbench(). The Workbench provides a variety of services, most of which we won't address here. It also provides access to its visual components, starting with its windows; while there may be more than one window (represented by interface IWorkbenchWindow) in a Workbench application, at most one can be active at any given time. Each window consists of multiple pages (IWorkbenchPage; although having more than one page is rare), of which only one may be active at any given time.

At this point you may be tempted to think that switching perspectives is equivalent to flipping the window's pages. This, however, is not the case. Programmatically, a perspective just defines the page layout (which is, by the way, fully customizable by the user). When you switch perspectives, you actually apply a different layout (and a corresponding set of views) to the same page.

Finally, each workbench page consists of several "parts" — editors or views (and again, only one may be active at a time).

Each of these interfaces provides access to components and services pertinent to their level within the hierarchy. For instance, IWorkbenchPage allows you to show views, open editors, and manipulate the current perspective, while IWorkenchWindow lets you work with its pages, and IWorbench with windows and other Workbench-wide services.

This hierarchy stops at IWorkbenchPart, because this is where customization comes to play: each view or editor implements its own user interface and provides its own services.

Several views are considered "standard" — Properties and Content Outline in the Generic Workbench, Problems, Tasks, Bookmarks, Resource Navigator, and some others, in the IDE. Many of these expose public APIs that you can peruse in your own solutions.

Workbench Parts


Fig. 1: Eclipse Workbench Parts and Action Bars.

While editors and views perform different functions within the Workbench, they have much in common — they are "parts" of the Workbench, and as such they must follow a certain protocol common to all Workbench parts. This protocol is embodied in interface IWorkbenchPart, which all Workbench parts must implement.

While developing your own views or editors, you will notice that instead of directly implementing the corresponding interface, you are asked to extend an abstract base class (ViewPart for views and EditorPart for editors, respectively). This approach is actually quite common throughout Eclipse. It gives the API providers the flexibility to amend and evolve the API (i.e., the interface) without inadvertently breaking backward compatibility. For example, if they decide to add a new method to the interface, then all existing implementations would break, unless updated. If, however, the implementations extend an abstract class, then that class can provide the default implementation of this method, without breaking any existing subclasses.

In the previous article's example, the SuperView's createPartControl(Composite) method was implemented in order to construct its user interface components. The method's argument were used as the parent Composite for the viewer. In fact, Workbench parts are essentially user interface widgets displayed and managed by the Workbench in a specific way. Thus, this method is called by the Workbench at the appropriate time within the part's lifecycle in order to create its visual representation. If the part allocates any platform resources during construction, it must dispose them in its dispose() method.

Workbench parts are often dynamic in nature and may need to communicate any relevant state changes to the Workbench. For this reason, each Workbench part supports a property change notification protocol — interested parties may add themselves as IPropertyListeners, which the part then notifies when one of its properties changes. Just what these properties may be depends on the part type; one common property shared by both views and editors is their title.

The Workbench supplies each of its parts with instance-specific services by means of a "site" (IWorkbenchPartSite) — the primary interface between the part and the Workbench. The way in which views and editors are supplied with it differs, but what they do have in common is that both are initialized with a site provided by the Workbench before their user interfaces may be constructed (views actually get a view-specific subtype of the site, as do the editors).

Action Bars

When exploring JFace, you learned that user-invokable functionality is encapsulated in Actions, which are visually represented by Contributions. In the Workbench, the components that expose contributions are called Action Bars. They include menus, toolbars, coolbars (a younger cousin of toolbar), and the status line. There's also the notion of "global action handlers", which give workbench parts the means to provide their own implementation of certain common actions (such as Cut, Copy, Paste, etc.) registered globally by the Workbench. Both views and editors have access to their respective action bars, though they differ in usage.

Views vs. Editors

As a user, you can probably tell the difference between views and editors with relative ease. For instance, views are usually there by default as part of the perspective, while editors are opened from one of the views. Views are usually stacked around the window's sides, while editors take up a more central location, and there is usually only one instance of the view shown at a time, but potentially multiple instances of the same editor type. Finally, each view (or a stack of them) gets its own action bars (toolbar and menu bar), while editors use the main application menu and toolbar to make contributions (you can see the action bars changing as you activate different views or editor types).

You've already seen that from the programming perspective, views and editors have a lot in common -- both are Workbench parts. However, the first thing you will notice is that views are initialized differently than editors. For one, overriding a ViewPart's init method is entirely optional, while the editor's init method must be implemented. That is because views are left to figure out on their own what information to display (e.g., some sort of singleton data source, such as the Workspace, of Workbench-wide Tasks, etc.), while the editors get their input from the client code that requests their opening; for instance, double-clicking a file in the Resource Navigator will open the appropriate editor with that file as its input. Thus, editors must accept their input as part of their initialization.

Secondly, editors have a "dirty" state, which is when they may need to be saved. Saving an editor involves storing its content in the provided input in such a way that it can be re-opened next time around. On the other hand, if a view wants to remember any state across its invocations, it should store them in an IMemento, which is a simple, hierarchical key/value pair data structure. The Workbench stores this memento at an internal location and passes it back to the view during its subsequent initialization.

Lastly, editors and views manage their action bars differently. While a view gets its own instance of IActionBars (available through its site) to which it can contribute actions directly, editors share a single instance of action bars per editor type. This is meant to make the management of contributions easier, given that each editor instance may attempt to contribute the same set of actions. Instead, editors use a helper class called the "action bar contributor" (implementing IEditorActionBarContributor), which is declared as part of the editor's extension. When the first instance of the editor is created, its action bar contributor is instantiated and used for any subsequent editor instances. The job of this class is to contribute actions on the editor's behalf, and updated them to reflect the currently active editor (the contributor is notified whenever the active editor changes).

Building the List Editor

You've already developed a view in the previous article, though it did not involve action contributions. To get a better feel for how action contributions work, as well as how editors differ from views, you are going to create a simple editor with several actions contributed by its action bar contributor.

You start by creating a plug-in project, much like we did last time. You specify that the plug-in will make contributions to the UI. This gives the correct plug-in dependencies (org.eclipse.ui, to be precise).

In the Plug-in Manifest Editor, declare an org.eclipse.ui.editors extension like this: Click the Extensions tab, then the "Add..." button in the "All Extensions" section and select "org.eclipse.ui.editors". Click "Finish". In the editor, right-click the extension and choose "New -> Editor". Fill out the new element's "Extension Element Details" section with your editor's ID, name, path to an icon (located somewhere in your plug-in), and extensions (as in "file extensions" — specify "list" as the only recognized extension). Two remaining properties that are required in order to proceed are "class" and "contributorClass". Those haven't been created yet, but you can use this editor to help get started: Clicking the "class" label (it looks like a web link) brings up a "New Class" dialog, pre-populated with the correct superclass (org.eclipse.ui.part.EditorPart). All you need to do is specify the name and click "Finish" to create your editor class.

The first method to implement is init. Before it does anything else, the editor needs to determine if it can handle the input provided to it. In our case, the editor requires that the input implement IPathEditorInput; if it doesn't, a PartInitException must be thrown. You proceed by loading the editor's input into the object model, which is ultimately what the editor uses (we will take a closer look at it shortly). Lastly, you set the editor's site, input (the protocol requires that we retain it), part name (you set it to the input's name, which will usually be the filename), and a user-friendly tooltip (which contains the full path of the input file).

Next, we implement the two familiar methods — createPartControl and setFocus. In createPartControl, you instantiate a JFace ListViewer, which will be sufficient for the purpose of displaying a simple list of strings. Specify that it support multi-item selection, which lets the user select and delete multiple items at the same time. Then configure the viewer with a default label provider and sorter; however, there isn't any existing content provider that "understands" our object model, so you have to implement your own.

Editor's Object Model

While the editor's input is very simple — a list of unique strings — and could be represented by one of Java's collection classes, a feature is required that these classes don't provide — you want to be notified whenever the model changes (e.g., elements are added or removed). Thus, create a simple wrapper equipped with listener/event mechanism, which will allow any interested party to register themselves as a listener and be notified (via a custom event) whenever changes are made to the underlying list.

You would be correct in concluding that you could just as easily maintain an instance of ArrayList internal to the editor, provide access to it only via editor's public methods, and in those methods track any changes to the list. However, models accompanied by a listener/event notification mechanism are more flexible (e.g., may be used outside of an editor context) and their use is wide-spread throughout Eclipse.

Now, the content provider can be implemented, which both converts the object model into a list of string elements (suitable for displaying in the ListViewer) and updates the viewer whenever changes are detected in the model (this part is accomplished by registering the content provider as the model's listener whenever the input changes).

Finishing the Minimal Implementation

A few methods in the editor class remain to be implemented. For simplicity, the "Save As" functionality is not going to be supported, and thus false from method isSaveAsAllowed will always be returned. Saving back to the original input, however, will be supported, and thus the method doSave(IProgressMonitor) will be implemented. Its body is rather trivial — write the model's elements into a flat text file, each on its own line. However, one important protocol that you need to support here is the editor's "dirty" state; recall that an editor is in this state when it contains unpersisted data that will be lost when the application closes. Our editor implementation is required to maintain this state (and provide it via method isDirty), and notify any interested listeners (via their IPropertyListener interface) whenever this state changes. When a save is successful, the editor should no longer be dirty; thus, we should set our internal flag to false and fire a property change event (with ID PROP_DIRTY) to indicate the change. The doSave method may only be called when the editor is in fact dirty, and so we don't have to check if the state did in fact change.

Contributing Editor Actions

To implement the action bar contributor for the editor, you must go back to the Plug-in Manifest Editor and in the extension's details section, click the "contributorClass" label (which looks like a hyperlink). This class instantiates all actions in its constructor and contributes them to their respective action bars: the Add action is appended to the top-level Edit menu (since it already has similar Add actions), and the Delete, Undo, and Redo actions is set as "global action" handles. What this means is that whenever this contributor is active, it will provide its own implementations of the global Delete, Undo, and Redo actions defined by the Workbench and accessible in the Edit menu as well as through keyboard shortcuts.

Regardless of its particular function, each action class you implement must be aware of its "target" editor instance; that is, the particular editor with which it is to interact. The contributor is notified whenever an editor instance becomes active in its setActiveEditor(IEditorPart) method; thus, you propagate this notification to each action instance and allow it to respond accordingly.

Using Operation History

Now that you have the right infrastructure in place, you can implement each action class. The Add action is the simplest — all it needs to do is solicit input from the user (for example, using the JFace InputDialog) and add it to the active editor's model, if appropriate. Similarly, the Delete action needs to remove the elements currently selected in the active editor. However, Delete should be disabled when there are no selected elements. Thus, this action must be aware of the active editor's selection changes. You can accomplish that by exposing the editor's viewer selection via the ISelectionProvider interface and registering the Delete action as a ISelectionChangedListener on the active editor instance.

While you could implement the above actions to perform their functions directly on the active editor's model, you would run into a problem: how would Undo and Redo be implemented? These two actions don't provide any model-specific functionality; all they need to do is undo the last action, or redo the action that was last undone, whatever it may be. Ideally, they would be able to undo and redo multiple actions, not just one.

Because this is a frequently encountered problem in interactive applications with editing capabilities, the Workbench provides IWorkbenchOperationSupport, which is a service that allows the application to represent its user invokable actions in a command-like pattern and use the IOperationHistory to execute, undo, and redo them. This service is rather flexible and I won't go into much detail beyond what's necessary for this exercise. In short, the functionality of Add and Delete is encapsulated in the respective IUndoableOperation, which knows how to execute, undo, and redo itself. The actions simply execute the operation in the editor's undo context and allow the Undo and Redo actions to navigate the operation history as required. As the last bit, you register the editor itself as an operation history listener and mark it dirty whenever there is an unsaved operation in its context.

Test-Running the Editor

To see the results, run the plug-in as an Eclipse Application. When the runtime workbench appears, switch to the Resource Perspective (though you can use any other perspective with a view that allows you to manipulate your Workspace). Create a new, simple project, and in it a simple file with extension ".list". This will cause a new, empty file to be created and opened in the List Editor we just developed. You should now be able to add elements using the "Add List Element..." action in the main Edit menu, and delete them (when selected) using the Del key. You can also save and re-open the modified files.

One oddity you may notice is that the Content Outline view, usually present in most perspectives by default, displays a message stating that the outline is not available. As its name indicates, this view is used to present a concise outline of the active editor's contents. In order to do this, the editor must provide a special adapter whose job it is to display an editor-specific outline. We did not develop one in this article, mostly because our content model cannot get any simpler than a list, and thus the outline itself would have to be nothing more than a list. For more information about this feature, see class ContentOutline.

Conclusion

The Eclipse Workbench serves as a reusable foundation for developing desktop applications. It consists mainly of views and editors, commonly referred to as Workbench Parts. Views and editors expose their functionality as actions, which they contribute to their respective action bars. As an example, you were shown how to develop a simple editor for manipulating a list of elements, with support for global actions and undoable operations.

Resources

About the Author

Peter Nehrer is an independent software consultant living in Toronto, Ontario. He specializes in Eclipse-based enterprise solutions and J2EE applications. His professional interests include development tools, model-driven software development, and information sharing. He is the author and contributor to several Eclipse-related Open Source projects. Peter is an IBM Certified Solutons Developer for XML and related technologies. He holds an M.S. in Computer Science from the University of Massachusetts at Amherst, MA.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date