Component Framework Proposal

Draft
Revision 1.0.4
By Stefan Xenos and Nick Edgar
Last modified 2004/11/05



Table of Contents

1.0 Introduction

This proposal outlines a framework for managing complex executable extensions. This framework is a first step toward allowing views and editors to be combined recursively, and created inside arbitrary SWT composites. Although it is intended for creating reusable UI components, the framework is useful for any extension point that creates complicated Java objects. For this reason, we will refer to the objects being created as "components" even though our components are typically views and editors.

This proposal is broken into three parts. The introduction describes the motivation for creating the component framework and its requirements. The second section describes the component framework in itself, which could be used for any executable extension point. The third section describes how the workbench will use the component framework for views and editors.

1.1 Understanding the problem

This section explains why the component framework is needed to support nested parts. We will show the problems involved in designing an extensible Site interface, and will show how they can be solved using the patterns in the component framework. Skip to the next sections if you don't need convincing and just want to see the specification. IMPORTANT NOTE: the code examples in this section are only intended to illustrate patterns and do not reflect the actual implementation

Currently, parts communicate with their parent through their site. The site provides the view with access to the outside world. In general, it is better to provide new API on the part's site than have the part reach to some global object, since the site can provide some context and can manage resources allocated to the part.

Here is a simplified version of the View and Site interface. We've omitted most of the details. The important part is that the View is given a Site when it is initialized, it uses various methods on the Site throughout its life, and the Site has a dispose() method that can clean up after the View.

/**
 * A view's main interface to the world.
 */
public class Site {
    /**
     * Create a view site, given the plugin that contains the implementation for the view
     */
    public Site(Plugin viewPlugin) {
    }

    /**
     * Return the view's ID
     */
    public String getId() {...};

    public IActionBars getActionBars() {...};

    /**
     * Clean up any leftover resources allocated by the site
     */
    public void dispose() {
    };
}

/**
 * Base class for all views.
 */
public abstract class View {
    private Site mySite;

    public View(Site viewSite) {
       mySite = viewSite;
    }

    public Site getSite() {
       return mySite;
    }

    public abstract String getTitle();

    public void dispose() {};
}

/**
 * A factory that can create views by ID.
 */
public ViewFactory {
    public static View createView(String viewExtensionId, Site viewSite) {
       //... create a view and return it
    }
}

Anyone that wants to instantiate a view would need to create an instance of Site and create the view by calling ViewFactory.createView. It would be possible to subclass Site in order to provide the part with additional context. For example, a subclass of Site could overload getActionBars() to substitute a different implementation of IActionBars to be used by the view.

One of the problems with using the Site as the main interface to a view is that we continually need to add new methods to the Site base class. For example, we might decide to add a logError method that allows views to log exceptions to their plugin log without needing to reach to their Plugin object or worry about constructing IStatus instances with the correct plugin ID.

/**
 * A view's main interface to the world.
 */
public class Site {
    Plugin plugin;

    public Site(Plugin viewPlugin) {
       plugin = viewPlugin;
    }

    // ...all other Site methods omitted...

    public void logError(Throwable t) {
       plugin.getLog().log(new Status(..., t));
    };
}

The site could also provide the view with localized ways to allocate resources. For example, if the view could allocate Images through its site, the Site could guarantee that the images would be cleaned up properly when the view is destroyed.

/**

 * A view's main interface to the world.
 */
public class Site {

    // .... everything in the above version of Site plus this:

    Map allocatedImages = new HashMap();

    public Image createImage(ImageDescriptor toCreate) {
       Image image = toCreate.createImage(true);
       allocatedImages.put(toCreate, image);
       return image;
    }

    public void destroyImage(ImageDescriptor toDestroy) {
       Image image = allocatedImages.get(toDestroy);
       if (image != null) {
          allocatedImages.remove(toDestroy);
          image.dispose();
       }
    }

    public void dispose() {
       Collection values = allocatedImages.getValueSet();
       Iterator iter = values.iterator();
       while (iter.hasNext()) {
          Image img = (Image)iter.next();
          img.dispose();
       }
    }

}

In order to provide the best protection against leaks, new API should use this pattern as much as possible for views. The obvious problems with this are:
  1. Whoever implements the Site class can't possibly know about every type of resource that might be allocated by every view
  2. The Site interface would become ridiculously large and complicated.
It would be possible to work around this by writing new subclasses of Site that add new methods, but then Views that use the new APIs would not function properly when given a different Site implementation. The ideal solution would allow plugins to contribute new default behavior to the Site base class, and only require the Site class to be subclassed when this behavior needs to be specialized. This can be achieved by making Site adaptable. Rather than always adding new methods to Site, plugins couldcontribute adapters. Subclasses of site would still be able to override the default adapter behavior by explicitly implementing the adapter interface, but parts that use the adapter would no longer be tied to a particular Site implementation.

Here is an example that demonstrates the pattern:

/**
 * Adapter for a view site that allows allocation/deallocation of images
 */
public interface IImageAllocator {
    public Image createImage(ImageDescriptor toCreate);
    public void destroyImage(ImageDescriptor toDestroy);
}

/**
 * Implementation of the image allocator adapter provided via XML
 * by some plugin.
 */
public class ImageAllocator implements IImageAllocator, IDisposable {
   
public Image createImage(ImageDescriptor toCreate) {
       ...
    };

