Wednesday, December 29, 2010

Reusing EMF.Edit-generated ItemProviders for implementing Eclipse UI IContentProvider



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:
  1. Implement IWorkbenchAdapter for your model class
  2. Create a org.eclipse.core.runtime.IAdapterFactory implementation that can create IWorkbenchAdapter adapters for your model classes
  3. 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:

6 comments:

  1. Handbags are a very popular fashion hermes handbags wholesale accessory - in fact one could say that some of us have a bit of an obsession with the handbag! And one of the most popular handbags these days is the oversized handbag. Whether it is the Hobo, the Cross Body Bag, the Satchel or the Shoulder bag, the oversized hermes handbags discount handbag is a very popular fashion item. Why? Because it has the whole three S's - it's serviceable, sophisticated and sexy - all at the same time. The oversized handbag meets all the criteria that we could possibly want in a handbag (apart from being small and cute which is another whole article in itself and the small and cute handbag is like a whole different planet from the oversized hermes birkin cheap bag and just can't be compared....).

    ReplyDelete
  2. The Air Jordan 4 Cavs Retro Black/Orange Blaze-Old Royal commemorates Mike’s iconic shot over Craig Ehlo in the first round of the NBA Playoffs.Jordan Retro 4 Cavs For Sale pair takes on a Black nubuck base with Orange Blaze and Old Royal accents throughout inspired by MJ’s famous shot against the Cleveland Cavaliers in 1989. Over the years the Air Jordan 4 Knicks Cavs became a true collectors item, but prices continued to rise. Now,the Air Jordan 4 Cavs Retro For Sale,a predominantly black nubuck upper is contrasted by plenty of blue through the upper, as well as orange on the outsole and some accents ,and more and more pairs are coming up high quality athletic shoes, due to the age and the materials used by Jordan Brand.

    ReplyDelete
  3. Happy to find your blog and the great images that you have on a regular basis!

    Buy Cheap RS Gold
    Buy D3 Gold

    ReplyDelete
  4. Released in late 2004, the game began cheap wow gold to take off in his first few months in a dazzling. Its spectacular growth gw2 gold even allowed him to enter the Guinness Book of Records to exceed 10 million users in cheap guild wars 2 gold 2009

    ReplyDelete
  5. public class OrganizationWorkbenchAdapter implements IWorkbenchAdapter {
    private OrganizationItemProvider itemProvider = guild wars 2 gold(OrganizationItemProvider) new AbispulsaItemProviderAdapterFactory().createOrganizationAdapter();
    /* (non-Javadoc)
    * @see org.eclipse.ui.model.IWorkbenchAdapter# gw2 gold getChildren(java.lang.Object)
    */

    ReplyDelete
  6. Ubisoft has confirmed that his remarkable musical title Rocksmith, mainly for players who want to learn rs gold 07 to play guitar or bass, has sold 1.4 million copies worldwide since its launch on the market a year ago and medio.Adem s, the company gala has been referred to a report which highlights that enjoy Rocksmith is "the fastest way to learn to play the guitar." So much cheap 07 runescape gold so, that about 95 percent of users say they have improved their ability to guitarra.Por addition, Ubisoft has released more curious as other data that have been downloaded more than 3 million additional songs, or have performed more than 120,000 million runescape gold shop musicales.

    ReplyDelete