Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
Re: [platform-ui-dev] Component framework proposal


Mike,

Thanks for taking the time to look at this. Yikes: I can't believe so many typos slipped through!

I'd like to respond to a few comments here, though:



1. Selecting the "right" IErrorContext. There would only ever be one "right" instance of IErrorContext -- that would be the ErrorContext instance created specifically for the view that is associated with that view's plugin bundle. However, you are correct in that there could possibly be more than one implementation of IErrorContext. As the spec stands now, there is the possibility for more than one global service to exist on the same interface.

Currently, the "first" implementation is selected in plugin activation order, but this could be considered a programming error and could be enforced more rigidly. Really, a service interface is intended as a unique identifier for the service and declaring two services on the same interface should be an error (much like creating two views with the same ID). Unless anyone has a better suggestion, I will add the one-implementation-per-interface rule to the spec.

I suppose it would be possible to add "scopes" for services. The XML markup could specify whether the service is intended for global use, one particular extension point, parent-to-child communication, child-to-parent communication, etc... but I wasn't sure if this was any better than JavaDoc on the service interface.


2. You mention that a view might not be able to function in the context of a Composite + IWorkbenchPage. Doing crazy stuff like creating views inside dialogs is one of the motivations for this code change, so I'm rather hoping that it will be possible in all cases... but obviously I haven't written all the code yet. If you are aware of another dependency I haven't encountered yet, we should chat.

 
3. I think I should rewrite the "optional interfaces" section to make the alternatives more obvious. Instead of having the component ask "does this service exist", the service provider can simply provide an empty implementation of the service that exists in all contexts. In both cases the component is just as reusable but in the latter case the special-case code is written once inside the service implementation rather than many times inside of each component that uses it.


- Stefan




Mike Wilson/Ottawa/IBM@IBMCA
Sent by: platform-ui-dev-admin@xxxxxxxxxxx

11/03/2004 09:34 AM

Please respond to
platform-ui-dev

To
platform-ui-dev@xxxxxxxxxxx
cc
Subject
Re: [platform-ui-dev] Component framework proposal





See markup in red. Come chat if anything isn't clear.



McQ.




Stefan Xenos/Ottawa/IBM@IBMCA
Sent by: platform-ui-dev-admin@xxxxxxxxxxx
11/02/04 18:03
Please respond to
platform-ui-dev


To
platform-ui-dev@xxxxxxxxxxx
cc

Subject
[platform-ui-dev] Component framework proposal







The component framework proposal is now available online.

This is part of the spec for the workbench part API in Eclipse 3.1,
intended to support easier nesting and reuse of UI components.

http://dev.eclipse.org/viewcvs/index.cgi/%7Echeckout%7E/platform-ui-home/components-proposal/ComponentFrameworkProposal.html


 - Stefan

Title: Component Framework proposal


Component Framework Proposal


Revision 1.0.3
By Stefan Xenos and Nick Edgar
Last modified 2004/11/02




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.

Primary goals:

  • Robustness / leak proofing

  • Nesting of components

  • Allow components to be reused outside the workbench

  • Scalability (allow an open-ended set of components)

  • API versioning

  • Ease of use

1.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 services. When the view or editor is destroyed so would all of the services allocated for it. This gives each service a chance to clean up after itself.

For example, instead of reaching into a global preference store, a view or editor could access all preferences through a local preference service. A unique instance of the local preference service [Space issue?] would be created for each view, and would be disposed with the view. When the preference service 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 Nesting of components

There is demand for the ability to embed views and editors inside one another. Some examples:

  • An XML editor might embed the properties view

  • A refactoring wizard might include a source editor

  • A plugin may wish to create a set of pluggable UI components that are not views or editors themselves, but can be used inside any view or editor

  • Various workbench objects (like the PartSashContainer that handles the layout of docked parts within the workbench) could be exposed as API

Many downstream plugins have solved these problems by creating their own frameworks for reusable UI components. Unfortunately, [missing text?] 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.3 Creating parts outside the workbench

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

  • Unit-test editors and views by instantiating them within a JUnit test suite.

  • Create an RCP application that does not depend on the workbench but includes view-like pluggable parts that could also be used as views within Eclipse

1.4 Scalability

The workbench currently offers a closed set of services 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.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.

  • It should only require one method call to create a component and one method call to destroy it.

  • There should not be unnecessary duplication between XML and java code.

  • Components should not be responsible for dealing with error conditions (such as missing dependencies) that can be detected by the framework.

  • Components should not need to implement interfaces they don't care about.

  • A parent context should not need to provide child components with interfaces it doesn't care about.

2.0 Component Framework

Components are pluggable objects that are build 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:

  • Have exactly one constructor

  • Take zero or more other components as arguments to their constructor

  • Are fully initialized by their constructor

Components do not:

  • Take arrays or primitives as arguments to their constructor

  • Take more than one argument of the same type in their constructor

  • Need to support any particular base class or interface

Any class with these properties can be used as a component. Since components are plain-old-java-objects [POJO? :-)], 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 service 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 service: 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 IComponent 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: [hm... toy example? E.g. you don't know that the view can function reasonably in this context.]

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 be [missing words ]
     *
     * @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 MissingDependencyException if the requested component depends on another component that
     *         cannot be constructed by this factory
     * @throws CoreException if there is a more permanent problem with using the given class as
     *         a component [ *** Hunh? *** ]
     * @since 3.1
     */
    public IContainer createContainer(Class componentImplementation) throws MissingDependencyException, 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.
     * @since 3.1
     */
    public IMutableContainerFactory createDerivedFactory();
}


