Eclipse Platform allows us to be more productive by reusing many parts of the
framework.
For example, whether we want to display content in a standard
TreeViewer SWT widget or the much more versatile
Common Navigator Framework (CNF), we can
reuse the same objects for both.
We need to provide a
model (the object itself) and two
presenters: an
ITreeContentProvider (which extends the generic
IContentProvider) and an
ILabelProvider. [Note: I just realized that this is the real world example of the often hailed
MVP pattern aka Model-View-Presenter!]
While you
can implement a
content provider and
label provider yourself, if you're like me it's sooo much easier if you just
reuse Eclipse's
BaseWorkbenchContentProvider and
WorkbenchLabelProvider and then make your objects adaptable to
IWorkbenchAdapter.
Overview
There are three steps to make this happen:
- Implement IWorkbenchAdapter for your model class
- Create a org.eclipse.core.runtime.IAdapterFactory implementation that can create IWorkbenchAdapter adapters for your model classes
- Register your IAdapterFactory to the Eclipse Platform adapter manager
As noted in the post title, I have a surprise shortcut for you! ;-)
But just to make things interesting, I'll describe in detail how
laborious it still is to adapt to
IWorkbenchAdapter even by reusing
BaseWorkbenchContentProvider and
WorkbenchLabelProvider for your models.
Implementing IWorkbenchAdapter
Here is one way to implement
IWorkbenchAdapter :
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.ui.model.IWorkbenchAdapter;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import com.abispulsa.Contact;
public class ContactWorkbenchAdapter implements IWorkbenchAdapter {
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getChildren(java.lang.Object)
*/
@Override
public Object[] getChildren(Object o) {
return ((Contact)o).getMobileNumbers().toArray();
}
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getImageDescriptor(java.lang.Object)
*/
@Override
public ImageDescriptor getImageDescriptor(Object object) {
return AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/user.png");
}
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getLabel(java.lang.Object)
*/
@Override
public String getLabel(Object o) {
return ((Contact)o).getName();
}
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getParent(java.lang.Object)
*/
@Override
public Object getParent(Object o) {
return ((Contact)o).eContainer();
}
}
To be honest, I think the name
IWorkbenchAdapter is confusing. It would be clearer if the interface name is
IWorkbenchContent, simply for the fact that
BaseWorkbenchContentProvider expects the objects to be an instance of
IWorkbenchAdapter. The fact that we should
adapt our objects to
IWorkbenchAdapter instead of implementing
IWorkbenchAdapter interface directly in our objects in another matter. In other words, it's possible to use
BaseWorkbenchContentProvider with our objects *without* making our objects "adapt" to anything else, as long as it implements
IWorkbenchAdapter.
The above is pretty much a manual way to implement a
IWorkbenchAdapter for model class. And of course, you have to do that for
*every* model class you have. One adapter per model class. That's scary, huh?! Unless you have a predefined interface or base class... but I have a better idea (surprise! but later on below..)
Creating the IAdapterFactory
This is the less painful step, implement
IAdapterFactory that can create
IWorkbenchAdapter for your objects :
import org.eclipse.core.runtime.IAdapterFactory;
import org.eclipse.ui.model.IWorkbenchAdapter;
import com.abispulsa.Contact;
import com.abispulsa.MobileNumber;
import com.abispulsa.Organization;
public class AbisPulsaAdapterFactory implements IAdapterFactory {
private ContactWorkbenchAdapter contactAdapter = new ContactWorkbenchAdapter();
private OrganizationWorkbenchAdapter organizationAdapter = new OrganizationWorkbenchAdapter();
private MobileNumberWorkbenchAdapter mobileNumberAdapter = new MobileNumberWorkbenchAdapter();
/* (non-Javadoc)
* @see org.eclipse.core.runtime.IAdapterFactory#getAdapter(java.lang.Object, java.lang.Class)
*/
@Override
public Object getAdapter(Object adaptableObject, Class adapterType) {
if (adapterType == IWorkbenchAdapter.class) {
if (adaptableObject instanceof Contact)
return contactAdapter;
else if (adaptableObject instanceof Organization)
return organizationAdapter;
else if (adaptableObject instanceof MobileNumber)
return mobileNumberAdapter;
}
return null;
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.IAdapterFactory#getAdapterList()
*/
@Override
public Class[] getAdapterList() {
return new Class[] { IWorkbenchAdapter.class };
}
}
Register the Factory for the Model Classes
For each model class that can be adapted to
IWorkbenchAdapter using your adapter factory, register it to the Eclipse Platform :
import org.eclipse.core.runtime.Platform;
import com.abispulsa.Contact;
...
private AbisPulsaAdapterFactory adapterFactory = new AbisPulsaAdapterFactory();
...
Platform.getAdapterManager().registerAdapters(adapterFactory, Contact.class);
Using BaseWorkbenchContentProvider in the TreeViewer
It's actually pretty standard stuff, but in case I have someone new to SWT/Eclipse/RCP/RAP ecosystem, this should provided a quick orientation to connect the above stuff:
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.ui.model.BaseWorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import com.abispulsa.AbispulsaFactory;
...
treeViewer = new TreeViewer(parent, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
getSite().setSelectionProvider(treeViewer);
treeViewer.setLabelProvider(new WorkbenchLabelProvider());
treeViewer.setContentProvider(new BaseWorkbenchContentProvider());
...
Contact contact = AbispulsaFactory.eINSTANCE.createContact();
contact.setName("Hendy Irawan");
treeViewer.setInput(contact);
Done!!!
Reflecting Back
WHOA....! A lot of stuff just to get something displayed on a tree viewer!
Actually, it's not that bad and it's quite powerful. Eclipse Platform provides :
- Your objects can now be displayed in pretty much any kind of exotic view possible. BaseWorkbenchContentProvider and WorkbenchLabelProvider will handle all that for you, consistently.
- Your objects can, of course, be displayed inside a Common Navigator Framework (CNF) view, which can display several objects using a flexible combination of content providers, actions, popup menus, etc.
With other (non-Eclipse) frameworks, with the same amount of code you'd still be a looooong way to achieving the flexibility that Eclipse Platform provides.
Eclipse FTW!
But wait... EMF Comes to the Rescue
I haven't even written the main content of my post yet ! Hehe...
Some of you may notice that I used
EMF to generate my model classes above. (Hurray!)
While happily hacking my
IWorkbenchAdapter adapters (see the confusion? it's not cool to write "IWorkbenchAdapter adapter"), I noticed that I have to set the image for the model, however I already had the images provided by the generated EMF-Edit providers. This practically makes the image on my workbench model different from the one used by EMF Editors.
Here's the
generated *EMF* Item Provider :
/**
* This is the item provider adapter for a {@link com.abispulsa.Organization} object.
* <!-- begin-user-doc -->
* <!-- end-user-doc -->
* @generated
*/
public class OrganizationItemProvider
extends ItemProviderAdapter
implements
IEditingDomainItemProvider,
IStructuredItemContentProvider,
ITreeItemContentProvider,
IItemLabelProvider,
IItemPropertySource {
...
/**
* This returns Organization.png.
* <!-- begin-user-doc -->
* <!-- end-user-doc -->
* @generated NOT
*/
@Override
public Object getImage(Object object) {
return overlayImage(object, getResourceLocator().getImage("full/obj16/Organization.png"));
}
Hey.... I can
reuse that!
So I change this hardcoded piece :
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getImageDescriptor(java.lang.Object)
*/
@Override
public ImageDescriptor getImageDescriptor(Object object) {
return AbstractUIPlugin.imageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/organization.png");
}
Into something more consistent, delegate the stuff to my EMF Edit provider:
private OrganizationItemProvider itemProvider = (OrganizationItemProvider) new AbispulsaItemProviderAdapterFactory().createOrganizationAdapter();
...
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getImageDescriptor(java.lang.Object)
*/
@Override
public ImageDescriptor getImageDescriptor(Object object) {
return ImageDescriptor.createFromURL((URL) itemProvider.getImage(object));
}
When I did that, I noticed that
EMF.Edit's ItemProvider provides other methods, like
getChildren(),
getText(), etc. that I can use... So I hack away and change the
IWorkbenchAdapter implementation :
public class OrganizationWorkbenchAdapter implements IWorkbenchAdapter {
private OrganizationItemProvider itemProvider = (OrganizationItemProvider) new AbispulsaItemProviderAdapterFactory().createOrganizationAdapter();
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getChildren(java.lang.Object)
*/
@Override
public Object[] getChildren(Object o) {
return itemProvider.getChildren(o).toArray();
//return ((Organization)o).getContacts().toArray();
}
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getImageDescriptor(java.lang.Object)
*/
@Override
public ImageDescriptor getImageDescriptor(Object object) {
return ImageDescriptor.createFromURL((URL) itemProvider.getImage(object));
}
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getLabel(java.lang.Object)
*/
@Override
public String getLabel(Object o) {
return itemProvider.getText(o);
//return ((Organization)o).getName();
}
/* (non-Javadoc)
* @see org.eclipse.ui.model.IWorkbenchAdapter#getParent(java.lang.Object)
*/
@Override
public Object getParent(Object o) {
return itemProvider.getParent(o);
//return ResourcesPlugin.getWorkspace().getRoot();
}
}
I put the original method implementations there so you can compare. The original implementations are so tightly coupled to the model structure, the EMF.Edit-powered ones are more generic.
Then I noticed that I practically has no dependency to the
original model (
Organization class), because I can change :
private OrganizationItemProvider itemProvider = (OrganizationItemProvider) new AbispulsaItemProviderAdapterFactory().createOrganizationAdapter();
into something like:
AbispulsaItemProviderAdapterFactory factory = new AbispulsaItemProviderAdapterFactory();
ITreeItemContentProvider treeItemProvider = (ITreeItemContentProvider) factory.adapt(object, ITreeItemContentProvider.class);
IItemLabelProvider labelProvider = (IItemLabelProvider) factory.adapt(object, IItemLabelProvider.class);
Be surprised that right now I don't have any
mention of the original model's class name
anywhere!
Content Provider Wars Episode V: The EMF Strikes Back!
Unfortunately the
ITreeItemContentProvider etc. interfaces are EMF's, and it's unusable by SWT and other Eclipse UI views, which requires
IWorkbenchAdapter or
IContentProvider and
ILabelProvider.
Then something struck me: come on, since so much infrastructure has been provided by EMF, surely EMF wouldn't leave out something so obvious: a
content provider and a
label provider!
Enter the warlords:
These providers are contained within the plugin
org.eclipse.emf.edit.ui.
We only need to subclass these providers and provide the our own list of
org.eclipse.emf.common.notify.AdapterFactory objects :
Our EMF-powered Content Provider :
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory;
import org.eclipse.emf.edit.provider.resource.ResourceItemProviderAdapterFactory;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import com.abispulsa.provider.AbispulsaItemProviderAdapterFactory;
public class AbispulsaContentProvider extends AdapterFactoryContentProvider {
private static AdapterFactory adapterFactory;
static {
adapterFactory = new ComposedAdapterFactory(new AdapterFactory[] {
new ResourceItemProviderAdapterFactory(),
new AbispulsaItemProviderAdapterFactory(),
new ReflectiveItemProviderAdapterFactory()
});
}
public AbispulsaContentProvider() {
super(adapterFactory);
}
}
Our EMF-powered Label Provider : (
identical code to the above!)
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory;
import org.eclipse.emf.edit.provider.resource.ResourceItemProviderAdapterFactory;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider;
import com.abispulsa.provider.AbispulsaItemProviderAdapterFactory;
public class AbispulsaLabelProvider extends AdapterFactoryLabelProvider {
private static AdapterFactory adapterFactory;
static {
adapterFactory = new ComposedAdapterFactory(new AdapterFactory[] {
new ResourceItemProviderAdapterFactory(),
new AbispulsaItemProviderAdapterFactory(),
new ReflectiveItemProviderAdapterFactory()
});
}
/**
* @param adapterFactory
*/
public AbispulsaLabelProvider() {
super(adapterFactory);
}
}
Using Our EMF-powered Content and Label Providers
For an SWT TreeViewer :
treeViewer.setLabelProvider(new AbispulsaLabelProvider());
treeViewer.setContentProvider(new AbispulsaContentProvider());
Right now it's safe to throw all those painfully coded
IWorkbenchAdapter, UI
AdapterFactory's, and the adapter factory registration code to the dumpster. ;-)
Super quick, huh? :-)
A nice side effect is you don't rely on any "workbench" stuff... But of course now you rely on EMF Edit UI :-)
Another nice side effect is the content/label providers actually can be used with any
EMF EObject, due to
ReflectiveItemProviderAdapterFactory magic. :-)
EMF-powered Content in Common Navigator Framework (CNF) View
Enough with the Star Wars talk, it's time to show how beautiful that concise piece of code working inside the powerful
Common Navigator Framework (CNF) View.
Declaratively of course!
Here's the relevant parts in
plugin.xml. The XML codes below might seem scary to readers who aren't familiar yet with how
Eclipse plugin extensions work, but in reality there's no XML typing involved at all! All these extensions are editable using PDE's Extensions visual editor.
Declare the
CNF view part:
<extension point="org.eclipse.ui.views">
<category id="com.abispulsa.ui.abispulsa_category" name="AbisPulsa">
</category>
<view allowMultiple="false" category="com.abispulsa.ui.abispulsa_category"
class="org.eclipse.ui.navigator.CommonNavigator" icon="icons/contacts.ico"
id="com.abispulsa.ui.contactsNav_view" name="Contacts Nav"
restorable="true">
</view>
</extension>
Put the view in perspective: (in this case, all perspectives)
<extension point="org.eclipse.ui.perspectiveExtensions">
<perspectiveExtension targetID="*">
<view id="com.abispulsa.ui.contactsNav_view" minimized="false"
ratio="0.4" relationship="left" relative="org.eclipse.ui.editorss">
</view>
</perspectiveExtension>
</extension>
Declare our
NCE (Navigator Content Extension), which uses our
EMF-powered
content and
label providers, handling all
EObject instances:
<extension point="org.eclipse.ui.navigator.navigatorContent">
<navigatorContent id="com.abispulsa.ui.modelContent" name="Model"
contentProvider="com.abispulsa.presentation.AbispulsaContentProvider"
labelProvider="com.abispulsa.presentation.AbispulsaLabelProvider">
<triggerPoints>
<instanceof value="org.eclipse.emf.ecore.EObject">
</instanceof>
</triggerPoints>
</navigatorContent>
</extension>
Then
bind our
CNF navigator view with our
NCE :
<extension point="org.eclipse.ui.navigator.viewer">
<viewerContentBinding viewerId="com.abispulsa.ui.contactsNav_view">
<includes>
<contentExtension pattern="com.abispulsa.ui.modelContent" />
</includes>
</viewerContentBinding>
</extension>
Done!
The real beauty of the
EMF enhanced solution is you actually have to set it up only once. From now on all changes you do to your models will be reflected
directly in the
UI content/label providers and all the views that use your
EMF models. How cool is that? ;-)
PS. For your viewing pleasure, here's the resulting
Common Navigator Framework (CNF) view, powered by
EMF models. :-)
P.P.S. As a bonus, for those who notice, these EMF models are powered by
CDO Model Repository ! ;-)
Additional resources: