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 09:26, 12 January 2009 by Pwebster.ca.ibm.com (Talk | contribs) (ILocator Prototype)

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(HS + 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.

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)

Back to the top