Writing your own Component Peers Tutorials - Part 1
Written by Brad Baker
This is the first article in a series of articles on writing your own component peers. They are based on my experiences while creating the EchoPointNG library of components. The other articles are can be found here :
WritingYourOwnComponentPeersPart1
The Echo2 framework comes with a comprehensive set of components that will help you build web applications. However you may want to add new components beyond what comes in the base framework.
One way to create new components is to aggregate simple components. For example you could create a LoginPanel component that is an aggregation of the Grid, TextField and Button component. You could add internal document listeners and action listeners to make it act as one component.
But what if you want a component that doesn't exist or you want to change significantly the way a component works. To do this you must write your own component rendering peer.
I created the EchoPointNG library for exactly this reason, I wanted to contribute components above and beyond the ones in the base framework, components such as Tree, ExpandableSection, Menu and ComboBox.
In order to do this I had to create the components themselves, derived from nextapp.echo2.app.Component, as well as their respective rendering peers.
This tutorial is designed to help people get started in building their own components and rendering peers.
The first part discusses the general framework support for adding your own components and rendering peers and the second part drills down further into the actual code mechanics you need to provide.
I will use existing Echo2 and EPNG components as examples where I can.
I also suggest as a starting point you read the Echo2 Tutorials to make sure you understand the basics of the Echo2 framework.
http://nextapp.com/platform/echo2/echo/doc/tutorial/
You should then should read the Echo2 Technical Overview to understand how the Echo2 framework handles client and server synchronisation. You MUST understand this in order to write a well behaved component.
http://echo.nextapp.com/site/echo2/doc/tov
Now finally you must be familair with the w3c Document Object Model (DOM). This is used both on the server side (via standard org.w3c.dom interfaces) and on the client side via the JavaScript DOM bindings.
http://www.w3.org/DOM/
http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/
http://www.w3schools.com/dom/default.asp
OK now that you have read these documents lets get down to brass tacks.
A Simple Example
Lets create a very simple component called RulerLine. This will allow a horizontal ruler line to be drawn on the client. Note this is a very simple component because it has no input or special state to manage but it will get us started. Here it is as a starting point.
1 /**
2 * <code>RulerLine</code> is a very simple component
3 */
4 public class RulerLine extends Component {
5
6 }
Now it might seem we have a nothing piece of code here, but in fact we have a component that supports background and foreground colors, its own font and can be part of the Echo2 component hirearchy.
Component to Rendering Peer mapping
Echo2 uses a neat Java resources trick to map a component to a rendering peer. When it has a component to render, it looks up all instances of a special properties file and then looks into these instances for component class to rendering peer class mapppings.
The name of the properties file is :
/META-INF/nextapp/echo2/SynchronizePeerBindings.properties
You simply put this file into the root directory of your JAR file and the Echo2 will find it by name and look in it to find the required mappings. For example the EPNG file looks like a bit like this.
echopointng.BalloonHelp echopointng.ui.syncpeer.BalloonHelpPeer echopointng.ButtonEx echopointng.ui.syncpeer.ButtonExPeer echopointng.CheckBoxEx echopointng.ui.syncpeer.ToggleButtonExPeer echopointng.ComboBox echopointng.ui.syncpeer.ComboBoxPeer echopointng.ContainerEx echopointng.ui.syncpeer.ContainerExPeer echopointng.DateChooser echopointng.ui.syncpeer.DateChooserPeer echopointng.ExpandableSection echopointng.ui.syncpeer.ExpandableSectionPeer ...
From this little example, you can see that the echopoint.BalloonHelp component is mapped to the echopointng.ui.syncpeer.BalloonHelpPeer rendering peer.
You would create an instance a file called /META-INF/nextapp/echo2/SynchronizePeerBindings.properties in your JAR file and place the following in it
example.RulerLine example.ui.RulerLinePeer
This tells the Echo2 framework that the RulerLine component is rendered by the RulerLinePeer rendering peer.
Rendering Peers
Echo2 rendering peers are meant to be stateless. A single instance of a rendering peer is used for ALL components that are mapped to it. This helps reduce the memory footprint of the Echo2 web application. This also means you have to be careful when writing your rendering peer to make sure you don't set instance variables etc..
The rendering peer is responsible for "rendering" XML and more commonly XHTML content into special XML messages that are sent down to the Echo2 browser client AJAX engine.
The client side AJAX engine knows how to interpet many of these messages and perform actions such as DOM removal/addition and so on.
I won't explain the structure of the XML message because I have rarely needed to know it. Echo2 does such a good job of "encapsulating" this into Java API calls that you never see the XML sent back and forth. Rather, everything marshals back and forth into Java objects. It's unreal and very Java oriented!
You can see the XML messages being sent back and forth if you append ''debug'' as a parameter to the URL of your Echo2 app. This opens a debug panel that shows all server communications in real time. For example http://demo.nextapp.com/Email/app?debug will show the Echo2 Email demo app with the debug panel in action.
http://demo.nextapp.com/Email/app?debug
Adding XHTML to the client DOM
A rendering peer must implement a special interface called
nextapp.echo2.webcontainer.ComponentSynchronizePeer
.
The methods of this interface will be called during different lifecycle stages of the rendering process.
ComponentSynchronizePeer.renderAdd is called to add the XHTML representation of the component onto the browser client. There are two ways to do this but we will concentrate on the most common which is DOM addition.
There is a concept of a purely client side rendered component. However these are an advanced concept that we wont cover in this part of the tutorial
Echo2 has a series of features that allow you to add XHTML content to the Document Object Model (DOM) on the browser client.
If your rendering peer uses this mechanism to add XHTML to the server (and most do) then you must also support another special interface called nextapp.echo2.webcontainer.DomUpdateSupport.
It has a single method called renderHtml that can be called to render the XHTML that will be sent down the browser client and inserted into the DOM there.
Here is that code
1 public void renderHtml(RenderContext rc, ServerComponentUpdate update, Node parentNode, Component component) {
2 Document doc = rc.getServerMessage().getDocument();
3 Element hrE = doc.createElement("hr");
4 hrE.setAttribute("id",ContainerInstance.getElementId(component));
5 parentNode.appendChild(hrE);
6 }
This trivial rendering peer simply creates a org.w3c.dom.Element instance with the tag name hr. It then sets the id of the XHTMl tag with a unique value that identifies this component. The net result of all this is that the following XHTML is generated
<hr id="c_1234" />
However a lot happens under the covers. The RenderContext and ServerComponentUpdate objects hold a bunch of context information that may be important to your component peer. They allow you to do tricks like partial updates and so forth. To keep things simple we don't use them yet.
Now lets have a look at our renderAdd method. This is where the real magic happens.
1 public void renderAdd(RenderContext rc, ServerComponentUpdate update, String targetId, Component component) {
2 Element domAddElement = DomUpdate.renderElementAdd(rc.getServerMessage());
3 DocumentFragment htmlFragment = rc.getServerMessage().getDocument().createDocumentFragment();
4 renderHtml(rc, update, htmlFragment, component);
5 DomUpdate.renderElementAddContent(rc.getServerMessage(), domAddElement, targetId, htmlFragment);
6 }
7
What this does is create a special XML message directive that informs the Echo2 client side engine that we want to add some XHTML to the DOM.
This is done via this Java code.
Element domAddElement = DomUpdate.renderElementAdd(rc.getServerMessage());
If you look deeper in the DomUpdate class you will see how it creates a special section in the XML document that will be interpreted at the client browser by the Echo2 AJAX engine.
Then this code is called
DocumentFragment htmlFragment = rc.getServerMessage().getDocument().createDocumentFragment(); renderHtml(rc, update, htmlFragment, component);
The first line create a special holder object called a DocumentFragment in w3c DOM. This is a special placeholder object that generates no XML but allows for the parent/child relationship inside a DOM structure.
The second line calls our renderHtml method using the document fragment as the parent node. The <hr id="c_1234" /> XHTML would be generated.
DomUpdate.renderElementAddContent(rc.getServerMessage(), domAddElement, targetId, htmlFragment);
The last line adds the generated XHTML into the XML message that will be sent down to the Echo2 browser client AJAX engine.
Echo2 will "lump" together a series of component updates and put them into the one XML message to be sent to the browser client. This is done for performance reasons. For example a button press event might result in 10 components being changed. All 10 updates will be sent back into the one XML server/client interaction.
When the Echo2 AJAX client engine gets this XML message it will "add" our XHTML into the right place within the client DOM.
Updating XHTML on the client DOM
Lets say some property changes on our RulerLine component. Echo2 knows the component is currently represented on the browser client so it won't add it again. Instead it will detect this property change and call the renderUpdate method.
We then have a chance to update the representation of the component on the client. However because we are a simple component we won't get fancy with "partial updates" but rather we will remove ourselves and re-add ourselves. This is our very simple "update" mechanism. Later we will add support for partial updates.
Lets see our method
1 public boolean renderUpdate(RenderContext rc, ServerComponentUpdate update, String targetId) {
2 DomUpdate.renderElementRemove(rc.getServerMessage(), ContainerInstance.getElementId(update.getParent()));
3 renderAdd(rc, update, targetId, update.getParent());
4 return false;
5 }
6
The first line creates an XML directive message that will be used to tell the client side AJAX engine to remove a certain DOM element by id.
Now if you are clever you will notice that the id here is not the id of our RulerLine component but rather a component returned via update.getParent().
Well actually thats not right. The getParent() method is a bit of a misnomer. The parent in this case is the component in question (RulerLine) and its called parent because it might have changed children components. I personally would have preferred it called getTargetComponent() or something like that so it's less confusing.
So what we see is that the element with our id is to be removed from the DOM.
The second line calls the renderAdd() method again to add the new XHTML for the RulerLine because it will have been removed. This is the simplest way of updating the client side representation of components in Echo2, ie use an remove/re-add strategy.
The Echo2 framework is very careful about the order in which it processes these XML message directives. Removes are always done before adds.
Removing XHTML on the client DOM
It turns out for our very simple component rendering peers, you don't have to do anything. Echo2 knows the id of the component in the DOM and hence can "remove it" by sending down an XML directive message for you.
However if you need to clean anything up it does invoke the renderDispose() method. In our case here we do nothing however you can use it to send down special XML directives to clean up client side resources like event handlers and so on.
1 public void renderDispose(RenderContext rc, ServerComponentUpdate update, Component component) {
2 // we dont have anything on dispose of per see
3 }
Here are all the files in total
1 package example;
2
3 import nextapp.echo2.app.Component;
4
5 /**
6 * <code>RulerLine</code> is a very simple component
7 */
8 public class RulerLine extends Component {
9
10 }
11
1 package example.ui;
2
3 import nextapp.echo2.app.Component;
4 import nextapp.echo2.app.update.ServerComponentUpdate;
5 import nextapp.echo2.webcontainer.ComponentSynchronizePeer;
6 import nextapp.echo2.webcontainer.ContainerInstance;
7 import nextapp.echo2.webcontainer.DomUpdateSupport;
8 import nextapp.echo2.webcontainer.RenderContext;
9 import nextapp.echo2.webrender.servermessage.DomUpdate;
10
11 import org.w3c.dom.Document;
12 import org.w3c.dom.DocumentFragment;
13 import org.w3c.dom.Element;
14 import org.w3c.dom.Node;
15
16 /**
17 * <code>RulerLinePeer</code>
18 */
19
20 public class RulerLinePeer implements ComponentSynchronizePeer, DomUpdateSupport {
21 /**
22 * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#getContainerId(nextapp.echo2.app.Component)
23 */
24 public String getContainerId(Component child) {
25 throw new IllegalStateException("RulerLinePeer does not work as a DOM container");
26 }
27
28 /**
29 * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderAdd(nextapp.echo2.webcontainer.RenderContext,
30 * nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String,
31 * nextapp.echo2.app.Component)
32 */
33 public void renderAdd(RenderContext rc, ServerComponentUpdate update, String targetId, Component component) {
34 Element domAddElement = DomUpdate.renderElementAdd(rc.getServerMessage());
35 DocumentFragment htmlFragment = rc.getServerMessage().getDocument().createDocumentFragment();
36 renderHtml(rc, update, htmlFragment, component);
37 DomUpdate.renderElementAddContent(rc.getServerMessage(), domAddElement, targetId, htmlFragment);
38 }
39
40 /**
41 * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderDispose(nextapp.echo2.webcontainer.RenderContext,
42 * nextapp.echo2.app.update.ServerComponentUpdate,
43 * nextapp.echo2.app.Component)
44 */
45 public void renderDispose(RenderContext rc, ServerComponentUpdate update, Component component) {
46 // we dont have anything to dispose of per see
47 }
48
49 /**
50 * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderUpdate(nextapp.echo2.webcontainer.RenderContext,
51 * nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String)
52 */
53 public boolean renderUpdate(RenderContext rc, ServerComponentUpdate update, String targetId) {
54 DomUpdate.renderElementRemove(rc.getServerMessage(), ContainerInstance.getElementId(update.getParent()));
55 renderAdd(rc, update, targetId, update.getParent());
56 return false;
57 }
58
59 /**
60 * @see nextapp.echo2.webcontainer.DomUpdateSupport#renderHtml(nextapp.echo2.webcontainer.RenderContext,
61 * nextapp.echo2.app.update.ServerComponentUpdate, org.w3c.dom.Node,
62 * nextapp.echo2.app.Component)
63 */
64 public void renderHtml(RenderContext rc, ServerComponentUpdate update, Node parentNode, Component component) {
65 Document doc = rc.getServerMessage().getDocument();
66 Element hrE = doc.createElement("hr");
67 hrE.setAttribute("id",ContainerInstance.getElementId(component));
68 parentNode.appendChild(hrE);
69 }
70 }
71
72