Notice that createComponent throws two types of exception. A CoreException indicates a problem with the component itself, and a MissingDependencyException indicates that some other component or adapter could not be found. This can help locate the cause of problems, and a robust extension point may want to disable components that throw CoreExceptions.

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 services to one specific extension point. It is also possible to create a derived factory in order to pass information to the constructor of one specific object, as we will do in this example. 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 as a service instance
derivedFactory.addComponentInstance(viewComposite);

// Add the view's plugin bundle as a service instance (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 IComponent.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:

/**
 * ServiceAdapter for creating and managing SWT Composites as services.
 */
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 availableServices) throws CoreException {
        // Create a new Composite which will be used as a service.
        // If we needed any additional
        Composite newChild = new Composite(parent, SWT.NONE);
        newChild.setLayout(new FillLayout());
        return newChild;
    }

    protected void dispose(Object service) {
        ((Composite)service).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 service will be 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 the view's composite as a service instance
derivedFactory.addComponentFactory(new CompositeFactory(parentComposite));

// Add the view's plugin bundle as a service instance (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 Global Services

Services are a type of component that can be used by other components. Services are registered through the org.eclipse.ui.component.service [obvious: not in o.e.ui if it is generic] extension point. Services are global in the sense that they are created by factories that are visible everywhere, however each instance of a services is associated with a specific IContainer. Each service implements a service interface. Other components can use the service interface in their constructor. When they do so, a new instance of the service will be created for that component which will be managed by the same IContainer.

Each component supplies an implementation and an interface. Here is an example service interface:

/**
 * Service 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 service implementation:

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

    public ExceptionLoggingService(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.ui.component.service">
   <service
     class="org.eclipse.ui.workbench.ErrorContext"
     interface="org.eclipse.ui.workbench.IErrorContext">
   </service>
</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.

2.5 Using services

This example shows how a component can use a service.

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 service
                   errorContext.log(e);
                }
            }
       });
    }
}


  1. 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 service we defined above. [Yikes! Now we have the “infinitely complex service web” problem. In a complex system built from this, what are the odds that “right” ErrorContext gets chosen?]

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 service 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 any of its services, 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 service 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 Services

Global service factories are dynamic in the sense that they come and go as plugins are installed or uninstalled. If a plugin providing a service is uninstalled, a new implementation is selected from a different plugin. If no other plugin provides that service, the service becomes unavailable and it will not be possible to construct components that use it.

Service instances themselves are not dynamic. Once a service is created, it will exist for as long as the component it was created for. Even if the a plugin stops providing a service, all instances of that service will continue to exist until disposed. All services 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.

Plugins that need more advanced dynamic support should use OSGI [It's written “OSGi”] services. The component framework is not intended as a replacement for OSGI, and there is no point in reimplmenting things that OSGI already does well. [Not clear. Do OSGi services provide similar features? Would it be difficult to implement something with your services and switch later? What are the trade offs, etc.]

2.8 Optional Services

Sometimes a component does not require a service but can make use of it service if it exists. This should usually be handled by explicitly creating an empty global implementation of the service, but sometimes it is more efficient to have the component explicitly check if the service exists. This can be done by taking an argument of type IAdaptable, like this:

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

    String myName = "default name";

    public MyComponent(IAdaptable optionalServices) {

       // Check if an IMemento service exists.

       IMemento memento = (IMemento)optionalServices.getAdapter(IMemento.class);
      
       if (memento != null) {
          String name = memento.getString("name");
          if (name != null) {
             myName = name;
          }
       }

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

We could create the component like this:

// Create a memento with the name "custom name"
IMemento memento = XMLMemento.createWriteRoot("test");
memento.putString("name", "custom name");

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

// 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 IMemento:

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 situations where a component requires an open-ended set of services or where it is not possible to offer a default implementation of a service. [But wouldn't I want to make the highest use possible of this capability, so that I could be used in the widest range of situations?]

2.9 Multiplexing services

In UI code, it is common to create aggregate components that have an "active" child. Many of the adapters that the aggregate offers its parent will redirect their implementation to the active child. We refer to this as "multiplexing" the service. The aggregate itself will know how to select its active child, but it may not know about all the services that need to be multiplexed. The default implementation of a service determines how and if it should be multiplexed.

This can be explained better 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 an implementation of IFocusable as a global service:

/**
 * 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 IMultiplexer activePart;
   
    /**
     * Creates the default implementation of IFocusable, given the main control
     * of the part and an IMultiplexer 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, IMultiplexer 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 service:

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


All of the services we have looked at so far have been offered by a parent for use in a child's constructor. In this case, the service is offered as an adapter on the child to be used by the parent. In both cases, the service is declared in the same way. Any service that supports multiplexing takes an IMultiplexer in its constructor. If the part does not support multiplexing, the IMultiplexer will always returns null from getCurrent. 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 IMultiplexer 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 service can work for both multiplexing and non-multiplexing parts. The Page2 class doesn't implement IFocusable or IMultiplexing part, but we are still able to ask for an IFocusable interface and use it to give focus to the part.

3.0 Views and Editors as Components

TODO [...]



Back to the top