Jump to: navigation, search

Eclipse4/RCP/Contexts

< Eclipse4‎ | RCP

The Eclipse 4 Application Platform manages state and services using a set of contexts; this information is used for injection. Contexts are used as the sources for Dependency Injection. In this respect, they are somewhat analogous to modules in Guice. Normally code should not have to use or know about the context.


What is a Context?

A context (a IEclipseContext) is a hierarchical key-value map. The keys are strings, often Java class names, and the values are any Java object. Each context has a parent, such that contexts are linked together to form a tree structure. When a key is not found in a context, the lookup is retried on the parent, repeating until either a value is found or the root of the tree has been reached.


The Use of Contexts in Eclipse 4

Eclipse 4 associates contexts to the container elements in the UI
Eclipse 4 uses contexts to simplify access to workbench services (the service locator pattern) and other interesting state. Contexts provide special support for creating and destroying values as necessary, and for tracking changes made to context values.

Values are normally added to an Eclipse Context via IEclipseContext#set(key,value) or #modify(key,value). Values are retrieved by #get(key), which returns null if not found. There is a special variant of get: Java class names are frequently used as keys and instances as values, so there is a special T get(Class<T>) that casts the value as an instance of T.

The power of contexts comes as Eclipse 4 associates a context with most model elements — the logical UI containers — such that the context tree matches the UI hierarchy. So an MPart and its containing MPerspective, MWindow, and the MApplication, each have contexts and are chained together. Looking up a key that is not found in the part will cause the lookup to continue at the perspective, window, and application. At the top of the tree is a special node that looks up keys among the available OSGi services. Many application-, window-, and view-level services are installed by the Eclipse 4 at various levels in the context hierarchy. Thus the placement of values within the context hierarchy, such as in the perspective or window's context, provides a natural form of variable scoping.

Note.png
Example
For example, many client-server applications may require communicating with multiple servers, but with one server chosen as a master server at any one time. An Eclipse 4 application could record this master server in the application's context using a well-known key (e.g., "MASTER_SERVER"). All parts requesting injection for that key will have the value resolved from the application's context. Should that value change, all parts will be re-injected with the new value. A particular part could have a different master server from other parts by setting the master in that part's context. All other parts will continue to resolve MASTER_SERVER from the application. But perhaps the developers later realize that it would be very powerful to have a different master server for each window. The master could instead be set in each window's context. Or perhaps the app would prefer to have a different master server for each perspective, or even on particular part stacks. Or the app could continue to set the normal master server in the application's context, and optionally override it on a per-window basis by setting the override value in the window's context.


Context Variables

Being able to resolve a value from somewhere in the context hierarchyis very powerful. But to change the value, we need to know where in the context hierarchy the value should be set. Rather than hard code this location, we can instead declare a context variable: we declare the variable at the appropriate context, and instead modify, rather than set, the context value: the context then looks up the chain to find the variable declaration and sets the value there. This separates defining where a context value should be placed from the code that actually sets it.

Note.png
Example (continued)
By declaring a context variable for the master server, if we later decide that we want the master-server to actually be maintained on a per-perspective basis, then we simply move the context variable definition to be on the perspective; the code obtaining and modifying the value is completely oblivious to the change.


Context Chains and the Active Context

The editor is the active leaf
Contexts are chained together via the parent link. A context may have many children, but a context only exposes its active child. The chain of active children from a node is called its active branch, and the end node is the active leaf. There are many active branches in a context tree, but there is only ever a single active branch from the root.

A node can be made active in two ways. Calling #activate() makes the receiver the active child of its parent node, but does not otherwise disturb the rest of the tree. Calling #activateBranch() on the other hand effectively the same as:

   void activateBranch() {
      activate();
      if(getParent() != null) getParent().activateBranch(); 
   }

It makes the receiver the active child of its parent, and then recursively calls #activateBranch() on its parent.

It's often useful to resolve values from the active leaf with #getActive(key).

Eclipse 4 keeps its IEclipseContext activation state in sync with the UI state, such that the active window's context is the active window-level context, and each window's active part is that window's active leaf o.

Context Functions

Contexts support a special type of value called a context function. When a retrieved key's value is a context function, the IEclipseContext calls compute(context, key) and returns the result of the computation. Context sanctions must subclass org.eclipse.e4.core.contexts.ContextFunction.

For example, the Eclipse 4 Workbench makes the current selection available via a context function:

appContext.set(SELECTION, new ContextFunction() {
    @Override
    public Object compute(IEclipseContext context, String contextKey) {
        IEclipseContext parent = context.getParent();
        while (parent != null) {
            context = parent;
            parent = context.getParent();
        }
        return context.getActiveLeaf().get("out.selection");
    }
});