    public void destroyImage(ImageDescriptor toDestroy) {
       ...
    };

    public void dispose() {
       // Dispose any images leaked through createImage
    };
}

/**
 * Next attempt at a site interface
 */
public class Site implements IAdaptable {

    List disposableAdapters = new ArrayList();

    public Object getAdapter(Class adapterType) {
       Object result = //...some magic that looks through the adapter extension point and
                       //creates an instance of ImageAllocator

       if (adapter instanceof IDisposable) {
          disposableAdapters.add(adapter);
       }

       return result;
    }

    /**
     * Allow all the adapters to clean up anything they allocated.
     */
    public void dispose() {
       Iterator iter = disposableAdapters.iterator();
       while(iter.hasNext()) {
          IDisposable next = (IDisposable)iter.next();

          next.dispose();
       }
    }
}

/**
 * Base class for all views.
 */
public abstract class View {
    private IAdaptable mySite;

    public View(IAdaptable viewSite) {
       mySite = viewSite;
    }

    public IAdaptable getSite() {
       return mySite;
    }

    public abstract String getTitle();

    public void dispose() {};
}


Notice that this type of adapter is slightly different from most other adapters in Eclipse since it keeps some state and need to be notified when the Site is disposed.

A particular view would look like this:

/**
 * My implementation of a view
 */
public class MyView extends View {
    IImageAllocator allocator;

    public MyView(IAdaptable viewSite) {
       allocator = (IImageAllocator)viewSite.getAdapter(IImageAllocator.class);

       if (allocator == null) {
          throw new Exception("I can't exist without images!");
       }
    }

    //...
}

Most views will contain similar code in their constructor that requests a set of adapters from their site and check that they aren't null. We can add some syntactic sugar to remove this repetition. Rather than having the view explicitly request every adapter it needs, it can take them as arguments to its constructor and the factory that creates the view can use reflection to ensure that it gets all the adapters it needs. Using this pattern, the same view would look like this:

/**
 * My implementation of a view
 */
public class MyView extends View {
    IImageAllocator allocator;

    public MyView(IImageAllocator imageAllocator) {
       allocator = imageAllocator;
    }

    //...
}

Since the entire Site API would be provided by adapters, the Site class could be reduced to this:

/**
 * My implementation of a view
 */
public abstract class Site implements IAdaptable {
    public abstract Object getAdapter(Class adapter);
    public abstract void dispose();
}

In the component framework, this simplified Site interface is called IContainer. We now have a highly extensible Site interface, however the same versioning problems apply to the view as well. The parent of the view may want to access methods on the View, but we don't want to be continually adding things to the View base class. For this reason, we communicate with the view through adapters as well.

This is the core pattern behind the component framework. It consists of:
  1. An extension point that allows 3rd party plugins to contribute interfaces to components (views and their sites)
  2. A factory that allows components to be created through constructor injection
  3. A container interface that manages the lifecycle of components and their adapters

1.2 Requirements

Primary goals:

1.2.1 Robustness / leak proofing

Much of the Eclipse API is currently accessed through singleton objects. This makes it easy for a view or editor to leak listeners, fail to clean up reference counts, leak OS resources, etc. since there is no way of tracking which resources were allocated by a particular view. This problem would be reduced if views and editors were more like mini-applications. The view or editor would access the rest of the world through a set of local interfaces. When the view or editor is destroyed, all the interfaces would be notified giving them a chance to clean up any leaked resources.

For example, instead of reaching into a global preference store, a view or editor could access all preferences through a local preference adapter. A unique instance of the local preference adapter would be created for each view, and would be disposed with the view. When the preference adapter is destroyed it would clear its listener list, ensuring that the view will not leak any global listeners. This example shows what we mean by a component. Essentially, a component is an object that communicates with the rest of the application through a set of interfaces given to it in its constructor.

1.2.2 Nesting of components

There is demand for the ability to embed views and editors inside one another. Some examples:
Many downstream plugins have solved these problems by creating their own frameworks for reusable UI components. Unfortunately, this only works for specific views and editors, does not encourage interoperability between plugins that have adopted different frameworks, and requires a lot of work. The goal here is to adopt a framework in the workbench itself that allows all editors, views, and other workbench objects to be easily nested.

1.2.3 Creating parts outside the workbench

It should be possible to instantiate views and editors outside the workbench. Some examples:

1.2.4 Scalability

The workbench currently offers a closed interface to parts. This forces parts to reach to global objects whenever they need something that isn't available from their site. It should be possible for any plugin to contribute to the set of local API available to a part, and it should be possible to instantiate a part even if its parent doesn't know about all of a part's dependencies.

1.2.5 Ease of use

Views and editors currently have a complicated lifecycle that must be managed by the workbench. This complexity should not be exposed to client code.

2.0 Component Framework

First, some terminology. The pluggable objects that clients write are called "components". For example, the class that contains the implementation of a view itself would be called a component. A component exists inside an IContainer. In the case of editors and views, IContainer replaces the existing IWorkbenchSite. Unlike IWorkbenchSite, the component never needs to directly access the IContainer since all of its useful interface is provided through adapters. Instead, the component supplies a constructor that takes all the interfaces it needs, and it communicates with the outside world through these interfaces.

