Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

E4/Contexts

< E4
Revision as of 19:17, 17 March 2010 by Remysuen.ca.ibm.com (Talk | contribs) (OSGi Services)

Summary

In the general, execution or evaluation contexts, that can provide information to the currently executing code. This document discusses the general problem, provides a survey of currently available technology in Eclipse, and establishes requirements for a common solution.

Problems

The following are some problems present in Eclipse 3.x that we are seeking to address:

  1. We have a large number of global singleton accessors (see Platform, PlatformUI, etc)
    • This tightly couples consumers of "things" to their producer/provider, inhibits reuse, etc
  2. Mechanisms are not very dynamic - react to context changes drastically rather than incrementally (just close the affected controls)
  3. IEvaluationContext has global state that gets swapped according to context change (such as the focus control)
  4. Current solutions are not multi-threadable - they assume evaluation occurs in the UI thread
  5. Scaling granularity
    • Current solutions don't scale down to services with brief lifetimes
    • Current solutions may not scale up to very large numbers of services
  6. There are too many parallel trees that mimic each other (widget tree, service locator tree, etc)
  7. Currently don't track service consumers - to notify when services come/go
  8. We expose containerisms to client code - they need to know where things come from
  9. No support for looking up services that are composed out of other services on the fly

Overview of current and suggested context mechanisms

IEquinoxContext (e4)

Bug 259423 Add notion of "context" that can be injected into objects

org.eclipse.core.runtime.RegistryContextFactory - Creates objects based on the registry's configuration element. Creates a user-specified class and injects into it contents of the configuration element and, optionally, the supplied context

org.eclipse.equinox.context.IEquinoxContext - This interface represents a hierarchical context with dependency injection capabilities. The context might have number of objects associated with it. For instance, a context might include log, status, and preferences. We'll call such associated objects "services" (through they don't have to be OSGi services).

An example use (from tests):

Integer testInt = new Integer(123);
String testString = new String("abc");
 
// create context
IEquinoxContext context = ContextFactory.createContext();
// elements to be populated via fields
context.addObject("busyCursorTimeout", testInt);
context.addObject("username", testString); // this checks capitalization as well
 
ObjectBasic userObject = new ObjectBasic();
context.injectInto(userObject);

IServiceLocator (3.x)

In Eclipse 3.x we have the notion of part hierarchy: Workbench, WorkbenchWindow, WorkbenchPart (Editor/View), and nested part (MultiPageEditorPart or PageBookView).

Bug 92646 comment #11 [Workbench] [RCP] Allow developers to register Workbench services

There is a description of our services/service locator hierarchy in comment #11, basically we use this model to support 3 things:

  • A service behaviour accessed through a local context (not PlatformUI :-)
  • Scoping of the service to the local context
  • Local service lifecycle, allowing the de-activation of contributions and listener clean up
IFooService fooService = (IFooService) getSite().getService(IFooService.class);
fooService.activateFoo("org.eclipse.ui.textScope");

Each service is hierarchical and mirrors the IServiceLocator hierarchy. Services are created lazily on the call to getService(Class). The lookup algorithm is:

  1. Check our cache to see if we already have an instance of the service
  2. Check our local org.eclipse.ui.services.AbstractServiceFactory to see if we can create a local override version
  3. Go to the org.eclipse.ui.services registry, org.eclipse.ui.internal.services.WorkbenchServiceRegistry to see if we can create the service
  4. Use the parent locator to try and look up the service

When creating a service, you have access to:

Object create(Class serviceInterface, IServiceLocator parentLocator, IServiceLocator locator);
  • serviceInterface: the service we need to create
  • parentLocator: A locator that can return a parent service instance if desired
  • locator: the service locator which can be used to retrieve dependent services

When we create our service locators using org.eclipse.ui.internal.services.IServiceLocatorCreator we pass in an owner (an IDisposable). If we are using services that go away (for example, the plugin is unloaded) we will call dispose() on the owner. For example, creating a service locator for a workbench window looks like:

serviceLocator = (ServiceLocator) slc
	.createServiceLocator(workbench, null, new IDisposable(){
		public void dispose() {
			final Shell shell = getShell();
			if (shell != null && !shell.isDisposed()) {
				close();
			}
		}
	});

IEvaluationContext (3.x)

Provided by org.eclipse.core.expressions this is the application context used in 3.x by the IEvaluationService (and hence all declarative expressions like enabledWhen/activeWhen/visibleWhen)

EvaluationContext is hierarchical and has a lookup strategy that checks the local cache and then asks the parent. In 3.x we create the root org.eclipse.core.expressions.EvaluationContext in our EvaluationAuthority(ExpressionAuthority) and populate it from the ISourceProviders, mostly contributed through org.eclipse.ui.services. In 3.x, the context hierarchy does not match the part hierarchy, it is basically flat. This supports the global application context used by expressions and the command framework, but prevents the notion of local context.

