Blog

Writing IEclipseContext-less code

The problem

The IEclipseContext is the center of the the e4 DI system. One can think of it as observable hierarchical key-value map. One of the major idea of e4 was that people should write code which does not depend on framework classes but use DI to get the values they need no matter if they need an OSGi service, an application service, a local service, a value published inside the IEclipseContext and it works out great.

@Inject
MyService service;

@Inject
public void setSelection(Person p) {
   // ....
}

The opposite does not work yet though. One is NOT able to e.g. publish the selected person from a TableViewer easily into the system but has to generate a compile time dependency on the IEclipseContext so all your nice framework agnostic code is spoiled with the IEclipseContext dependency! The code often looks like this:

@Inject
IEclipseContext context;

private void myUI() {
   TableViewer v = ...
   v.addSelectionChangedListener(.... { ... context.set(Person.class,p) });
}

This is a reoccuring pattern I see in more and more e4 codebases who violate the programming model principles we set up.

The solution

The solution is simple. Instead of injecting IEclipseContext we inject a structure which is bound directly to a slot in the IEclipseContext:

@Inject
@ContextValue(contextKey="my.Person")
ContextBoundValue<Person> value;

private void myUI() {
   TableViewer v = ...
   v.addSelectionChangedListener(.... { ... value.publish(p); });
}

This code is slightly better because the ContextBoundValue is much smaller than the IEclipseContext and encapsulates the desired behavior. We can get even better! You are using Eclipse-Databinding

@Inject
@ContextValue(contextKey="my.Person")
IObservableValue value;

private void myUI() {
   DatabindingContext dbc = ...;
   TableViewer v = ...
   dbc.bindValue(ViewerProperties.singleSelection().observe(v),value);
}

Or you prefer JavaFX-Properties?

@Inject
@ContextValue(contextKey="my.Person")
Property<Object> value;

Or you prefer ...? The system is completely open to anything you want the injected value to be because it allows you to register adapters (through OSGi-Services) to transform the injected value to anything in the universe. BTW this makes another reoccuring code pattern obsolete when using Eclipse Databinding in e4 apps where I have not only once written code like this:

WritableValue master = new WritableValue();

// ...
IValueProperty p = ...;
IObservableValue domainObs = p.observeDetail(master);

@Inject
public void setPerson(@Optional Person p) {
  master.set(p);
}

With the above support you can now write:

@Inject
@ContextValue(contextKey="my.Person")
IObservableValue master;

// ...
IValueProperty p = ...;
IObservableValue domainObs = p.observeDetail(master);

How does it work?

It's a lot less sophisticated then you might think it is. The heart of it is the ExtendedObjectSupplier who deals with the @ContextValue annotation.

public class ContextBoundValueSupplier extends ExtendedObjectSupplier {
  @Inject
  AdapterService adapterService;
	
  @Override
  public Object get(IObjectDescriptor descriptor, IRequestor requestor, boolean track, boolean group) {
    ContextValue qualifier = descriptor.getQualifier(ContextValue.class);
    Requestor r = (Requestor) requestor;
    EclipseContextBoundValue<?> c = r.getInjector().make(EclipseContextBoundValue.class, r.getPrimarySupplier());
    c.setContextKey(qualifier.contextKey());
		
    Class<?> desiredClass = getDesiredClass(descriptor.getDesiredType());
		
    if( desiredClass == ContextBoundValue.class) {
      return c;
    } else {
      return c.adaptTo(desiredClass);
    }
 }

  private Class<?> getDesiredClass(Type desiredType) {
    if (desiredType instanceof Class<?>)
      return (Class<?>) desiredType;
    if (desiredType instanceof ParameterizedType) {
      Type rawType = ((ParameterizedType) desiredType).getRawType();
    if (rawType instanceof Class<?>)
      return (Class<?>) rawType;
    }
    return null;
  }
}

and the context bound value implementation:

public class EclipseContextBoundValue<T> implements ContextBoundValue<T> {
	private IEclipseContext context;
	private String contextKey;
	private List<Callback<T>> callbacks;
	private List<Callback<Void>> disposalCallbacks;
	private AdapterService adapterService;
	private T value;
	
	@Inject
	public EclipseContextBoundValue(IEclipseContext context, AdapterService adapterService) {
		this.context = context;
		this.adapterService = adapterService;
	}
	
	public void setContextKey(final String contextKey) {
		this.contextKey = contextKey;
		this.context.runAndTrack(new RunAndTrack() {
			
			@SuppressWarnings("unchecked")
			@Override
			public boolean changed(IEclipseContext context) {
				setCurrentValue((T) context.get(contextKey));
				return true;
			}
		});
	}
	
	@SuppressWarnings("unchecked")
	private void setCurrentValue(T o) {
		this.value = o;
		if( callbacks != null ) {
			for( Callback<?> c : callbacks.toArray(new Callback<?>[0]) ) {
				((Callback<T>)c).call(o);
			}
		}
	}
	
	@Override
	public T getValue() {
		return value;
	}
	
	@Override
	public void publish(T value) {
		context.modify(contextKey, value);
	}

	@Override
	public Subscription subscribeOnValueChange(final Callback<T> callback) {
		if( callbacks == null ) {
			callbacks = new ArrayList<Callback<T>>();
		}
		callbacks.add(callback);
		return new Subscription() {
			
			@Override
			public void dispose() {
				callbacks.remove(callback);
			}
		};
	}
	
	@Override
	public Subscription subscribeOnDispose(final Callback<Void> callback) {
		if( disposalCallbacks == null ) {
			disposalCallbacks = new ArrayList<Callback<Void>>();
		}
		disposalCallbacks.add(callback);
		return new Subscription() {
			
			@Override
			public void dispose() {
				disposalCallbacks.remove(callback);
			}
		};
	}

	@Override
	public <A> A adaptTo(Class<A> adapt) {
		return adapterService.adapt(this, adapt, new ValueAccessImpl(context));
	}

	@Override
	public boolean canAdaptTo(Class<?> adapt) {
		return adapterService.canAdapt(this, adapt);
	}

	@PreDestroy
	void dispose() {
		if( disposalCallbacks != null ) {
			for(Callback<?> callback : disposalCallbacks.toArray(new Callback<?>[0])) {
				callback.call(null);
			}
			disposalCallbacks.clear();
		}
		if( callbacks != null ) {
			callbacks.clear();
		}
		value = null;
	}
	
	static class ValueAccessImpl implements ValueAccess {
		private final IEclipseContext context;
		
		public ValueAccessImpl(IEclipseContext context) {
			this.context = context;
		}
		
		@SuppressWarnings("unchecked")
		@Override
		public <O> O getValue(String key) {
			return (O) context.get(key);
		}

		@Override
		public <O> O getValue(Class<O> key) {
			return context.get(key);
		}
		
	}
}

If you want to see the full source code you need to look into:

You can find samples in the JUnit-Test suite at http://git.eclipse.org/c/efxclipse/org.eclipse.efxclipse.git/tree/bundles/runtime/org.eclipse.fx.core.di.context.tests/src/org/eclipse/fx/core/di/context/tests/ContextBoundValueTestCase.java

published on
Previous
What IEclipseContext#declareModifiable is good for