Components are built using constructor injection. Constructor injection means that the framework looks at the object's constructor to determine what it needs to do to build that object.

Components:
Components do not:
Any class with these properties can be used as a component. Since components are plain-old-java-objects, they do not need to depend on the component framework. Components are fully initialized by their constructor, meaning it is never necessary to call an initialize method or any combination of set methods after constructing the object. Components never accept null as an argument to their constructor.

For example, if a view could be created as a component it might look like this:

/**
 * "Hello world" view using the component framework
 */
public class HelloWorldView {
    public HelloWorldView(Composite parent) {
        Label helloWorld = new Label(parent, SWT.NONE);
        helloWorld.setText("Hello world");
    }
}

The HelloWorldView component depends on one other object: a Composite created for it by whoever instantiated the view. Compare this with the same view written using the existing API:

/**
 * "hello world" view using the Eclipse 3.0 API.
 */
public class HelloWorldView extends ViewPart {
    public void createPartControl(Composite parent) {
        Label helloWorld = new Label(parent, SWT.NONE);
        helloWorld.setText("Hello world");  
    }

    public void setFocus() {
    }
}

The main difference is that the component version does not require the ViewPart base class and is fully initialized after construction. In both cases, the extension point markup would look like this:

<extension point="org.eclipse.ui.views">
    <view
       name="Title Test View"
       icon="icons\view.gif"
       class="org.eclipse.ui.mytest.HelloWorldView"
       id="org.eclipse.ui.mytest.HelloWorldViewID">
    </view>
</extension>

2.1 Instantiating a view

Components are instantiated using factories. For the moment, let's focus on views before we explore components in general. Views could be created using a method on IWorkbenchPage that would look something like this. Note that this example is only intended to illustrate the general idea -- the actual protocol for creating views is likely to change.

/**
 * Creates a view of the given type inside the given composite. The caller must dispose the container once they are done with it.
 *
 * @param viewId id of the view extension to use
 * @param parentComposite parent composite for the view
 * @return an IContainer that contains all components needed for the view
 */
IContainer createView(String viewId, Composite parentComposite);


This creates a new instance of the view in the given composite. Notice that the factory method doesn't return an instance of the view itself but an instance of IContainer, which looks something like this:

/**
 * Main interface to a component.
 */
public interface IContainer extends IAdaptable {
     public void dispose();
}

This interface wraps the component and all of its dependencies. Once we've obtained an IContainer handle, we are obligated to dispose it when we are done with it. All access to the component is done through adapters, which insulates the application from changes in individual views. For example, if a view that previously implemented the IViewInterface1 migrates to using the newer IViewInterface2, its container will continue to work as long as someone has provided an adapter between the two versions of IViewInterface. The getAdapter() method on IContainer searches for adapters in the following order:

1. If the component itself implements the adapter type, it returns the component.
2. If the component itself implements IAdapter, and the component's getAdapter(...) method returns non-null, we return that adapter.
3. If the adapter manager has an adapter between the component and the adapter type, we return that adapter.
4. If the container's factory can construct a component of the requested type, we create that component, add it as a local dependency to the IContainer, and return it.
5. Return null

For example, it would be possible to create our HelloWorldView (above) in a modal dialog like this:

void createHelloWorldViewInADialog(IWorkbenchPage page) {
    Display display = Display.getDefault()
    Shell shell = new Shell(Display.getDefault());

    shell.setLayout(new FillLayout());

    // Create the view and its widgets
    IContainer myView = workbenchPage.createView("org.eclipse.ui.mytest.HelloWorldViewID", shell);

    shell.open();

    while (!shell.isDisposed()) {
       if (!display.readAndDispatch ()) display.sleep ();
    }

    // Dispose the view
    myView.dispose();
}

2.2 Instantiating components in general

All components are constructed using an IContainerFactory, however most component-based extension points will provide some sort of convenience method to wrap their IContainerFactory. The IWorkbenchPage.createView method in the previous section is a convenience method for the org.eclipse.ui.views extension point.
 
IContainerFactory looks like this:

/**
 * Factory for IContainer instances. The default factory is returned by
 * Components.getFactory(). Clients wishing to implement their own specialized
 * factories should call createDerivedFactory() to get access to a factory
 * whose behavior can be modified programmatically. Not intended to be
 * implemented by clients.
 *
 * @since 3.1
 */
public interface IContainerFactory {
   
    /**
     * Creates and returns a new IContainer instance, given the
     * implementation class for its component. The caller MUST call IContainer.dispose()
     * when it is done with the component. The factory does not need any prior
     * knowledge of the component class.
     *
     * @param componentImplementation concrete class to be instantiated by the factory. The class
     *           must be a valid component (it must have exactly one constructor which only
     *           references other component interfaces known to this factory).
     * @return a newly constructed <code>IContainer</code> instance
     * @throws CoreException if unable to create the component
     */
    public IContainer createContainer(Class componentImplementation) throws CoreException;
   
    /**
     * Creates a specialization of this factory. By default, the specialized
     * factory will have the same behavior as its parent. However, the derived
     * factory can add, or override the implementation for any components.
     *
     * @return new factory instance that allows individual components to be overridden.
     * By default, the derived factory will delegate all of its behavior to the receiver.
     * Changes in the receiver will affect the derived factory.
     */
    public IMutableContainerFactory createDerivedFactory();
}