String hi = "hi";
EvaluationContext context = new EvaluationContext(null, hi);
context.addVariable("selection", hi);
//... not exactly a stellar example :-)
String selection = (String)context.getVariable("selection");

In e4

In E4 we're investigating a different part of the IEvaluationContext API, resolveVariable(*).

Object resolveVariable(String name, Object[] args);

Each EvaluationContext in the hierarchy can be provided one or more org.eclipse.core.expressions.IVariableResolvers. That allows the EvaluationContext to delegate the lookup. The algorithm is to iterate through the resolvers and return the first non-null answer, and if nothing is found delegate the resolve query to the parent.

In the e4 command investigation, Bug 257429 Command investigation phase 1, each workbench model Part<?> has an associated context. The IServiceLocator behaviour could be implemented by resolving a service variable "org.eclipse.e4.service" with the service as an argument.

IHandlerService hs = (IHandlerService) l.resolveVariable(IServiceLocator.SERVICE, new Object[] { IHandlerService.class });

The actual order of lookup for the context to execute a menu item is:

  1. Find the focus control
  2. Walk up the SWT parent hierarchy until we find a control that is owned by a Part<?> like a view or workbench window
  3. Go to the application and look up the IEvaluationContext that was created with that Part<?>

With this mapping, the IEvaluationContext and IServiceLocator hierarchy are identical, and extending this to support different kinds of lookups for different variables means adding more IVariableResolvers.

ILocator Prototype

This prototype was a generalization of the org.eclipse.ui.services.IServiceLocator and the org.eclipse.core.expressions.IEvaluationContext. As with the standard IServiceLocator, using org.eclipse.e4.locator.ILocator avoids having to deal with any global singletons from the client's point of view. But in the framework, getting a service or variable is separated into the tree datastructure that supports local data at each node, and pluggable strategies that determine how each piece of data is set or retrieved.

The prototype ILocator supports:

  • Object getService(Class serviceInterface)
  • Object getVariable(String name)
  • void addVariableChangeListener/removeVariableChangeListener(PropertyChangeListener listener)

The Locator gets a variable by delegating to the LookupStrategy:

LookupStrategy strategy = getLookupStrategy(name);
return strategy.lookup(this, name);
// a strategy implements Object lookup(IUpdateableLocator context, String name)

An IUpdateableLocator supports the extra methods needed to walk the tree structure and get local data, for example:

  • IUpdateableLocator getParent()
  • IUpdateableLocator[] getChildren()
  • IUpdateableLocator getActiveChild()
  • Object getLocalVariable(String name)
  • void setLocalVariable(String name, Object value)
  • fireLocalVariableChange(String name, Object oldValue, Object newValue)

Usecase 1 - lookup the IHandlerService

The client code is the same as in 3.x, IHandlerService hs = (IHandlerService) locator.getService(IHandlerService.class) and accesses the lookup strategy for the handler service.

public Object lookup(IUpdateableLocator context, String name) {
	if (!name.equals(IHandlerService.class.getName())) {
		return null;
	}
	Object o = context.getLocalVariable(name);
	if (o == null) {
		o = new HandlerService(context);
		context.setLocalVariable(name, o);
	}
	return o;
}

Usecase 2 - IHandlerService uses the locator

The HandlerService uses the local storage to store active handlers for that context. This is currently implemented with a handler service chain, although would not need to be if the context to be used was passed in as part of the request:

public Object activateHandler(String commandId, IHandler handler) {
	locator.setVariable(PREFIX + commandId, handler);
	return commandId;
}


Executing a command is simply looking up the handler (with more checks of course):

Object o = locator.getVariable(PREFIX + commandId);
IHandler handler = (IHandler) o;
return handler.execute(parameters);

Since the handlers are stored in the locator hierarchy they will be looked up using the default lookup strategy, which is org.eclipse.e4.locator.ActiveChainLookupStrategy. The algorithm is to go to the bottom of the active child chain, then walk up the parent chain until the first value can be returned.

public Object lookup(IUpdateableLocator context, String name) {
	IUpdateableLocator lookupContext = context;
	while (lookupContext.getActiveChild() != null) {
		lookupContext = lookupContext.getActiveChild();
	}
	Object result = null;
	while (lookupContext != null && result == null) {
		result = lookupContext.getLocalVariable(name);
		if (result == null) {
			lookupContext = lookupContext.getParent();
		}
	}
	return result;
}

The algorithm returns the correct answer no matter where the question is asked.

  1. When executing in the active editor, it will return the active editor handler for that command
  2. When executing in the global context (like a keybinding), it will go down to the active editor and return the active editor handler
  3. When executing in a view that is not currently active (either because of cheatsheets or macros, or because an event need to trigger the command in that view), it will correctly return the view handler for that command.