The result of a context function are memoized: they are only recomputed when another referenced value is changed. See the section on Run And Tracks below.


Run And Tracks

RunAndTracks, affectionally called RATs, are a special form of a Runnable. RATs are executed within a context, and the context tracks all of the values accessed. When any of these values are changed, the runnable is automatically re-evaluated. The following example will print 20.9895 and then 20.12993:

final IEclipseContext context = EclipseContextFactory.create();
context.set("price", 19.99);
context.set("tax", 0.05);
context.runAndTrack(new RunAndTrack() {
    @Override
    public boolean changed(IEclipseContext context) {
        total = (Double) context.get("price") * (1.0 + (Double) context.get("tax"));
        return true;
    }
 
    @Override
    public String toString() {
        return "calculator";
    }
});
print(total);
context.set("tax", 0.07);
print(total);

A RAT continues executing until either its context is disposed, or its changed() method returns false.

Note that RATs are only re-evaluated when the value is changed (i.e., IEclipseContext#set() or #modify() are called), and not when the contents of the value are changed.

Exposing Services and State on an Eclipse Context

Values are normally addd to an Eclipse Context via IEclipseContext#set(key,value) or #modify(key,value). But these require knowing and being able to find the context to be modified. But developers sometimes need to be able to add values on-the-fly. There are a few techniques.

Context Functions

A Context Function is provided both the key that was requested and the source context, where the retrieval began. The context function can return an instance created for that particular context, or set a value in that context — or elsewhere. This approach is very useful for computing results based on the active part (IEclipseContext#getActiveLeaf()).

OSGi Services

The Eclipse 4 workbench roots its context hierarchy from an EclipseContextOSGi, a special Eclipse Context that knows to look up keys in the OSGi Service Registry. EclipseContextOSGi instances are obtained via EclipseContextFactory#getServiceContext(BundleContext). These contexts — and the services requested — are bounded by the lifecycle of the provided bundle.


Context Functions Exposed As OSGi Declarative Services

This approach exposes a context function as the implementation of a service defined OSGi Declarative Services. This pattern is used for creating the IEventBroker, using the new DS annotations support.

@Component(service = IContextFunction.class, property = "service.context.key=org.eclipse.e4.core.services.events.IEventBroker")
public class EventBrokerFactory extends ContextFunction {
    @Override
    public Object compute(IEclipseContext context, String contextKey) {
        EventBroker broker = context.getLocal(EventBroker.class);
        if (broker == null) {
            broker = ContextInjectionFactory.make(EventBroker.class, context);
            context.set(EventBroker.class, broker);
        }
        return broker;
    }
}

Note that the service is actually exposed as an IContextFunction, not an IEventBroker. This approach is specific to being used for values retrieved from an IEclipseContext.

Creating New Contexts

Contexts can be created either as a leaf of another context (see IEclipseContext#newChild()) or as a new root (see EclipseContextFactory#create()). A special EclipseContext implementation exists (EclipseContextOSGi, obtained by EclipseContextFactory#getServiceContext()) to expose OSGi Services too.


Advanced Topics

How do I access the current context?

Current really depends on the requesting context. An MPart or IViewPart rarely wants the active part, which may not be itself, but a particular part, such as the active editor.

IServiceLocator, either implemented by or provided by many components in the Eclipse Workbench, was the primary means to obtain services in the Eclipse Workbench. It is now backed by an IEclipseContext. You can either fetch values directly via IServiceLocator#getService(key) or obtain the IServiceLocators IEclipseContext directly (IServiceLocator.getService(IEclipseContext.class)). Most UI containers implement IServiceLocator like IWorkbench, IWorkbenchWindow, IWorkbenchPart.


@Active vs ACTIVE_*

@Active is an annotation that causes our DI to look up a value from the source context's active leaf, where the source context is the context that was used for injecting that object.

ACTIVE_PART, on the other hand, looks for the active leaf as constrained by the source context's window's context.

public class ActivePartLookupFunction extends ContextFunction {
    @Override
    public Object compute(IEclipseContext context, String contextKey) {
        MContext window = context.get(MWindow.class);
        if (window == null) {
            window = context.get(MApplication.class);
            if (window == null) {
                return null;
            }
        }
        IEclipseContext current = window.getContext();
        if (current == null) {
            return null;
        }
        return current.getActiveLeaf().get(MPart.class);
    }
}

ACTIVE_SHELL is (needs some work)

The moral: the implementation of active X is not necessarily as straightforward as might appear.

References

The old E4 wiki pages provides background [E4/Contexts|on the influences on IEclipseContext].