All arguments to a component's constructor are provided by its factory. Different factories know how to create different types of components or will provide different implementations for the same components. All factories are derived from the root factory, which is returned by Components.getFactory(). A plugin can create derived factories to supply alternative adapter implementations to the components it creates.

In our case, we will create a container factory to pass a specific Composite into the constructor of HelloWorldView. The code looks like this:

// Get the global factory
IContainerFactory viewFactory = Components.getFactory();

// Create the Composite for the view
Composite viewComposite = new Composite(parentComposite, SWT.NONE);
viewComposite.setLayout(new FillLayout();

// Create a specialized factory that knows about the view's composite and plugin bundle
IMutableContainerFactory derivedFactory = viewFactory.createDerivedFactory();

// Add the view's composite to the factory so that it can be seen by the view constructor
derivedFactory.addComponentInstance(viewComposite);

// Add the view's plugin bundle to the factory (not required in this example, but this
// is recommended practise for any component created from an extension point).
derivedFactory.addComponentInstance(pluginBundle);

// Create the view. Provide its constructor and some context (the page that created it).
IContainer view = derivedFactory.createContainer(HelloWorldView.class);

// Do something with the view
// ...

// Now clean up
view.dispose();
viewComposite.dispose();


This code will work, but it has two problems:

1. Even if HelloWorldView didn't require a Composite, we would still have created one (which is wasteful).
2. We need to manually dispose the view's composite after we're done with it. This defeats the point of IContainer.dispose(), which is supposed to clean up all of the component's dependencies automatically.

2.3 Creating dependent components on demand

Rather than managing the view's Composite ourselves, we could supply a factory that knows how to create and destroy Composites as needed. Such a factory would look like this:

/**
 * ComponentAdapter for creating and managing SWT Composites.
 */
public class CompositeFactory extends ComponentAdapter {

    private Composite parent;
   
    /**
     * The SWT composites created by this factory will all be children of the given
     * composite.
     */
    public CompositeFactory(Composite parent) {
        this.parent = parent;
    }
   
    protected Object create(IAdaptable availableAdapters) throws CoreException {
        // Create a new Composite
        Composite newChild = new Composite(parent, SWT.NONE);
        newChild.setLayout(new FillLayout());
        return newChild;
    }

    protected void dispose(Object toDispose) {
        ((Composite)toDispose).dispose();
    }

    public String getInterfaceName() {
        // Return the fully qualified class name of the Composite class. The framework will call
        // this method to determine what type of interface will be implemented by the objects
        // constructed by this factory.

        return Composite.class.getName();
    }
}

Using CompositeFactory, we could construct HelloWorldView like this:

// Get the global factory
IContainerFactory viewFactory = Components.getFactory();

// Create a specialized factory that knows about the view's composite and plugin bundle
IMutableContainerFactory derivedFactory = viewFactory.createDerivedFactory();

// Add a factory that can create the view's composite
derivedFactory.addComponentFactory(new CompositeFactory(parentComposite));

// Add the view's plugin bundle to the factory (not required in this example, but this
// is recommended practise for any component created from an extension point).
derivedFactory.addComponentInstance(pluginBundle);

// Create the view. Provide its constructor and some context (the page that created it).
IContainer view = derivedFactory.createComponent(HelloWorldView.class);

// Do something with the view
// ...

// Now clean up
view.dispose();

The composite will now be allocated as needed and disposed inside the call to view.dispose().

2.4 Declaring Component Interfaces

The types that a component receives in its constructor are called component interfaces. Component interfaces are registered through the org.eclipse.core.component.interface extension point, and each has a default implementation. When a component is created, its parent may provide an implementation of any service it knows about. If the component requests an interface that isn't provided explicitly by the parent, the default implementation is used.

Here is an example component interface:

/**
 * Component interface:
 *
 * Provides a context for reporting and logging exceptions.
 */
public interface IErrorContext {
    /**
     * Create an IStatus error describing the given throwable
     */
    public IStatus createStatus(Throwable t);

    /**
     * Create an IStatus message with the given severity, message, and (optional) throwable
     */
    public IStatus createStatus(int severity, String message, Throwable t);

    /**
     * Logs an IStatus message
     */
    public void log(IStatus status);

    /**
     * Logs an exception to the system log
     */
    public void log(Throwable t);
}


Here is the associated default implementation:

/**
 * Default implementation of the IErrorContext interface:
 *
 * Creates and logs errors in the context of a plugin bundle
 */
public class ErrorContext implements IErrorContext {
    private Bundle pluginBundle;

    public ErrorContext(Bundle pluginBundle) {
          this.pluginBundle = pluginBundle;
    }

    public IStatus createStatus(Throwable t) {
       String message = t.getMessage();
       if (message == null) {
          message = t.getString();
       }
       return createStatus(Status.ERROR, message, t);
    }

    public IStatus createStatus(int severity, String message, Throwable t) {
       return new Status(severity, pluginBundle.getSymbolicName(), Status.OK, message, t);
    }

    public void log(Throwable t) {
       log(createStatus(t));
    }

    public void log(IStatus status) {
       Plugin plugin = Platform.getPlugin(pluginBundle.getSymbolicName());
       plugin.getLog().log(status);
    }
}

Finally, here is the extension markup:

<extension point="org.eclipse.core.component.interface">
   <interface
     class="org.eclipse.ui.workbench.ErrorContext"
     interface="org.eclipse.ui.workbench.IErrorContext"
     childadapter="false"
     scope="plugin">
   </interface>
</extension>

Notice that the extension XML needs to indicate which service interface is being implemented by the service, but not its dependencies. When it comes time to create the service, the framework detects that the ErrorContext needs to be associated with a plugin Bundle by examining its constructor. This means that if plugin A defines the ErrorContext service and plugin B defines a view that uses the service, then all of the status messages constructed by the view will be associated with the view's own plugin - not the plugin that defined the ErrorContext service.

The scope attribute is a path indicating how much context is needed to create the service. The plugin scope indicates that this service is associated with a plugin, which is why it is allowed to take a Bundle in its constructor. More on scopes later.

The childadapter="false" tag is optional. The default value, false, indicates that a component can take the interface in its constructor and/or supply an alternative implementation to its children. If childadapter="true" were specified, the component would be allowed to implement the interface or request it from its children. Essentially, this determines if the interface is intended for top-down or bottom-up communication between a component and its parent.

Plugins are not allowed to declare services using interfaces defined in another plugin. This prevents two plugins from defining a service using the same interface, and ensures that circular dependencies can only occur within the same plugin.

2.5 Using Component Interfaces

This example shows how a component can use an interface supplied by its parent.

public class MyView {
    private IErrorContext errorContext;

    public MyView(Composite parent, IErrorContext errorContext) {
       this.errorContext = errorContext;

       Button errorButton = new Button(parent, SWT.PUSH);
       errorButton.setText("Log an exception");
      
       errorButton.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                try {
                   // Throw a NPE
                   String myString = null;
                   String bogusCode = myString.substring(10, 30);
                } catch (Exception e) {
                   // Log the NPE using the error context
                   errorContext.log(e);
                }
            }
       });
    }
}