Usecase 3 - active contexts

I have a sample IContextService the follows the same pattern used by IHandlerService. But in our hierarchy of parts, it makes sense that each part sets the context IDs it cares about without worrying about what other parts or levels have done. Setting an active context ID involves adding it to the local datastore and firing correct property change notifications. To get the active context IDs, find the active context chain and sum all of the IDs store in the local datastore.

public Object lookup(IUpdateableLocator context, String name) {
	IUpdateableLocator lookupContext = context;
	while (lookupContext.getActiveChild() != null) {
		lookupContext = lookupContext.getActiveChild();
	}
	HashSet result = new HashSet();
	Set set = null;
	while (lookupContext != null) {
		set = (Set) lookupContext
				.getLocalVariable(ContextService.LOCAL_CONTEXTS);
		if (set != null) {
			result.addAll(set);
		}
		lookupContext = lookupContext.getParent();
	}
	return result;
}

Contexts with ComputedValue

Similar to the other designs, this is a "tree of local maps". A prototype is available in org.eclipse.e4.core.services. At any given Context, you can set a variable to a value or to a ComputedValue (function).

final UIContext mainContext = new UIContext(null, "globalContext");
mainContext.set(IWorkbench.class.getName(), this);
mainContext.set(IExceptionHandler.class.getName(), exceptionHandler);
mainContext.set(ResourceUtility.class.getName(), resourceUtility);
// read in from an extension point:
mainContext.set("org.eclipse.e4.core.services.IBackgroundRunner", new BackgroundRunnerValue());
mainContext.set("selection", new ActiveChildValue("selection");

Any request for a lookup will

  1. look in the local cache
  2. if their is a local value
    1. if it is an object, return it
    2. if it is a ComputedValue, return the computation
  3. if there is a parent, return parent.internalGet(orignalContext, name) (use it as a prototype)
  4. return null

An example of the ComputedValue used for a service is BackgroundRunnerValue:

protected Object compute(Context context) {
	return new IBackgroundRunner() {
		public void schedule(long delay, String name,
				final IRunnableWithProgress runnable) {
			new Job(name) {
				protected IStatus run(IProgressMonitor monitor) {
					return runnable.run(monitor);
				}
			}.schedule(delay);
		}
	};
}

ComputedValue can also be used to implement the active context chain lookup strategy. It would follow any activeChild variable (in theory, maintained by the workbench) to the bottom of the chain, and then follow the standard lookup procedure (up the parent chain).

protected Object compute(Context context) {
	if (context.isSet("activeChild")) {
		Context childContext = (Context) context.get("activeChild");
		return childContext.get(attr);
	}
	return null;
}

The prototype supports the notion of listeners that track changes in a variable. Listening for selection changes would look something like:

mainContext.runAndTrack(new Runnable() {
	public void run() {
		Object selection = mainContext.get("selection");
		text.setText("" + selection);
	}
}, "accessing selection from " + name);


Q: How would we support something like active contexts?

  1. It seems we would use one variable to store the local IDs at each Context level.
  2. A ComputedValue for activeContextIds could then sum them up.
  3. Would we still use an IContextService to manage and/or simplify the interaction between activating a context and retrieving the activeContextIds?

Discussed in 20090115 meeting

  • Services are still useful. They can manage the co-ordination of setting or looking up data above and beyond what context.set(name, value) does. They can also arbitrate in more complex scenarios (i.e. how do we deal with 2 active handlers at the Part level? How do we let the user activate 2 handlers at the part level).
  • Who knows what?
    • Context <--> Part<WorkbenchWindow> <--> Widget (Shell)
    • UserObject (view) <-- Context <--> Part<ContributedPart> <--> Widget (Composite)
  • For the example usecase like the IContextService, should the context IDs used be String values in a variable that is a Set, or should they each be their own variable, categorized in some way?
    • As their own variable, it becomes easier to listen for that specific context (as opposed to being told the context changed, so look it up).
    • What does categorization of variables mean? Is that added value provided by the IContextService, or is that inherent in the Context?
  • Q: If we have a workbench model, should contexts contain the active lookup elements (ComputedValue) and use the EMF model for local storage instead of an internal map?
    • (not of interest for M1 or possibly even M2)
    • Should we restrict the workbench model to workbench/layout/rendering information?
    • Should we allow contributors to "graft" their model onto the workbench model?
    • Should we provide an IModelService (from a Context) that contributors could publish their model to (and then supply information through ComputedValue)

General Requirements

The following are some very general requirements of a context system:

  1. Support notion of service availability lifecycle. The service publisher can add or withdraw services at any time
  2. Support notion of service usage lifecycle. Some services may want to perform cleanup when clients are no longer using the service
  3. Eliminate global singletons
  4. Separation of usage from knowledge of environment to increase reuse (inversion of control)
  5. Lookup of both simple variable values and "live" service objects
  6. Flexible algorithm for service lookup - how a service is retrieved is completely in the hands of the local context, or perhaps some pluggable strategy inserted into the local context

Interesting Use Cases

The following are some interesting concrete use cases that came up during discussion

  1. Font lookup. Some composite needs to know what font to use. How the font is chosen depends on the context for that particular instance of the composite. Embedding the composite in a dialog would result in one font, and embedding the identical control into a view would result in another.
  2. Printing. The workbench window has a 'print' button. the print button lives in some higher context (the window), but needs to access a more specific context (the active part). When the button is clicked it should:
    • If the active part has a selection, print the selection
    • If the active part is an editor with no selection, print the document
    • If the active part is of another kind, print the contents of the part
    • If there is no active part do nothing
  3. Colors and fonts preference page. This preference page shows a 'preview' which is an embedded view or editor. That view or editor should be the 'real' view or editor rather than a fake one. When that part is embedded in the preference page, it should retrieve preference values from its local context (the preferences that the user has set but not yet applied). When the same editor control appears outside the preference page, it should obtain preference values directly from the preference service.
  4. Cheat sheet / macro recorder playback. Commands execute in a local context (selection, focus control, etc). When the user clicks in a cheat sheet or macro playback, the 'real' focus control and selection are in the cheat sheet or macro view. The command should execute as if the current selection or focus control is the object on which the command is running. (Even apparently global things like 'focus control' are in fact also relative to a context - there should be no single global notion of active context)

Available Services

As it is not possible to ask the context for available keys and because it is the hope that clients do not directly call methods on the context, it is important for clients to be able to know what they can actually inject.

Model

Model objects that are themselves MContext instances will be included in their own context.

For example, MWindows and MParts are MContexts, so you can query their context for the model object.

// window == mwindow
MWindow window = (MWindow) mwindow.getContext().(MWindow.class.getName());
// part == mpart
MWindow part = (MPart) mpart.getContext().(MPart.class.getName());

Because these model objects are available in the context, you can just have them be injected into the client code directly.

public class AccountsPart {
  @Inject
  private MPart part;
 
  @Inject
  private MWindow window;
 
  void setDirty(boolean dirty) {
    part.setDirty(dirty);
  }
}

To encourage reuse, you should inject the lowest common denominator that you need. For example, if you only care about marking the part dirty, then you can just do...

public class AccountsPart {
  @Inject
  private MDirtyable dirtyable;
 
  void setDirty(boolean dirty) {
    dirtyable.setDirty(dirty);
  }
}

...which allows your code to be reused by other MDirtyable implementations that might not actually be MParts. This leads into an interesting point, which is that a model interface's superinterfaces are also added into the context. In the following code...

public class AccountsPart {
  @Inject
  private MDirtyable dirtyable;
 
  @Inject
  private MUILabel label;
 
  @Inject
  private MContext context;
 
  @Inject
  private MPart part;
}

...all the fields are actually the same MPart instance!

Services

Essentially, anything starting with that goes E*Service should be in the context, some examples are:

  • org.eclipse.e4.core.commands.ECommandService
  • org.eclipse.e4.core.commands.EHandlerService
  • org.eclipse.e4.workbench.modeling.EPartService
  • org.eclipse.e4.workbench.modeling.EModelService
  • org.eclipse.e4.workbench.modeling.ESelectionService
  • org.eclipse.e4.ui.bindings.EBindingService
  • org.eclipse.e4.ui.services.EContextService

There are also some other things in there such as, but not limited to, the following:

  • org.eclipse.e4.workbench.ui.IPresentationEngine
  • org.eclipse.jface.window.IShellProvider

As always, you can just prefix your object's fields with the @Inject annotation and then they will be injected when the object is constructed.

public class AccountsPart {
  @Inject
  private EPartService partService;
}

OSGi Services

By default, the context lookup will query for OSGi services if a request cannot be satisfied in the context chain. Hence, you can also ask for OSGi services like LogService or PackageAdmin.

public class AccountsPart {
  @Inject
  private LogService logService;
 
  @Inject
  private PackageAdmin packageAdmin;
}

Constants

There are some constants defined in org.eclipse.e4.ui.services.IServiceConstants that are available for consumption. The most commonly used ones will probably be ACTIVE_PART and ACTIVE_SHELL. Though ACTIVE_SHELL will likely be preceded by IShellProvider. To use a constant, you would need the @Named annotation.

public class AccountsPart {
  @Inject
  @Named(IServiceConstants.ACTIVE_PART)
  private MPart part;
}

Optional Requirements

If you do not necessarily need a service satisfied, you just need to flag it with the @Optiona annotation and it will be left as null if it cannot be satisfied by the context.

public class AccountsPart {
  @Inject
  @Optional
  private EModelService modelService;
}

Back to the top