EMF ItemProvider is the most important middleman in EMF framework providing all of the interfaces needed for model objects to be viewed or edited. They adapt EMF objects by providing the following:
1. Content and label providers which enables model objects to be viewed in Eclipse viewers
2. Property source for model objects to make them available in Eclipse Property View
3. Commands for editing model objects
4. Forwarding change notifications on to Eclipse viewers
EMF generates the ItemProviders for you assuming some defaults. Although the defaults work most of time, there are occasions where you would like to change these. You could make the changes directly in generated code and add @generated NOT annotations. However, by doing this you invite trouble from MDSD gurus. Because the principle says, “Don’t touch generated code!”. Ideally you would like to retain the generated ItemProviders as it is and extend them somehow with your changes.
The tutorial shows how to do this in an elegant way by using Dependency Injection (DI). We will use the EMF example of “Extended Library Model” to take us through this.
Setup
1. Create a new Eclipse workspace
2. Add the “EMF Extended Library Model Example” projects using “New Project Wizard”
3. Run the example projects and create a sample Library model
Extending ItemProviders the EMF way
The generated editor by default displays the title of Book in the tree editor. This is because of the default getText() implementation in BookItemProvider.
/** * This returns the label text for the adapted class. * <!-- begin-user-doc --> * <!-- end-user-doc --> * @generated */ @Override public String getText(Object object) { String label = ((Book)object).getTitle(); return label == null || label.length() == 0 ? getString("_UI_Book_type") : //$NON-NLS-1$ getString("_UI_Book_type") + " " + label; //$NON-NLS-1$ //$NON-NLS-2$ }
Lets say we want to change this such that the number of pages in the book is also displayed along with its title. You could do this by changing the generated code and adding the @generated NOT annotation.
/** * This returns the label text for the adapted class. * <!-- begin-user-doc --> * <!-- end-user-doc --> * @generated NOT */ @Override public String getText(Object object) { String label = ((Book)object).getTitle() + " (" + ((Book) object).getPages() + ") "; return label == null || label.length() == 0 ? getString("_UI_Book_type") : //$NON-NLS-1$ getString("_UI_Book_type") + " " + label; //$NON-NLS-1$ //$NON-NLS-2$ }
If you now run the editor, the number of pages is displayed together with the book name.
This however breaks the Generation Gap Pattern in code generation.
Extending ItemProviders by extension
The cleaner approach is to extend the generated ItemProvider to include your changes. Now we have to also make sure that the tooling uses our customized ItemProvider. This is rather easy.
EMF ItemProviders are in fact EMF adapters. They provide some behavioural extensions to the modeled objects. The recommended way of creating adapters in EMF is using AdapterFactories. The ItemProviders are also created in this way, using the generated ItemProviderAdapterFactory, EXTLibraryItemProviderAdapterFactory in this example. You could override createXXXAdapter() methods to create an instance of your custom ItemProvider and register your extended ItemProviderAdapterFactory.
Lets first do this the traditional way (without DI).
1. Following the Generation Gap Pattern article by Heiko, you could change the extlibrary.genmodel to output the generated code in a src-gen folder of the edit plugin and put your extensions in src folder. In this tutorial, to further isolate our changes we create a new extension plugin, org.eclipse.example.library.edit.extension.
2. Create a new class BookItemProviderExtension which extends BookItemProvider within the new plugin.
public class BookItemProviderExtension extends BookItemProvider { public BookItemProviderExtension(AdapterFactory adapterFactory) { super(adapterFactory); } @Override public String getText(Object object) { return super.getText(object) + " (" + ((Book) object).getPages() + ") "; } }
3. Create EXTLibraryItemProviderAdapterFactoryExtension which extends EXTLibraryItemProviderAdapterFactory
public class EXTLibraryItemProviderAdapterFactoryExtension extends EXTLibraryItemProviderAdapterFactory { @Override public Adapter createBookAdapter() { if (bookItemProvider == null) { bookItemProvider = new BookItemProviderExtension(this); } return bookItemProvider; } }
4. Modify the editor code to use the new ItemProviderAdapterFactoryExtension
public class EXTLibraryEditor extends MultiPageEditorPart implements IEditingDomainProvider, ISelectionProvider, IMenuListener, IViewerProvider, IGotoMarker { ... protected void initializeEditingDomain() { // Create an adapter factory that yields item providers. // adapterFactory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE); adapterFactory.addAdapterFactory(new ResourceItemProviderAdapterFactory()); adapterFactory.addAdapterFactory(new EXTLibraryItemProviderAdapterFactoryExtension()); adapterFactory.addAdapterFactory(new ReflectiveItemProviderAdapterFactory()); ... } ... }
If you run the editor again, by including the new extension plugin you would get the same result as before. With this step, we managed to isolate our changes to a new plugin.
Extending ItemProviders using Google Guice
Although, we managed to isolate our changes without changing the generated code, this might not be enough in the long run. What if
1. you need multiple ItemProvider implementations for the same EMF Object and want to switch between them
2. you want to extend many ItemProviders in the inheritance hierarchy. For example, you need to change PersonItemProvider and WriterItemProvider (which extends PersonItemProvider).
Although, you don’t need to use DI to solve these problems, DI would do it for you in a simpler, cleaner way. In this tutorial we will use Google Guice to achieve this. Google Guice is cool light weight dependency Injection framework. You could inject your dependencies just by writing few lines of code and some annotations. If you don’t like annotations, you could even use Guice without them. If you are not familiar with Google Guice read, Getting Started.
Lets go ahead and “Guicify” our earlier example. We start with simple modifications and go on to detailed ones in later steps.
1. Firstly, you need to add a dependency to Google Guice from our org.eclipse.example.library.edit.extension plugin.
Google Guice is currently not publicly available as an eclipse plugin. There is a bugzilla request to add it to the Orbit bundle. It is however available with Xtext as an eclipse plugin. Since I have Xtext in my target, I use this in my tutorial. If you don’t have this, you need to add Google Guice as an external jar to your project.
2. The next step would be to get rid of the “new” statements in the extended ItemProviderFactory. This is what binds the ItemProviderAdapterFactory to a specific ItemProvider implementation. We use Google Guice field injection to inject BookItemProvider.
public class EXTLibraryItemProviderAdapterFactoryExtension extends EXTLibraryItemProviderAdapterFactory { @Inject protected BookItemProvider bookItemProvider; @Override public Adapter createBookAdapter() { return bookItemProvider; } }
3. We now need to create a Google Guice Module to bind the extended ItemProvider. So go ahead and create a module as follows:
public class LibraryModule extends AbstractModule implements Module{ @Override protected void configure() { bind(BookItemProvider.class).to(BookItemProviderExtension.class).in(Scopes.SINGLETON); } }
4. You could also inject the extended ItemProviderAdapterFactory into our editor. Since we don’t want the editor to have a Google Guice dependency, we make the following changes to the module and extended ItemProvider.
public class LibraryModule extends AbstractModule implements Module{ private final AdapterFactory adapterFactory; public LibraryModule(AdapterFactory adapterFactory) { this.adapterFactory = adapterFactory; } @Override protected void configure() { bind(AdapterFactory.class).toInstance(adapterFactory); bind(BookItemProvider.class).to(BookItemProviderExtension.class).in(Scopes.SINGLETON); } }
public class BookItemProviderExtension extends BookItemProvider { @Inject public BookItemProviderExtension(AdapterFactory adapterFactory) { super(adapterFactory); } @Override public String getText(Object object) { return super.getText(object) + " (" + ((Book) object).getPages() + ") "; } }
5. Now we need to create a Guice Injector.
public class EXTLibraryItemProviderAdapterFactoryExtension extends EXTLibraryItemProviderAdapterFactory { public LibraryItemProviderAdapterFactoryExtension() { Guice.createInjector(new LibraryModule(this)); } @Inject protected BookItemProvider bookItemProvider; @Override public Adapter createBookAdapter() { return bookItemProvider; } }
6. Run it and you get the same result as before.
You could now inject a different implementation of the ItemProvider by only creating a new binding in the module file.
That was a rather trivial example. Lets take a more significant example where we need to make changes to PersonItemProvider and WriterItemProvider (which extends PersonItemProvider).
The Extended Library Model example, by default displays the Lastname of the Writer for the attribute Name. This comes from the following lines of code in PersonItemProvider, the superclass of WriterItemProvider.
@Override public String getText(Object object) { String label = ((Person)object).getLastName(); return label == null || label.length() == 0 ? getString("_UI_Person_type") : //$NON-NLS-1$ getString("_UI_Person_type") + " " + label; //$NON-NLS-1$ //$NON-NLS-2$ }
Lets change this to display the Firstname instead of Lastname.
1. Create a new extension ItemProvider for Person, PersonItemProviderExtension and override the getText() method as follows
public class PersonItemProviderExtension extends PersonItemProvider { @Inject public PersonItemProviderExtension(AdapterFactory adapterFactory) { super(adapterFactory); } @Override public String getText(Object object) { String label = ((Person) object).getFirstName(); return label == null || label.length() == 0 ? getString("_UI_Person_type") : //$NON-NLS-1$ getString("_UI_Person_type") + " " + label; //$NON-NLS-1$ //$NON-NLS-2$ } }
2. Inject the extended PersonItemProviderExtension into the ItemProviderAdapterFactory extension.
public class EXTLibraryItemProviderAdapterFactoryExtension extends EXTLibraryItemProviderAdapterFactory { ... @Inject protected PersonItemProvider personItemProvider; @Override public Adapter createPersonAdapter() { return personItemProvider; } }
3. Update the Google Guice Module
@Override protected void configure() { bind(AdapterFactory.class).toInstance(adapterFactory); bind(BookItemProvider.class).to(BookItemProviderExtension.class).in(Scopes.SINGLETON); bind(PersonItemProvider.class).to(PersonItemProviderExtension.class).in(Scopes.SINGLETON); }
If you run the code now, you will see that we haven’t got the expected results yet. This is because, the WriterItemProvider still extends PersonItemProvider and not PersonItemProviderExtension, where we integrated the changes. We could go ahead and create a new WriterItemProviderExtension which extends PersonItemProviderExtension. But in this way we would tie the WriterItemProviderExtension to PersonItemProviderExtension implementation. We would like to inject each of these extensions without creating any inter-dependency between any of them.
4. We can change inheritance to delegation and use injection again here, that is, inject PersonItemProviderExtension into WriterItemProviderExtension and delegate the getText() call.
Changing inheritance to delegation however comes at the cost of some Java specific issues which I will talk about in a later part of my article.
public class WriterItemProviderExtension extends WriterItemProvider { @Inject private PersonItemProvider personItemProvider; @Inject public WriterItemProviderExtension(AdapterFactory adapterFactory) { super(adapterFactory); } @Override public String getText(Object object) { return personItemProvider.getText(object); } }
5. Don’t forget to update your EXTLibraryItemProviderAdapterFactoryExtension and Guice Module to bind WriterItemProviderExtension.
public class EXTLibraryItemProviderAdapterFactoryExtension extends EXTLibraryItemProviderAdapterFactory { ... @Inject protected WriterItemProvider writerItemProvider; @Override public Adapter createWriterAdapter() { return writerItemProvider; } }
@Override protected void configure() { bind(AdapterFactory.class).toInstance(adapterFactory); bind(BookItemProvider.class).to(BookItemProviderExtension.class).in(Scopes.SINGLETON); bind(PersonItemProvider.class).to(PersonItemProviderExtension.class).in(Scopes.SINGLETON); bind(WriterItemProvider.class).to(WriterItemProviderExtension.class).in(Scopes.SINGLETON); }
If you run the code now, you will see that FirstName is displayed as Name attribute of Writer, instead of Lastname.
I will cover this in the second part of the tutorial. Hang on!
Sources
Tagged: EMF, Guice, ItemProvider