This view creates a button which, when pressed, will throw a NPE and log it. The status message will be constructed and logged using the IErrorContext interface we defined above. The default implementation of the interface is associated with a unique component instance, so if we were to create two instances of MyView we would also end up with two instances of ErrorContext.

There is never any ambiguity about which implementation of IErrorContext to use. If the parent of MyView explicitly supplies an IErrorContext service, that implementation is used. Otherwise, the unique default implementation is used. There can never be two global implementations.

2.6 Lifecycle

Many components need to do explicit cleanup. Components that need to perform cleanup will implement the IDisposable interface. For example, the following component attaches a listener to a global preference store and detaches it when done.

/**
 * Provides access to a plugin's preference store in a manner that prevents listener leaks.
 */
public interface IPreferences {
    public String getString(String prefId);
    public void setString(String prefId, String value);
    public void addListener(IPropertyChangeListener l);
    public void removeListener(IPropertyChangeListener l);
}

/**
 * Concrete implementation of the IPreferences interface
 */
public LocalPreferenceStore implements IPreferences, IDisposable {
    ListenerList listeners = new ListenerList();
    Preferences prefs;
   
    private class PropertyChangeListener implements Preferences.IPropertyChangeListener {
        /*
         * @see org.eclipse.core.runtime.Preferences.IPropertyChangeListener#propertyChange(org.eclipse.core.runtime.Preferences.PropertyChangeEvent)
         */
        public void propertyChange(Preferences.PropertyChangeEvent event) {
            firePropertyChangeEvent(event.getProperty(), event.getOldValue(), event.getNewValue());
        }
    }

    PropertyChangeListener listener = new PropertyChangeListener();

    /**
     * Create a wrapper around the given bundle's preference store.
     */
    public LocalPreferenceStore(Bundle pluginBundle) {
       Plugin plugin = Platform.getPlugin(pluginBundle.getSymbolicName());
       prefs = plugin.getPluginPreferences();
       prefs.addListener(listener);
    }

    private void firePropertyChange(String name, Object oldValue, Object newValue) {
        PropertyChangeEvent event = new PropertyChangeEvent(this, name, oldValue, newValue);
        Object[] listeners = this.listeners.getListeners();
        for (int i= 0; i < listeners.length; i++)
            ((IPropertyChangeListener) listeners[i]).propertyChange(event);
    }

    /**
     * This method will automatically be called when the component that uses this adapter is disposed.
     * It should clean up anything that has been allocated by the service.
     */
    public void dispose() {
       prefs.removeListener(listener);
    }

    public String getString(String prefId) {
       return prefs.getString(prefId);
    }

    public void setString(String prefId, String value) {
       prefs.setValue(prefId, value);
    }
}

Similarly, components may implement the IDisposable interface. A component that implements IDisposable will be disposed before the objects it depends on, as shown by the following example:

public LifecycleService implements IDisposable {
    public LifecycleService() {
       System.out.println("LifecycleService created");
    }

    public void dispose() {
       System.out.println("LifecycleService destroyed");
    }
}

public LifecycleView implements IDisposable {
    public LifecycleView(LifecycleService service) {
       System.out.println("LifecycleView created");
    }

    public void dispose() {
       System.out.println("LifecycleView destroyed");
    }
}

Finally, we could execute the following code to trace when the LifecycleService is created and destroyed:

IMutableContainerFactory factory = Components.getFactory().createDerivedFactory();
factory.addComponentImplementation(LifecycleService.class);

