compas

Abstract

This article describes some efforts to use the Common Navigator Framework (CNF). In doing so it incorporates the information already covered in different articles, but also focuses on the specific use case of providing a view of something completely unrelated to the platform resources. So the aim is not to add some content to the “Project Explorer” which is an example of resource-oriented CNF usage, but to provide a view on a completely own data model.

Introduction

Project Explorer
A very common UI element to represent data is a tree view. In SWT this UI element is implemented using the Tree widget. Following the MVC design pattern in the TreeViewer, JFace simplifies the usage of the Tree widget by delegating the task of content adoption to the ContentProvider and the label production to the LabelProvider (and using Sorters and Filters for sorting and filtering). Still for a single representation one has to construct a viewer and configure it with a corresponding Label- and ContentProvider. Further code reduction can be achieved by the use of WorkbenchContentProvider and WorkbenchLabelProvider if the elements can be made adaptable (implement IAdaptable interface and making them first-class workbench citizens). This approach is helpful, if the elements has to be displayed in several different viewers (e.G. Table). Finally, the Common Navigator Framework (CNF) is a facility provided by the Eclipse Platform which allows the usage of multiple Label- and ContentProvider on the same view. The providers are activated and used dynamically and can be configured declarative or programmatically. The advantage of CNF approach is the ability to combine elements in one view which have different origins(e.G. contributed by different plugins). CNF is used in Eclipse IDE: e.G. “ProjectExplorer” and “CVS Synchrnoize” are both instances of the CNF.

The usage of the CNF in your own application for purposes of representation of resource-based (and usually file-based) content is discussed in articles of Micael Elder in detail. The main idea is to instantiate the view, declare the default content and UI interface and make some additions where needed. This post has a different aim: we start from scratch and represent completely resource unrelated content. Before diving in the implementation details, some overview is provided.

UI Overview

Project Explorer with default options There are many things which can be configured by the usage of CNF and it is beyond the scope of this post to cover all of them. Still there are several things to understand before the actual code can be written. The user interacts with a View which shows the data elements. Which elements are shown is configured using the navigation content extensions. Shown elements can be filtered with Filters and sorted using Sorters on behalf of the user. There are some predefined actions and their positions in the UI and corresponding extension points to contribute to. Project Explorer Pop-up. The actions for Working sets, Customize View, Link with editor belong to this category. The user can also right-click on particular element in the tree and sees a popup-menu. This menu is configured based on the content element and can be (is) contributed by several plugins. The action contribution is also covered in the article series from Michael Elder.

Minimal non-resource based CNF viewer

The following example provide a set of minimal steps required to create a non-resource based CNF viewer. Your plug-in requires at least a dependency to org.eclipse.ui.navigator, org.eclipse.ui.

Data model

Let us assume a simple data model that should be represented in the view. There are two kinds of elements: parents and children (see Composite design pattern). Both Child and Parent are POJOs:

public class Child
{
    private String name;
    private Parent parent;

    public Child(String name)
    {
        super();
        this.name = name;
    }

    // getters and setter
...
}

Every child stores information about its parent, and every parent knows its children. In order to maintain the model in-sync the setChildren() method of the Parent class takes care of unsetting on all previous children and setting the parent on new children. This is just a sample code and there are different ways to implement this more elegantly (like e.G. holding the parent-child relation externally).

public class Parent extends Child
{
    private Child[] children = new Child[0];
    private Object rootElement;

    public Parent(String name)
    {
        super(name);
    }

    public void setChildren(Child[] children)
    {
        if (children != null)
        {
            setChildrensParent(null, this.children);
        }
        this.children = children;
        setChildrensParent(this, this.children);
    }

    /**
     * Sets children's parent
     * @param parent parent to be set
     * @param children children to set the parent
     */
    private static void setChildrensParent(Parent parent, Child[] children)
    {
        for (int i = 0; i < children.length; i++)
        {
            children[i].setParent(parent);
        }
    }

    // getter and setter
...
}

Please note, that nothing more is assumed about the parent and the child. It is a good idea to make such elements implement IAdaptable, which simplifies a lot of issues later, but this is another topic. You may have noticed the rootElement member of the Parent. Again, this is only used in order to keep the object tree structure as simple as possible and provide a hook to the root of the tree. The final tree structure contains one root object, several parent objects containing several child objects each.

public class Root extends PlatformObject
{
}

Declaring the viewer

In order to contribute a view to RCP/IDE the org.eclipse.ui.views extension point is used:

   <extension
         point="org.eclipse.ui.views">
      <view
            class="de.techjava.rcp.cnf.provider.CNFNavigator"
            id="de.techjava.rcp.cnf.view"
            name="Virtual CNF"
            restorable="true">
      </view>
   </extension>

The two important things here are the class attribute pointing to the class of the CNF Navigator and the id attribute that will be used later in order to identify the view. Instead of pointing to the org.eclipse.ui.navigator.CommonNavigator
the reference to ParentChildNavigator is provided which is a subclass of the CommonNavigator. The reason for that is, that CommonNavigator gets its initial input (during initialization) from the Workbench by calling getSite().getPage().getInput(). In the IDE scenario, the default page input is IWorkspaceRoot, in the RCP scenario it is null and can be configured in the WorkbenchAdvisor. Instead, another implementation of getInitialInput() can be provided, passing the dummy Root object, which indicates the root of the tree.

public class CNFNavigator extends CommonNavigator
{
    protected IAdaptable getInitialInput()
    {
        return new Root();
    }
}

Defining viewer content

In order to see something in the freshly-defined viewer, the information about the content has to be provided. The extension point org.eclipse.ui.navigator.navigatorContent is defined for this purpose. The main element responsible for the content is called navigatorContent and contains the following information:

  • id – the id of the content, to be referenced in the viewer
  • name – the name of the content, which is seen in the Customize View dialog using the content tab, which can be activated or deactivated there by the user.
  • contentProvider – the contentProvider class responsible for providing content elements
  • labelProvider – the labelProvider class responsible for rendering a content element in the view
   <extension
         point="org.eclipse.ui.navigator.navigatorContent">
      <navigatorContent
            activeByDefault="true"
            contentProvider="de.techjava.rcp.cnf.provider.CNFContentProvider"
            id="de.techjava.rcp.cnf.content.VirtualContent"
            labelProvider="de.techjava.rcp.cnf.provider.CNFLabelProvider"
            name="Virtual content"
            priority="normal">
         <triggerPoints>...</triggerPoints>
         <possibleChildren>...</possibleChildren>
      ...
      </navigatorContent>
   </extension>

In addition, nested elements triggerPoints and possibleChildren describe when the current content is activated. This means that a CNF Viewer can compose the content coming from multiple providers and needs to know what content to display. These will be covered in the following sections. 

How the platform uses the viewer

Before diving into the code section, it is worth spending a small section on the topic, how the platform uses the CNF viewer. In general, if an element is selected in the viewer, the CNF consults triggerPoints of all Navigation Content Extensions (NCEs) provided and tries to match the element to the trigger point expression. If the expression matches, the NCE is activated by the platform and the corresponding contentProvider is responsible for delivering content. If the element is activated by other means than in the viewer (e.G. in the Editor), the platform consults possibleChildren list and tries to match the corresponding expression. For resource-based approaches, the NCE with id org.eclipse.ui.navigator.resourceContent is usually provided. This NCE is enabled / triggered on any element that is instance of IResource. By this means e.G. the Project Explorer in the IDE passes the workspace root to the ResourceExtensionContentProvider which is responsible for delivering the children of the workspace root – the projects. In a non-resource based approach, the NCE must be triggered by the initial input of the viewer, in this example by the Root object. If the selected element is a Parent node, the NCE should also be triggered. The following definition of the trigger point accomplishes this task:

         <triggerPoints>
            <or>
               <instanceof value="de.techjava.rcp.cnf.data.Root" />
               <instanceof value="de.techjava.rcp.cnf.data.Parent" />
            </or>
         </triggerPoints>

