Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Efxclipse/Runtime/Recipes
This page holds best practice recipes when writing JavaFX application using e(fx)clipse
Contents
Service retrieval
Retrieving services from the OSGi-Registry is sometimes a bit cumbersome so org.eclipse.fx.core.Util
provides helpers to look up services with the following APIs
-
public static <S> S lookupService(Class<?> requestor, Class<S> serviceClass)
-
public static <S> S lookupService(Class<S> serviceClass)
-
public static <S> List<S> lookupServiceList(Class<?> requestor,Class<S> serviceClass)
-
public static <S> List<S> lookupServiceList(Class<S> serviceClass)
An added benefit of this utilities is that they work also outside OSGi by using ServiceLoader
instead to ensure a specific sort order services can implement the optional org.eclipse.fx.core.RankedService
.
Logging
e(fx)clipse has its own logging facade org.eclipse.fx.core.log.Logger
which allows to plug-in different log frameworks.
Currently available are:
-
java.util.Logging
(default) - log4j by adding
org.eclipse.fx.core.log4j
bundle to your OSGi-Runtime
Usage
There are different ways to use get a logger.
LoggerCreator
If you are running on OSGi or in a simple java application. You can retrieve a logger using org.eclipse.fx.core.log.LoggerCreator
import org.eclipse.fx.core.log.Logger; import org.eclipse.fx.core.log.LoggerCreator; public class MyClass { private static Logger LOGGER = LoggerCreator.createLogger(MyClass.class); // .... }
In an OSGi-Environment the OSGi-Service registry is searched to find a LoggerFactory
and in none OSGi-Applications the ServiceLoader
is consulted.
LoggerFactory Service
The different logger bundles contribute their LoggerFactory
implementation into the OSGi-Registry. In case you are e.g. contributing a service via DS you can get simple add a service reference and you'll get the LoggerFactory
with the highest rank injected.
import org.eclipse.fx.core.log.LoggerFactory; import org.eclipse.fx.core.log.Logger; public class MyServiceImpl implements MyService { private Logger logger; public void setLoggerFactory(LoggerFactory factory) { this.logger = factory.createLogger(MyService.class.getName()); } public void unsetLoggerFactory(LoggerFactory factory) { } }
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="myservice"> <implementation class="impl.MyServiceImpl"/> <service> <provide interface="service.MyService"/> </service> <reference bind="setLoggerFactory" cardinality="1..1" interface="org.eclipse.fx.core.log.LoggerFactory" name="LoggerFactory" policy="static" unbind="unsetLoggerFactory"/> </scr:component>
Eclipse DI & @Log annotation
If you make use of Eclipse DI in your code you can get a Logger
instance injected with:
import org.eclipse.fx.core.log.Log; import org.eclipse.fx.core.log.Logger; import javax.inject.Inject; public class MyDIComponent { @Inject @Log Logger logger; }
Google Guice & @Log annotation
If you use Guice as the DI container you use the org.eclipse.fx.core.guice
bundle to get a Logger injected in your component with:
import org.eclipse.fx.core.log.Log; import org.eclipse.fx.core.log.Logger; import javax.inject.Inject; public class MyDIComponent { @Log Logger logger; }
if you have configured your Guice-Module with:
import com.google.inject.Module; import com.google.inject.Binder; import org.eclipse.fx.core.log.LoggerFactory; import org.eclipse.fx.core.log4j.Log4JLoggerFactory; import org.eclipse.fx.core.guice.FXLoggerListener; public class MyModule implements Module { public void configure(Binder binder) { binder.bind(LoggerFactory.class).toProvider(Log4JLoggerFactory.class); // or JUtilLoggerFactory binder.bindListener(Matchers.any(), new FXLoggerListener()); } }
Instead of directly binding to a logger factory you can delegate to the OSGi-Service registry by using OSGiLoggerFactoryProvider
:
import com.google.inject.Module; import com.google.inject.Binder; import org.eclipse.fx.core.log.LoggerFactory; import org.eclipse.fx.core.guice.FXLoggerListener; import org.eclipse.fx.core.guice.OSGiLoggerFactoryProvider; public class MyModule implements Module { public void configure(Binder binder) { binder.bind(LoggerFactory.class).toProvider(OSGiLoggerFactoryProvider.class); binder.bindListener(Matchers.any(), new FXLoggerListener()); } }
Extending
Like outlined above there are 2 logger implementations available from the e(fx)clipse p2 repository. If you want to use a different logging framework you are able to plug-in your own by implementing LoggerFactory
and contributing it to the OSGi-Service-Registry.
import javax.inject.Provider; import org.eclipse.fx.core.log.LoggerFactory; import org.eclipse.fx.core.log.Logger; public class MyLoggerFactory implements LoggerFactory, Provider<LoggerFactory> { @Override public LoggerFactory get() { return this; } @Override public Logger createLogger(String name) { return new LoggerImpl(name); } static class LoggerImpl implements Logger { private String name; public LoggerImpl(String name) { this.name = name; } // .... } }
You contribute it to the OSGi-Service registry e.g. by using DS. You should give the service a higher ranking than 1 (which is the ranking of the log4j service) to ensure it is picked when a logger is requested.
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="my.logger.framework.factory"> <implementation class="my.logger.framework.MyLoggerFactory"/> <property name="service.ranking" type="Integer" value="2"/> <service> <provide interface="org.eclipse.fx.core.log.LoggerFactory"/> </service> </scr:component>
Eclipse DI
Publishing to the IEclipseContext
The IEclipseContext
is the central component of the Eclipse DI container. Retrieving values from it is as easy as writing @Inject
in your java class and the DI container will fill it with a value and keep it up-to-date if you used field or method injection.
The opposite - publishing a value into the context - is not as easy because your java component will get a dependency on the DI-Container because it needs to access the IEclipseContext
directly. e(fx)clipse provides you the possibility to get around this architectual problem by defining an annotation named @ContextValue
which marks a slot in IEclipseContext
instance which can be used to observe the value and modified.
import org.eclipse.fx.core.di.ContextBoundValue; import org.eclipse.fx.core.di.ContextValue; import javax.inject.Inject; public static class SimpleInject { @Inject @ContextValue(contextKey="user") public ContextBoundValue<String> value; private void updateValue() { value.publish("tomschindl"); } }
To make use of this your bundle needs to have a dependency on org.eclipse.fx.core.di
and your runtime has to include org.eclipse.fx.core.di.context
.
Injected value as an observable
A reoccuring pattern when developing with Eclipse Databinding and Dependency Injection is that the injected value is used as the master in master-detail binding scenario.
import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.fx.core.di.ContextBoundValue; import org.eclipse.fx.core.di.ContextValue; public class DetailView { private IObservableValue master = new WritableValue(); @Inject public void update(Person p) { master.getRealm().exec(new Runnable() { public void run() { master.setValue(p); } }); } }
The @ContextValue
framework introduce in the recipe above is able to reduce the code to
import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.fx.core.di.ContextBoundValue; import org.eclipse.fx.core.di.ContextValue; public class DetailView { @Inject @ContextValue("my.domain.Person") IObservableValue master; }
If you prefer the JavaFX observable system you get let the system inject you this type as well
import javafx.beans.property.Property; import org.eclipse.fx.core.di.ContextBoundValue; import org.eclipse.fx.core.di.ContextValue; public class DetailView { @Inject @ContextValue("my.domain.Person") Property<Person> property; }
Generally speaking you can use any type for the value injected by @ContextValue
as long as there's an adapter registered which is able to convert from ContextBoundValue
to it.
An observable value is not a one way street so it also allows publish through it:
public class ListView { @PostConstruct public void initUI(@ContextValue("my.domain.Person") Property<Person> property) { ListView<Person> v = new ListView<>(); // ... property.bind(v.getSelectionModel().selectedItemProperty()); } }
@FXMLLoader (Deprecated)
Please see "Eclipse DI" below.
Once "myLoginView.fxml" is loaded you can use @Inject
in your controller:
public class MyLoginController { @Inject MyLoginService loginService; @FXML TextField username; @FXML TextField password; }
@Adapt
Sometimes a generic container objects like an IStructuredSelection
are pushed into the context but in your application you want to get a concrete type. So generic type conversion can be implemented using the AdapterService
introduced below. To make direct use of the adapters as part of the the DI-Process you can use the @org.eclipse.fx.core.adapter.Adapt
annotation who will try to convert the value in the context using the the AdapterService.
For the example. Let's suppose there's an adapter who is able to convert from a String
to an Integer
you could use DI in the following way
import org.eclipse.fx.core.adapter.Adapt; @Inject @Adapt @Named("myValue") Integer myValueAsInteger;
Handling @Translation effeciently
If you mark an injected value with @Translation the DI container will inject you a Message-Object which translated Strings and reinject a new instance when ever you switch the locale. A very common task you have to do when deal with dynamic language switches is to update all UI-Elements. e(fx)clipse provide specialized classes helping you to deal with such a situation.
The first thing you should do is to create a class dealing with the registration and language flipping:
@Creatable public class MessageRegistry<Messages> extends org.eclipse.fx.core.di.AbstractMessageRegistry { @Inject public void updateMessages(@Translation Messages messages) { super.updateMessages(messages); } // Optional provide method to access current message public String MyString() { return getMessages().MyString; } }
And use it in your UI like this:
@Inject MessageRegistry r; @PostConstruct void init(BorderPane parent) { Label l = new Label(); // if you opted to NOT add the MyString() method r.register(l::setText, (m) -> m.MyString); // if you opted to have a MyString() method r.register(l::setText, r::MyString); }
Injection of OSGi-Services
By default the DI container of Eclipse will inject you a service found in the OSGi-Registry but this default comes with a few restrictions who might cause you troubles:
- you can only inject one instance (the one with the highest rank in the OSGi-Service-Registry)
- you won't get a new instance reinjected if a new service instance with a higher ranking is registered on the OSGi-Service-Registry
To fix both of those problems e(fx)clipse provides a new annotation @Service
who fixes both of your problems.
import org.eclipse.fx.core.di.Service public class MyView { @Inject @Service private MyOSGiService instance; // inject the highest service and reinject a new higher ranked one is available @Inject @Service private List<MyOSGiService> allServiceInstances; // all instances available and reinject when services are added/removed }
Dealing with preference values
If you are coming from e4 on SWT you probably used the org.eclipse.e4.core.di.extensions.Preference
annotation. The one from efxclipse is similar but more feature rich compared to the default one.
Support for default values
If no preference value is found (or the value is null) you can specify in the @Preference annotation a default value that is injected instead
import org.eclipse.fx.core.preferences.Preference; import org.eclipse.fx.core.preferences.Value; class MyComponent { @Inject @Preference(key="velocity",defaultValue="1000") int velocity; }
Publishing a preference value change
To inject a value from the preference store but when you want to publish a value you run in a similar situation as discussed in #Publishing_to_the_IEclipseContext. You can only publish by directly accessing IEclipsePreference
which ties your code to the eclipse/osgi framework which is unecessary.
To overcome this limitation e(fx)clipse ships with itsown annotation org.eclipse.fx.core.preferences.Preference
which can be used similar to @ContextValue
.
import org.eclipse.fx.core.preferences.Preference; import org.eclipse.fx.core.preferences.Value; class MyComponent { @Inject @Preference(key="myStringPrefValue") Value<String> sValue; @Inject @Preference(key="myIntegerPrefValue") Value<Integer> iValue; }
Similar to above you can use JavaFX properties:
import org.eclipse.fx.core.preferences.Preference; import javafx.beans.property.Property; class MyComponent { @Inject @Preference(key="myStringPrefValue") Property<String> sValue; @Inject @Preference(key="myIntegerPrefValue") Property<Integer> iValue; }
or Eclipse Databinding Observables:
import org.eclipse.fx.core.preferences.Preference; import org.eclipse.core.databinding.observable.value.IObservableValue; class MyComponent { @Inject @Preference(key="myStringPrefValue", type=String.class) IObservableValue sValue; @Inject @Preference(key="myIntegerPrefValue", type=Integer.class) IObservableValue iValue; }
FXML
FXML in OSGi
Loading FXML-Files in OSGi is a bit harder than doing it in a standard java application and one can not use the static FXMLLoader.load()
so we provide an extra class named org.eclipse.fx.osgi.utilOSGiFXMLLoader
public class MyView { @PostConstruct public void init(BorderPane p) { Node n = OSGiFXMLLoader.load(getClass(), "myView.fxml", null, null); } }
DI in FXML controller
Eclipse DI
If you are running in OSGi and with Eclipse DI (e.g. when you are writing an Eclipse 4 Application) you can make use of org.eclipse.fx.ui.di.InjectingFXMLLoader
.
public class MyView { @PostConstruct public void initUI(BorderPane p, IEclipseContext context) { InjectingFXMLLoader<Node> iFXMLLoader = InjectingFXMLLoader.create( context, getClass(), "myView.fxml" ); Node n = iFXMLLoader.load(); // ... } }
The above code is perfectly ok although you can get rid of the direct IEclipseContext
dependency by using @LocalInstance annotation.
public class MyView { @PostConstruct public void initUI(BorderPane parent, @LocalInstance FXMLLoader loader) { loader.setLocation(getClass().getResource("myView.fxml")); try { parent.setCenter(loader.load()); } catch (IOException e) { e.printStackTrace(); // ... } }
Google Guice
If you are not running on OSGi and Eclipse DI but develop a standard java application which uses Google Guice as the DI container you can make use of org.eclipse.fx.core.guice.InjectingFXMLLoader
public class MyGuiceComponent { public void init(Injector injector) { Node n = InjectingFXMLLoader.loadFXML(injector,getClass().getResource("myui.fxml")); // ... } }
Adapter System
The @ContextValue
support introduce above makes use of the adapter system provided by e(fx)clipse core runtime bundle (org.eclipse.fx.core
) which constits of the 3 main interfaces:
-
Adaptable
-
AdapterService
-
AdapterProvider
Usage
The simplest usage is if the source-object implements the Adaptable
interface
public class Sample { private void sellProduct(Person person) { Customer customer = person.adapt(Customer.class); // ... } }
If the source-object itself does not implement the Adaptable
interface or you are the one who has to implement a class which implements Adaptable
you have to use the AdapterService
whicn is provided through the OSGi-Service-Registry if the object is managed by Eclipse DI it will look like this:
public class Sample { @Inject AdapterService adapterService; private void sellProduct(Person person) { Customer customer = adapterService.adapt(person,Customer.class); // ... } }
Otherwise you need to query the OSGi-Service-Registry to get access to the AdapterService
.
Extending
To enhance the adapter system it is possible to register AdapterProvider
as OSGi-Services. All you need to do is to implement the AdapterProvider
likes this:
public class CustomerAdapterProvider implements AdapterProvider<Person, Customer> { @Override public Class<Person> getSourceType() { return Person.class; } @Override public Class<Customer> getTargetType() { return Customer.class; } @Override public boolean canAdapt(Person sourceObject, Class<Customer> targetType) { return true; } @Override public Customer adapt(final Person sourceObject, Class<Customer> targetType, ValueAccess... valueAccess) { // ... } }
and register it e.g. through DS with
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="my.adapter.sample.customer"> <implementation class="my.adapter.sample.CustomerAdapterProvider"/> <service> <provide interface="org.eclipse.fx.core.adapter.AdapterProvider"/> </service> </scr:component>
Filesystem
Java7 added a new filesystem API which also you to observe modifications of files and directories. Unfortunately this API is very low-level and not as easy to deal with as one might expect. Because of this we provide an API who is a bit more highlevel
import org.eclipse.fx.core.FilesystemService; import org.eclipse.fx.core.Util; public class Sample { public void sample() { FilesystemService fs = Util.lookupService(getClass(), FilesystemService.class); fs.observePath(Paths.get("/tmp")); } }
Media
JavaFX Media only supports the following protocols
- file
- http/https
- jar:file
which might cause you trouble if the resource is eg found in an OSGi-Bundle where protocol is bundleresource:/... .
To deal with resources in a opaque way and not worry if you run in OSGi or not efxclipse provides
-
org.eclipse.fx.ui.controls.media.MediaLoader
-
org.eclipse.fx.core.Util.getLocalURL(URL)
-
org.eclipse.fx.core.Util.getLocalPath(URL)
So loading a media file in your application should be done like this:
public void playSound() { URL url = getClass().getResource("mysound.mp3"); MediaLoader.createMedia( url ).map( MediaPlayer::new ).ifPresent( MediaPlayer::play ); }
MediaLoader
itself delegates to Util.getLocalURL(URL)
/getLocalPath(URL)
who does itself delegate the conversion to a special service(s) of type org.eclipse.fx.core.URLResolver
by default only a resolver for OSGi is available but you are free to contribute your own one.
e4
Position a TrimBar element to the far right
Sometimes it is necessary to position an element in the TrimBar to the far right or bottom. This can be implemented by adding a spacer element to the trimbar
<trimBars elementId="my.sample.trim.bottom" side="Bottom"> <children xsi:type="menu:ToolControl" > <tags>fillspace</tags> </children> <children xsi:type="menu:ToolControl" elementId="my.sample.trim.progress" contributionURI="bundleclass://my.bundle/my.bundle.ProgressControl"/> </trimBars>
Restart Service
Sometimes it is necessary to restart your application with a cleared persisted state. This could be the case when you want to reset the user interface to its default or after an update to your application.e4xmi
or a fragment.e4xmi
.
To use the RestartService
, you can inject it into your handler. To clean the persisted state on the next start of the application pass true
to the restart
method.
public class CleanRestartHandler { @Execute public void execute(RestartService restartService) { restartService.restart(true); } }
Extended @PostContextCreate Features
The life cycle hook @PostContextCreate
is a good place to perform initial tasks, like making your user supply login credentials and setting up a connection. If this fails, there is no need to continue starting the application. With e(fx)clipse it is possible to return a Boolean
to control whether to continue or not.
public class ApplicationLifecycle { @PostContextCreate Boolean connect() { if (connectToServer()) { return true; } return false; } private boolean connectToServer() { ... } }
Another scenario is that you are performing an automatic application update and you want to restart the application right away. For these and similar situations we have provided another extension to the @PostContextCreate
API. With e(fx)clipse it is also possible to return an instance of the enum LifecycleRV
from @PostContextCreate
to tell the framework what to do next. You have the following options:
- LifecycleRV.CONTINUE - Continue starting the application
- LifecycleRV.SHUTDOWN - Shutdown the application
- LifecycleRV.RESTART - Restart the application
- LifecycleRV.RESTART_CLEAR_STATE - Restart the application with a cleared persisted state
public class ApplicationLifecycle { @PostContextCreate LifecycleRV showStartUp(UISynchronize sync) { BlockCondition<LifecycleRV> c = new BlockCondition<>(); Stage s = new Stage(); VBox box = new VBox(); { Button b = new Button("Continue"); b.setMaxWidth(Double.MAX_VALUE); b.setOnAction((e) -> c.release(LifecycleRV.CONTINUE)); box.getChildren().add(b); } { Button b = new Button("Shutdown"); b.setMaxWidth(Double.MAX_VALUE); b.setOnAction((e) -> c.release(LifecycleRV.SHUTDOWN)); box.getChildren().add(b); } { Button b = new Button("Restart"); b.setMaxWidth(Double.MAX_VALUE); b.setOnAction((e) -> c.release(LifecycleRV.RESTART)); box.getChildren().add(b); } { Button b = new Button("Restart with cleared State"); b.setMaxWidth(Double.MAX_VALUE); b.setOnAction((e) -> c.release(LifecycleRV.RESTART_CLEAR_STATE)); box.getChildren().add(b); } s.setScene(new Scene(box,200,200)); s.show(); LifecycleRV rv = sync.block(c); // Block until release s.close(); return rv; } }
UI-State Persistence with Memento-Interface
By default e4 will persists things like the current window location, the location of the sashes, opened tabs, ... but sometimes you have to remember more information e.g. which item in the table has been selected, ... . The e4 application model provides a generic slot in MApplication#persistedState: Map<String,String>
so one can write:
public class MyPart { @PostConstruct public void init(MApplicationElement element) { int selectionId = Integer.parseInt(element.getPersistedState().get("selectionId")); } @PersistState public void saveUIState(MApplicationElement element) { element.getPersistedState().put("selectionId",getSelectionId()+""); } }
but this code has some problems:
- it does not check for NULL and parse errors
- it makes your UI code directly depending on the e4 application model
- it provides you no directly way to store complex informations
org.eclipse.fx.core.Memento
is there to solve all of those problems. So the code from above can be written in e4 & JavaFX like this:
public class MyPart { @PostConstruct public void init(Memento state) { int selectionId = state.get("selectionId",-1); } @PersistState public void saveUIState(Memento state) { state.put("selectionId",getSelectionId()); } }
This solves the 1st and 2nd problem from above.
The 3rd problem is addressed in conjunction with org.eclipse.fx.core.ObjectSerializer
public class MyPart { @PostConstruct public void init(Memento state) { ComplexObjectState complexState = state.get("complexObject",null); } @PersistState public void saveUIState(Memento state) { state.put("complexObject",getComplexObject(),ObjectSerializer.JAXB_SERIALIZER); } }
By default an object serializer on base of JAXB is available but you can plug in your own technology if you are not satisfied with the default one.
Uncatched ExceptionHandling
Uncatachted exceptions can be handled by registering an OSGi-Service of type org.eclipse.fx.core.ExceptionHandler
like this:
@Component public class MyExceptionHandler implement ExceptionHandler { public void handleException(ExceptionData data) { // do something with the exception } }