System.out.println("Creating a LifecycleView");

IContainer viewComponent = factory.createComponent(LifecycleView.class);
viewComponent.dispose();

System.out.println("Creating two LifecycleViews");

IContainer viewComponent2 = factory.createComponent(LifecycleView.class);
IContainer viewComponent3 = factory.createComponent(LifecycleView.class);
viewComponent3.dispose();
viewComponent2.dispose();

The code will generate the following output:

Creating a LifecycleView
LifecycleService created
LifecycleView created
LifecycleView destroyed
LifecycleService destroyed

Creating two LifecycleViews
LifecycleService created
LifecycleView created
LifecycleService created
LifecycleView created
LifecycleView destroyed
LifecycleService destroyed
LifecycleView destroyed
LifecycleService destroyed

2.7 Dynamic Interfaces

Component interfaces are dynamic in the sense that they may be registered or unregistered as their plugins are installed or uninstalled. If a plugin providing component interface is uninstalled, that interface will become unavailable and it will no longer be possible to construct objects that depend on the interface.

The objects instances themselves are not dynamic. Once an adapter is created to supply an interface, it will exist for as long as the component it was created for. Even if the a plugin stops providing an interface, all instances of that interface will continue to exist until disposed. All interfaces used by a component will be created before that component, so components do not need to query for the existence of services or to wait for services that will be created after-the-fact.

2.8 Optional Interface

Sometimes a component does not require an interface but can make use of it if it exists. This is normally not necessary for any interface registered through the org.eclipse.core.component.interface extension point since the default implementation will guarantee that the interface always exists even if the parent context doesn't know about it. However, this situation can occur if a parent and child are communicating with an interface that wasn't originally intended for use as a component interface, or if the child requires a variable set of interfaces.

Note: this pattern doesn't actually allow the component to be used in a wider context, but it does allow the component to request interfaces that aren't registered by the normal means.

This can be done by taking an argument of type IAdaptable, like this:

public interface INameService {
    public String getName();
}


/**
 * A component with no dependencies, but which can optionally accept an IMemento for initialization.
 */
public class MyComponent {

    String myName = "default name";

    public MyComponent(IAdaptable optionalInterfaces) {

       // Check if an INameService service exists.
       INameService nameService = (INameService)optionalInterfaces.getAdapter(INameService.class);
      
       if (memento != null) {
          myName = nameService.getName();
       }

       System.out.println("created component with name = " + myName);
    }
}

We could create the component like this:

// Create a memento with the name "custom name"
INameService nameService = new INameService() {
    public String getName() {
       return "custom name";
    }
}

// Create a factory which knows about our memento
IMutableContainerFactory context = Components.getFactory().createDerivedFactory();
context.addComponentInstance(nameService);

// Use the factory to instantiate MyComponent
IContainer component = context.createContainer(MyComponent.class);
// Will print the message "created component with name = custom name"
component.dispose();


We can also create the component without the optional INameService:

IContainer component = Components.getFactory().createContainer(MyComponent.class);
// Will print the message "create component with name = default name"
component.dispose();



WARNING: This pattern should be used with care since it adds special cases to the component code. It is mainly inteded for advanced situations where a component needs to request an interface that isn't normally visible in its scope.

The preferred method of dealing with missing interfaces is to provide a default implementation, like this:

<extension point="org.eclipse.core.component.interface">
   <interface
     class="org.eclipse.ui.DefaultNameService"
     interface="org.eclipse.ui.INameService">
   </interface>
</extension>


/**
 * Default implementation of INameService that will be used if the parent doesn't explicitly provide one
 */
class NameService implements INameService {
    public String getName() {
       return "default name";
    }
}

/**
 * A component with no dependencies, but which can optionally accept an INameService for initialization.
 */
public class MyComponent {

    String myName;

    public MyComponent(INameService nameService) {
       myName = nameService.getName();

       System.out.println("created component with name = " + myName);
    }
}


With this approach, it is still possible to create a MyComponent with or without an INameService, but the special-case code for handling missing interfaces is written once inside the default implementation rather than many times inside each component that uses the interface.

2.8 Component Scopes

Scopes are a way to determine what interfaces can be used in the constructor of a component. At some point, a programmer is going to ask "What interfaces am I allowed to pass into the constructor of my view?" Ultimately, this could be determined by examining the dependencies between each service, but it is not reasonable to expect a programmer to have this level of knowledge of the dependencies between services.

Scopes limit the dependencies between services in order to make this question easier to answer. For example, views are allowed to use any service in the "/plugin/part/view" scope. This makes it easy to write a PDE extension which displays the list of interfaces available to a views.

The only difference between scopes is how much context the components are allowed to expect. For example, components in the plugin scope will be given a plugin Bundle and components in the plugin/part scope need to be associated with a particular SWT Composite. Components may depend on services in the same scope or in a more general scope. It is still possible for Components to reference services in a more specific scope, but they must do so as an optional interface (as described in section 2.7).

Eclipse defines the following standard scopes.

