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:
- http://git.eclipse.org/c/efxclipse/org.eclipse.efxclipse.git/tree/bundles/runtime/org.eclipse.fx.core.di
- http://git.eclipse.org/c/efxclipse/org.eclipse.efxclipse.git/tree/bundles/runtime/org.eclipse.fx.core.di.context
- http://git.eclipse.org/c/efxclipse/org.eclipse.efxclipse.git/tree/bundles/runtime/org.eclipse.fx.core/src/org/eclipse/fx/core/adapter
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
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é
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!
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?
I’m glad you asked 😉 – see my other blog post from today but in short you do it through the MContext#variables
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.
Pingback: What IEclipseContext#declareModifiable is good for | Tomsondev Blog
Pingback: e(fx)clipse 0.9.0 released | Tomsondev Blog