Since the children delivered by the content provider on Root as parent element are instances of Parent and on Parent as parent element are instances of Child, the possibleChildren code looks as follows:

         <possibleChildren>
            <or>
               <instanceof value="de.techjava.rcp.cnf.data.Parent" />
               <instanceof value="de.techjava.rcp.cnf.data.Child" />
            </or>
         </possibleChildren>

Label and Content providers

After the way how the navigation content is being activated is discussed, the Content and Label providers can be addressed. The label provider implementation is straight-forward. Its task is to produce a textual and graphical representation of every element. In addition, it implements the IDescriptionProvider interface to provide description in the status bar when an element is selected. Please note, that this section is not CNF-specific but is related to JFace ContentProvider and JFace LabelProvider, as usual for any Viewer. It is provided here only for completion and if the user is not familiar with it in detail.

public class CNFLabelProvider extends LabelProvider implements ILabelProvider, IDescriptionProvider
{
    public String getText(Object element)
    {
        if (element instanceof Parent)
        {
            return ((Child)element).getName() + " [ " +((Parent)element).getChildren().length + " ]";
        } else if (element instanceof Child) 
        {
            return ((Child)element).getName();
        } 
        return null;
    }

    public String getDescription(Object element)
    {
        String text = getText(element);
        return "This is a description of " + text;
    }
    
    public Image getImage(Object element)
    {
        if (element instanceof Parent) 
        {
            return PlatformUI.getWorkbench().getSharedImages()
              .getImage(ISharedImages.IMG_OBJ_FOLDER);
        } else if (element instanceof Child) 
        {
            return PlatformUI.getWorkbench().getSharedImages()
              .getImage(ISharedImages.IMG_OBJ_FILE);
        }
        return null;
    }
}

The methods above are self-describing. For any element the text and image is selected based on element type. For children, the name is shown, for parents the number of children is displayed, additionally. The platform built-in icons of folder and file are used for convenience. The methods for the Label/Content provider usually follow this pattern of a instanceof cascade. 

The content provider is responsible for the following tasks:

  • At requested of Root it should deliver the list of Parent elements
  • At requested of Parent it should deliver the list of its children (Child elements)
  • At requested of Child it should deliver an empty list

Since no real data model is available, its creation is hooked to the first access (that is, of course, for demonstration purposes only)

public class CNFContentProvider implements ITreeContentProvider
{

    private static final Object[] EMPTY_ARRAY = new Object[0];
    private Parent[] parents;

    public Object[] getChildren(Object parentElement)
    {
        if (parentElement instanceof Root)
        {
            if (parents == null)
            {
                initializeParents(parentElement);
            }
            return parents;
        } else if (parentElement instanceof Parent)
        {
            return ((Parent) parentElement).getChildren();
        } else if (parentElement instanceof Child)
        {
            return EMPTY_ARRAY;
        } else
        {
            return EMPTY_ARRAY;
        }
    }

    public Object getParent(Object element)
    {
        if (element instanceof Child)
        {
            return ((Child) element).getParent();
        } else if (element instanceof Parent)
        {
            return ((Parent) element).getRoot();
        }
        return null;
    }

    public boolean hasChildren(Object element)
    {
        return (element instanceof Root || element instanceof Parent);
    }

    public Object[] getElements(Object inputElement)
    {
        return getChildren(inputElement);
    }

    public void dispose()
    {
        this.parents = null;
    }

    public void inputChanged(Viewer viewer, Object oldInput, Object newInput)
    { /* ... */
    }
    ...
}