Scope path
Required context
Description
/
None
Any service that omits the scope attribute automatically belongs to the global scope. Services in the global scope can be used by any other component, but may only depend on other global services. Global services are not given any context from the application.
/plugin
Bundle
Components in the /plugin scope are created in the context of a plugin bundle. All executable extensions (any component created using an extension point) belong to the plugin scope. Components in this scope can reference their own plugin Bundle in their constructor. Services can reference the Bundle associated with the component that requested them.
/plugin/part
Bundle, Composite
Components in the /plugin/part scope are associated with an SWT Composite. Components in this scope are given their own SWT Composite. They may change the layout on the given Composite, but may not change its layout data. The composite will be managed as a service, so the component does not need to dispose it. Components in this scope may be used as editors, views, or both.
/plugin/part/view
Bundle,
Composite,
IWorkbenchPage
All views belong to this scope. They are given the IConfigurationElement containing their extension markup, and an IWorkbenchPage.
/plugin/part/editor
Bundle,
Composite,
IWorkbenchPage,
IEditorInput
All editors belong to this scope. They are given the IConfigurationElement containing their extension markup, an IWorkbenchPage, and the IEditorInput containing their input.

These scopes should be sufficient for most situations. However, some plugins may wish to create new scopes in order to pass additional context to the objects in an extension point they provided.

Any plugin that defines its own scopes must include its fully qualified plugin ID as part of the scope path. For example, if the plugin org.eclipse.myplugin extends the /plugin/part/editor scope, the new scope name would look like /plugin/part/editor/org.eclipse.myplugin.scopename. All scope paths that do not contain a period (.) are reserved by the framework.

It is never a breaking change to move a service into a more general scope. However, it is always a breaking change to move a service into a more specific scope. A service interface may only exist in one scope.

2.9 Shared Adapters

Normally, a new instance of a default interface implementation is created for each component that requests it. This allows the adapter to keep some state associated with the component and to clean up resources once they are no longer needed by the component. However, some interface do not need any state or expose any API that could be used to create a leak. For example, the IErrorContext interface in section 2.4 could safely share one instance between all components in the same plugin. In order to reduce memory consumption, the component API should support the notion of a shared adapter. A shared adapter will be reused in the broadest possible context permitted by its scope. For example, an adapter for an interface in the global scope would become a singleton, shared adapters in the plugin scope will have at most one instance per plugin, and the shared flag will be ignored in the /plugin/part scope.

This is intended as a future optimization, and will not be included in the initial version of the component framework

3.0 Views and Editors as Components

This section describes how the workbench uses the component framework to create editors and views.

3.1 Interfaces offered by the workbench

Parent interfaces: (Interfaces given to a component as arguments in its constructor)

Scope
Interface
Description
/plugin
IErrorContext
Provides facilities for logging exceptions
/plugin
ISwtResources
Provides facilities for allocating SWT resources such as Fonts, Images, and Colors
/
INameable
Parts can use this service to change their name, content description, title image, and tooltip. The default implementation ignores all method calls.
/
IMemento
Parts will receive a memento which they can use to load previously-saved state
/plugin
IPartFactory
Interface that can be used to create child views, editors, and other UI parts
/plugin/part
IActionBars2
Interface used to add to the toolbar, cool bar, etc. Currently exposed on the view site.




Child interfaces: (Services that a component offers to its parent - either by implementing directly or as an adapter)

Scope
Interface
Description
/plugin/part
IFocusable
Parts can implement this service to allow their parent
/
IMultiPart
Parts should implement this if they contain other parts and have the notion of an "active" child. Other services can use this service if their default implementation should redirect to the active child.
/
IPersistable
Parts may implement this interface if they wish to save their state between sessions.





3.1.1 IMultiPart: Redirecting adapters from the active child

In UI code, it is common to create aggregate components that have an "active" child. This section suggests a general pattern for redirecting components in this way. Many of the adapters that the aggregate offers its parent will redirect their implementation to the active child. The aggregate itself will know how to select its active child, but it may not know about all the interfaces that need to be redirected in this manner. The default implementation of an interface determines how it should be redirected.

This can be shown using a concrete example. Workbench parts offer an IFocusable adapter to their parent that allows their parent to give them focus.

/**
 * Parts can implement this interface if they wish to overload the default
 * setFocus behavior.
 */
public interface IFocusable {
    /**
     * Gives focus to the part
     */
    public void setFocus();
}


This interface should be multiplexed by default. For example, if we call setFocus() on a multi-page editor that doesn't explicitly implement the IFocusable interface, it should redirect focus to its active child. If the part doesn't have an active child, focus should go directly to the part's main control. To supply this default behavior, we provide a default implementation of IFocusable:

/**
 * Default implementation of the IFocusable service. If a part doesn't explicitly
 * provide an adapter for IFocusable, this implementation will be used.
 */
public class DefaultFocusable implements IFocusable {

    private Composite control;
    private IMultiPart activePart;
   
    /**
     * Creates the default implementation of IFocusable, given the main control
     * of the part and an IMultiPart that can be queried for the active child.
     *
     * @param toGiveFocus main control of the pane
     * @param activePartProvider multiplexer that returns the active part
     */
    public DefaultFocusable(Composite toGiveFocus, IMultiPart activePartProvider) {
        control = toGiveFocus;
        activePart = activePartProvider;
    }
   
