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

Advertisement
This entry was posted in e(fx)clipse, e4. Bookmark the permalink.

7 Responses to Writing IEclipseContext-less code

  1. René says:

    Hi,

    nice idea, but what about the hierarchy if I have two views side-by-side? Because as far as I can see the ContextValue is only published into the context of the active view how would the other one recognize the change? For this I would still need the Selection-Service, isn’t it?

    Regards
    René

    • Tom Schindl says:

      This works with the help of IEclipseContext#declareModifiable()! Look at the test suite I have a unit test for it. I also thought about providing an publish(T value, String contextKey) but I think declareModifiable is the better way.

      I guess declareModifiable is not very well known in the e4 universe, but it frees you from searching for the target-context the modification should get in! I guess I’ll write another blog post!

      • René says:

        I know a little bit about the declareModfiable() it allows me to modify the variable in the parent context, but who has defined the variable in the parent context?

      • Tom Schindl says:

        I’m glad you asked 😉 – see my other blog post from today but in short you do it through the MContext#variables

      • René says:

        Sorry, I found the solution in the “https://tomsondev.bestsolution.at/2013/11/21/what-ieclipsecontextdeclaremodifiable-is-good-for/” blog you wrote a couple of minute ago.

        Thanks, now it’s really cool.

  2. Pingback: What IEclipseContext#declareModifiable is good for | Tomsondev Blog

  3. Pingback: e(fx)clipse 0.9.0 released | Tomsondev Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.