This tutorial explains how to use a "rich client" web UI, inside OFBiz, via the ZK Rich framework (http://potix.com). That is, a ZK frontend but an OFBiz backend.
From some comments in user ML it seems that this way of doing it is not the best one (if ever it works), please read rather
Prerequisites
- A recent OFBiz build which includes https://issues.apache.org/jira/browse/OFBIZ-528 committed. This should be any SVN tag from 533448 upwards.
- JDK 1.5, with JAVA_HOME environment variable set correctly
- Comfortable with configuring a J2EE webapp via web.xml
- A webapp (either your own or one of OFBiz's) already working, that you are happy to mess around with as a sandbox
- Comfortable with creating your own OFBiz component and using ofbiz-component.xml
- [Part 3 only] Familiarity with the Spring Framework, at least ApplicationContext and WebApplicationContext
Preparation
- Download the latest stable binary (2.3.1 at the time of this writing) of ZK from here: http://potix.com/download/
- Unzip this binary to a temporary directory, which we will call ZK_DIR
- Make a shortcut to the PDFs in ZK_DIR/doc - zk-devguide.pdf is the most useful
Installing ZK framework in your OFBiz webapp
- Copy all jars from ZK_DIR/dist/lib (but not the subdirs) onto your OFBiz classpath. See step 2 for how to do this
- Decide at what level you want ZK to be available....
- This webapp only: put them in webapp/WEB-INF/lib
- All webapps in this OFBiz component: put them somewhere relative to the root dir of this component (for instance lib/zk), then refer to them in your ofbiz-component.xml like so: <classpath type="jar" location="lib/zk/*"/>
- All of your webapps (sure?): put them somewhere on the framework classpath, for instance: OFBIZ_DIR/framework/base/lib3. Now we must register the various bits of the ZK engine in our webapp. Edit your web.xml, and without removing anything you have registered, insert the following snippets...
<!--- ZK: 1.1: servlet for ZK pages --> <servlet> <description>ZK loader for evaluating ZK pages</description> <servlet-name>zkLoader</servlet-name> <servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class> <init-param> <param-name>update-uri</param-name> <param-value>/zkau</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!--- ZK: 1.2: map.zul and .zhtml requests to this servlet --> <servlet-mapping> <servlet-name>zkLoader</servlet-name> <url-pattern>*.zul</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>zkLoader</servlet-name> <url-pattern>*.zhtml</url-pattern> </servlet-mapping> <!--- ZK: 2.1: servlet which handles client-server comms --> <servlet> <description>The asynchronous update engine for ZK</description> <servlet-name>auEngine</servlet-name> <servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>auEngine</servlet-name> <url-pattern>/zkau/*</url-pattern> </servlet-mapping> <!--- ZK: 3: make sure the browser will treat relevant file types correctly --> <mime-mapping> <extension>js</extension> <mime-type>application/x-javascript</mime-type> </mime-mapping> <mime-mapping> <extension>zhtml</extension> <mime-type>text/html</mime-type> </mime-mapping> <mime-mapping> <extension>zul</extension> <mime-type>text/html</mime-type> </mime-mapping>
4. Restart OFBiz and check that nothing nasty comes out in the logs!
Part 1 - your first ZUL page
Now we're going to make a very simple page using ZUML (the ZK User Interface Markup Language). In my firm we call these "ZULs", after the file extension, since it is much easier to say than ZUML.
1. Create a file hello.zul in the root of your webapp (the directory referenced by the location attribute of the relevant <webapp/> tag in your ofbiz-component.xml).
2. Paste the following code into it:
<?page title="Incredible Browser"?> <!-- sets browser window title --> <window title="Incredible Window"> <!-- creates a window within body --> Click here: <button label="Yes here!"/> </window>
3. Now visualize it in your browser via the obvious URL (just as if it were a JSP). So far, clicking the button does nothing so let's put in a bit of interactivity. Alter your ZUL to look like the following...
<?page title="Incredible Browser"?> <!-- sets browser window title --> <window title="Incredible Window"> <!-- creates a window within body --> <zscript> <!-- ONE --> clickCount = 0; //TWO clickIt() { clickCount++; //register the click countLabel.value = "Clicks: " + clickCount; //update the screen to show new value THREE } </zscript> Click here: <button label="Yes here!" onClick="clickIt()"/> <label id="countLabel" value="Clicks: ${clickCount}"/> <!-- FOUR --></window>
4. Refresh the page in your browser, to force the ZUL to be reinterpreted, then try clicking the button and see what happens. Now look at the lines with ONE, TWO comments and note the following interesting points....
- ONE: by default, a zscript element just contains Java beanshell code. By declaring our zscript block inside the window, we have access to all the window's child components simply by referring to the value of their id attributes.
- TWO: this variable clickCount exists within the scope of this window, until a refresh occurs
- THREE: here we refer directly to the label element below. We also use a syntactic sugar countLabel.value =, which is the same as saying countLabel.setValue()
- FOUR: the original displayed value of countLabel uses standard JSP Expression Language, to get at the value of the clickCount page-scoped variable. This only works at page interpretation time (refresh). After that, we have to use an event-handler (our clickIt() method), to dynamically access the label object and update its internal state.
5. Now go and work through the zk-quickstart.pdf tutorial
Part 2: Integration with OFBiz backend - direct
In this part, we will do something really useful for the first time - get an OFBiz entity from a list, alter it, and persist the changes. Note that I will NOT explain here, how to go one step further and use services. This is because the approach is basically identical to what I show here with a delegator - except of course you will use a dispatcher!
In this example, let's assume that our objective is to show a dropdown list of all the Person entities stored in OFBiz, and allow the user to alter their first name.
1. Lets start off by getting a delegator and using it to list all the Person entities - we will store the result in a variable. The code should be pretty self evident, it is simply beanshell, using familiar OFBiz API classes. The interesting bit is in the screen, where we use the special forEach attribute to iterate all the Persons and make a dropdown list from them. We can access fields on the each iterator variable (in ZK it is always called each) with the Expression Language DOT notation. And finally, see how we use the value attribute of each listitem, to hold a reference to the Person - not its PK but the GenericValue object itself. This will come in handy in the next step.
<?page title="Incredible Browser"?> <window title="Incredible Window"> <zscript> import org.ofbiz.entity.GenericDelegator; delegator = GenericDelegator.getGenericDelegator("default"); persons = delegator.findAll("Person"); </zscript> <listbox mold="select"> <!-- mold: select means a dropdown list --> <listitem forEach="${persons}" label="${each.firstName} ${each.lastName}" value="${each}"/> </listbox> </window>
2. Now that we can list our people, we need to have a screen for editing them. The next code sample expands upon the first by creating a little edit form but keeping it hidden until the user actually selects a person for editing. Note how we take advantage of the selected listitem's value payload - a Person entity. Also note how we use the grid component to layout the other components nicely.
<?page title="Incredible Browser"?> <window title="Incredible Window"> <zscript> import org.ofbiz.entity.GenericDelegator; delegator = GenericDelegator.getGenericDelegator("default"); persons = delegator.findAll("Person"); selectPerson(person) { //populate the edit form firstName.value = person.getString("firstName"); lastName.value = person.getString("lastName"); //and make it visible editRow.visible = true; } </zscript> <grid> <rows> <row> Select the person you wish to edit <listbox mold="select" onSelect="selectPerson(self.selectedItem.value)"> <listitem value="--- SELECT BELOW ---"/> <listitem forEach="${persons}" label="${each.firstName} ${each.lastName}" value="${each}"/> </listbox> </row> <row id="editRow" visible="false"> <textbox id="firstName"/><label id="lastName"/> </row> </rows> </grid> </window>
3. Finally, lets add a Save button and make it do something. Note how we dynamically update the person's name on the list, too. All ZK UI components can be dynamically manipulated via beanshell or Java in this way.
<?page title="Incredible Browser"?> <window title="Incredible Window" width="500px"> <zscript> import org.ofbiz.entity.GenericDelegator; delegator = GenericDelegator.getGenericDelegator("default"); persons = delegator.findAll("Person"); selectPerson(person) { //populate the edit form firstName.value = person.getString("firstName"); lastName.value = person.getString("lastName"); //and make it visible editRow.visible = true; } savePerson() { person = personList.selectedItem.value; //get the person object from the list person.set("firstName", firstName.value); //update its firstName based on the textbox person.store(); //ask the entity engine to store it personList.selectedItem.label = person.getString("firstName") + " " + person.getString("lastName"); } </zscript> <grid> <rows> <row spans="1,2"> <!-- spans distributes the colspans of the first grid row, since it only has 2 things --> Select the person you wish to edit <listbox id="personList" mold="select" onSelect="selectPerson(self.selectedItem.value)"> <listitem value="--- SELECT BELOW ---"/> <listitem forEach="${persons}" label="${each.firstName} ${each.lastName}" value="${each}"/> </listbox> </row> <row id="editRow" visible="false"> <textbox id="firstName"/><label id="lastName"/><button label="Save" onClick="savePerson()"/> </row> </rows> </grid> </window>
4. Imagine you have several pages, and they all use the delegator. It is very simple to factor out the preparation work into a separate script. By convention it has the .zs (ZScript) extension, but by default it simply contains plain ol' beanshell. In our ZUL page we then include it with another zscript tag. In the example below, my .zs is in the same directory as my ZUL, but you can put it anywhere in the webapp and reference it by a context-relative path starting with / .
setup.zs
import org.ofbiz.entity.GenericDelegator; delegator = GenericDelegator.getGenericDelegator("default");
ZUL page
<?page title="Incredible Browser"?> <window title="Incredible Window" width="500px"> <zscript src="setup.zs"/> <!-- import the script, within the scope of the window --> <zscript> persons = delegator.findAll("Person"); selectPerson(person) { //populate the edit form firstName.value = person.getString("firstName"); lastName.value = person.getString("lastName"); //and make it visible editRow.visible = true; } savePerson() { person = personList.selectedItem.value; //get the person object from the list person.set("firstName", firstName.value); //update its firstName based on the textbox person.store(); //ask the entity engine to store it personList.selectedItem.label = person.getString("firstName") + " " + person.getString("lastName"); } </zscript> <grid> <rows> <row spans="1,2"> <!-- spans distributes the colspans of the first grid row, since it only has 2 things --> Select the person you wish to edit <listbox id="personList" mold="select" onSelect="selectPerson(self.selectedItem.value)"> <listitem value="--- SELECT BELOW ---"/> <listitem forEach="${persons}" label="${each.firstName} ${each.lastName}" value="${each}"/> </listbox> </row> <row id="editRow" visible="false"> <textbox id="firstName"/><label id="lastName"/><button label="Save" onClick="savePerson()"/> </row> </rows> </grid> </window>
Part 3: Integration with OFBiz backend - via Spring
In my firm, we actually use another layer of Spring managed helper objects in between the ZUL pages and the OFBiz entity and service engines. This makes for easier unit testing, reuse, and less beanshell code in our pages.
TODO: WRITE UP THIS PART
Part 4: Considerations when using ZK in production
TODO
- Licensing
- Security
- Performance
- Memory usage
- Browser appearance (FF vs. IE)
MORE
2 Comments
Unknown User (kpodejma)
Thank you for this tutorial.
I think there should be a note about path.
When we use ContextFilter, we have to add path to allowedPaths.
Otherwise ContextFilter will redirect request to /yourapp/control/main
Larry Le
Thank you, I've test it in Zkoss 3.6.3 and it works.