    /**
     * First, try to give focus to the active child. If there is no active child, give focus to
     * the main control.
     */
    public void setFocus() {
        // If this part has the notion of an active child, give that child focus
        Object current = activePart.getCurrent();
        if (current != null) {
            IFocusable focusable = (IFocusable)Components.getAdapter(current, IFocusable.class);
            if (focusable != null) {
                focusable.setFocus();
                return;
            }
        }
       
        // If the part has no children, then give focus to the part itself.       
        control.setFocus();
    }
}


Here is the XML markup to register the interface:

<service
    interface="org.eclipse.ui.workbench.services.IFocusable"
    class="org.eclipse.ui.internal.part.serviceimplementation.DefaultFocusable
    childadapter="true"
    scope="plugin/part"/>


All of the interfaces we have looked at so far have been offered by a parent for use in a child's constructor. In this case, the interface is offered as an adapter on the child to be used by the parent. In both cases, we support the notion of a default implementation that is declared through XML. Any service that should be redirected based on the active child will take IMultiPart in its constructor. Here is an example part that supports multiplexing:

/**
 * Part that explicitly implements IFocusable.
 */
public class Page implements IFocusable {
    Text textField;

    public Page(Composite control) {
       textField = new Text(control, SWT.NONE);
    }

    public void setFocus() {
       textField.setFocus();
    }
}

/**
 * Non-multiplexing part that relies on the DefaultFocusable service
 * to give focus to its composite.
 */
public class Page2 {
    Text textField;
   
    public Page(Composite control) {
       textField = new Text(control, SWT.NONE);
    }
}

/**
 * Multiplexing part that contains a Page, a Page2, and a checkbox.
 * When the checkbox is selected, the first page will be active. When the
 * checkbox is deselected, the second page will be active. Calling setFocus
 * on the MultiplexingView will always give focus to the active page.
 */
public class MultiplexingView implements IDisposable, IAdaptable {
    private IContainer page1;
    private IContainer page2;

    // Helper class that implements the IMultiPart interface
    private Multiplexer multiplexer = new Multiplexer();
    private Button checkBox;

    public MultiplexingView(Composite myControl) {
       myControl.setLayout(new RowLayout());

       checkBox = new Button(myControl, SWT.CHECK);
       checkBox.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                updateActivePart();
            }
       });

       checkBox.setText("Activate part 1");

       // Create the child controls
       IMutableComponentFactory childFactory = Components.getFactory().createDerivedFactory();

       // Use the CompositeAdapter class we created earlier in order to provide each page
       // with its own control.
       childFactory.addComponentInstance(new CompositeAdapter(myControl));

       page1 = childFactory.createContainer(Page.class);
       page2 = childFactory.createContainer(Page2.class);

       // Activate the initial part
       updateActivePart();
    }

    private final void updateActivePart() {
        if (checkBox.getSelection()) {
            multiplexer.setCurrent(page1);
        } else {
            multiplexer.setCurrent(page2);
        }

    }

    /**
     * In order to indicate that this is a multiplexing part, we need to implement
     * the IMultiplexer adapter. We simply redirect to the multiplexer object.
     */
    public Object getAdapter(Class adapterType) {
       if (adapterType == IMultiplexer.class) {
          return multiplexer;
       }
    }

    /**
     * Release any resources allocated in the constructor
     */
    public void dispose() {
       page1.dispose();
       page2.dispose();
    }
}


Notice that MultiplexingView itself doesn't know about the IFocusable interface, however if we were to create a MultiplexingView and ask its container for an IFocusable interface, we would get an interface that behaves as expected.

// Stupid example that creates a MultiplexingView, gives it focus, then destroys it

// Create a MultiplexingView

IMutableComponentFactory childFactory = Components.getFactory().createDerivedFactory();
childFactory.addComponentInstance(new CompositeAdapter(myControl));

IContainer viewContainer = childFactory.createContainer(MultiplexingView.class);

// Give focus to the active part
IFocusable focusable = viewContainer.getAdapter(IFocusable.class);
focusable.setFocus();

// Destroy the view
viewContainer.dispose();
   
This example also shows how the same default implementation can work for both multiplexing and non-multiplexing parts. The Page2 class doesn't implement IFocusable or IMultiPart, but we are still able to ask for an IFocusable interface and use it to give focus to the part.

3.1.2 INameable: Changing the name of a part

In order to change their name, existing parts need to implement a set of get methods, maintain a listener list, and send notifications when their name changes. Component-based parts will change their name by using an INameable interface provided by their parent. INameable looks like this:

public interface INameable {
    public void setName(String newName);
    public void setContentDescription(String contentDescription);
    public void setImage(Image theImage);
    public void setTooltip(String toolTip);
}

For example, a view could set its name like this:

public class MyView {
    public MyView(Composite parent, INameable name) {
       name.setName("Some tab text");
    }
}

If a parent cares about the names of its children, it should provide an implementation of INameable that reacts when the child changes its name. Otherwise, the child will be given a default implementation of INameable that ignores all method calls. This eliminates the need to attach listeners to views.

3.1.3 IPartFactory: Creating child parts

Any part that wants to create other nested parts would use an IPartFactory interface.

public interface IPartFactory {
    public IContainer createView(String viewId, Composite parentComposite, IWorkbenchPage page, IContainerFactory services);
    public IContainer createEditor(String editorId, Composite parentComposite, IWorkbenchPage page,
IEditorInput input, IContainerFactory services);
    public IContainer createPart(String partId, Composite parentComposite, IContainerFactory services);
}