The class has three important methods: getChildren(Object), getParent(Object) and hasChildren(Object). The getChildren(Object) is responsible for delivering children elements to a given parent element. The getParent() delivers a parent for a given child element (e.G. when the child is selected using the editor and the viewer is requested to show the element in it). The hasChildren(Object) is a way of providing a more efficient implementation than (getChildren(element).length == 0). The getElements(Object) method is usually delegated to getChildren(Object). The difference between these methods is that getElements(Object) is called to obtain the tree viewer’s root elements, whereas getChildren(Object) is used to obtain the children of a given parent element in the tree (including a root). Since there is only one root (the not shown IWorkspaceRoot), it is OK to delegate. Finally, the parents member variable needs to be initialized on the first access, which can be done using the following code:

    /**
     * Init code for empty model
     */
    private void initializeParents(Object parentElement)
    {
        this.parents = new Parent[3];
        for (int i = 0; i < this.parents.length; i++)
        {
            this.parents[i] = new Parent("Parent " + i);
            this.parents[i].setRoot(parentElement);
            Child[] children = new Child[3];
            for (int j = 0; j < children.length; j++)
            {
                children[j] = new Child("Child " + i + j);
            }
            this.parents[i].setChildren(children);
        }
    }

Binding the content to the viewer

The last remaining piece in the puzzle is to bind the content defined in the navigation content extension to the view defined in the view extension. The extension point org.eclipse.ui.navigator.viewer is responsible for that. It consists of several parts: the viewer element, that references the id of the view and the viewerContentBinding element that one one hand specifies the view to bind the content to and on the other hand provides a reference to the content definition id.

   <extension
         point="org.eclipse.ui.navigator.viewer">
      <viewer
            viewerId="de.techjava.rcp.cnf.view">
      </viewer>
      <viewerContentBinding
            viewerId="de.techjava.rcp.cnf.view">
         <includes>
            <contentExtension
                  isRoot="false"
                  pattern="de.techjava.rcp.cnf.content.VirtualContent">
            </contentExtension>
         </includes>
      </viewerContentBinding>
   </extension>

Please note, that the content id is provided in the pattern attribute. Instead of pointing to particular content id, the regex can be used (e.G. de.techjava.rcp.cnf.content.*). That’s it, look at the result.
RCP virtual CNF
Under “Custimize View” the content tab allows to show the defined “virtual content”. Since no filters are defined, the filter section is empty.
RCP virtual CNF content selection 

Conclusion

To my opinion, for almost every application using a tree/list view to represent entities the CNF should serve as a basis for the implementation. Thus, CNF documentation should be improved and get to the level “for beginners” rather than “for experts”. In this post, I have tried to provide an introduction for non-expert RCP/PDE developers. I’m interested in your comments.

Further directions

Currently, neither filters, sorters nor actions or pop-up menu are defined for the viewer. This can be a subject of the follow-up post, if requested, but is partly covered in the articles of Michael Edler. Especially, the application of the new command framework has to be described.

Resources

Eclipse

Digital Paper Napkin by Michael D. Elder

A basic set of introduction articles written in 2006. The articles introduce contribution to the resource-based CNF viewer displaying the content of the property files. A good overview to get into the subject. The blog seems to be dead, according to many spam comments.

Eclipse from the bottom up by Michael Valenta

A good hint is to look on the implementation of the CVS Synchronize View. Pretty old content, but still valid.

vAAni by Aashish Patil

Weakly formatted article, providing some hints on how to implement a not “resource only”-based CNF viewer. Still, the information from here helped me to create a complete non-resource based CNF viewer described here.

Other

Thanks to Francis Upton, who reviewed the post and corrected some mistakes and misconceptions.

Special thanks to Michael “Mike” L. Baird for the beautiful picture of the compass. I found the picture on Google Image Search and became a big fan of his pictures (some of you know about my photo passion). It is funny that from all places in the world, the picture is shot in Morro Bay, about 200 Miles away from where I’m located now. It is also funny that Mike is a computer science guy, even if retired…