http://www.techjava.de/topics/2009/04/eclipse-common-navigator-framework/#top
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
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
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. . 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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.
1
2
3
|
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:
1
2
3
4
5
6
7
8
9
|
<
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.
1
2
3
4
5
6
7
|
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 viewername
– 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 elementslabelProvider
– the labelProvider class responsible for rendering a content element in the view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<
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:
1
2
3
4
5
6
|
<
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:
1
2
3
4
5
6
|
<
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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 ofParent
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<
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.
Under “Custimize View” the content tab allows to show the defined “virtual content”. Since no filters are defined, the filter section is empty.
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.
- Part 1 – Defining the Viewer
- Part 2 – Adding Content
- Part 3 – Configuring Menus
- Part 4 – Object Contribution
- Part 5 – Action Providers
- High Level Requirement on CNF
- Further directions
- PDF Versions
- Label decorators
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…