cocoon-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Jonas Ekstedt <ekst...@ibg.uu.se>
Subject A widget framework (long)
Date Wed, 27 Oct 2004 14:21:15 GMT
Hello

I have created a widget framework that I would like to donate to Cocoon
(if you want it). I think it has several advantages to the current
Cocoon Forms framework in that it is more straightforward to use and
that it is easier to extend. I've included some documentation below or
you can read the same text better formatted at:

http://home.student.uu.se/j/joek8725/widgets.html

It is by no means finished yet but I would be very happy if you could
find some use for it.

// Jonas

------------------------------------------------------------------------

WIDGETS

"Widgets" is a widget framework for Cocoon based on the taglib block. It
grew out of an exam project that I did a few months ago.


THE PROBLEM WITH COCOON FORMS

The problem (as I see it) with Cocoon Forms is its concept of a
ViewModel (the widget tree) that sits in between the model and the view:
    Model (eg. bean) <-> ViewModel <-> View (eg. JXTemplate) 

Having a ViewModel as a buffert between the Model and the View
introduces a number of problems. First of all any changes made to the
model has to be synhronized with changes to the ViewModel (eg if a list
containg beans is expanded with one more bean then the ViewModel has to
create widgets for the added bean). Another problem is if I have several
widgets mapped to the same bean value. Then a change of value in one
widget won't update the values of the other widgets. 

Another set of problems has to do with the View. I can't use viewlogic
(eg. JXTemplate) to alter which widgets are rendered. It would be a
piece of cake to make a wizard using JXTemplate but because the
ViewModel has to know which widgets are rendered any changes to the
visibility of the widgets has to go through the ViewModel. This means
that to make a wizard (or tab-layout etc) I have to create them as
Cocoon Form widgets using Java which is cumbersome. 

Anyways, here is my take on how to make a widget framework. See the
installation notes below if you want to try it out for yourself. 


THE FRAMEWORK

I'll explain the widget framework using a calculator example. A
three-paged wizard will ask for two values (page 1 and 2) and present
the sum on the third page. First we'll code the machinery for the wizard
and afterwards add the calculator logic. 


THE PAGE OBJECT

First I'll show you the flow code and the associated JXTemplate page. 

---------------------------------------------------------------
cocoon.load("resource://widgets/flow/Page.js");

function public_calculator() {

    var page = new Page();
    var calculatorHandler = new CalculatorHandler();
    page.setEventHandler(calculatorHandler);
    page.show("calculator.jx", {}, {wizard: "term1"});
    cocoon.sendPage("main.html");
}


function CalculatorHandler() {
}

CalculatorHandler.prototype.action_previous = function(requestProcessor, models, bizData)
{
    if (bizData["wizard"] == "term2") {
	bizData["wizard"] = "term1";
    } else if (bizData["wizard"] == "sum") {
	bizData["wizard"] = "term2";
    }
    return true;
}

CalculatorHandler.prototype.action_next = function(requestProcessor, models, bizData) {
    if (bizData["wizard"] == "term1") {
      bizData["wizard"] = "term2";
    } else if (bizData["wizard"] == "term2") {
      bizData["wizard"] = "sum";
    }
    return true;
}

CalculatorHandler.prototype.action_exit = function(requestProcessor, models, bizData) {
    return false;
}
---------------------------------------------------

---------------------------------------------------
<?xml version="1.0"?>

<html xmlns:jx="http://apache.org/cocoon/templates/jx/1.0"
  xmlns:wt="urn:widgets"
  xmlns:wi="http://widget/instance"
  >

  <body>
    <h1>Calculator wizard</h1>
    <jx:if test="${wizard == 'term1'}">
      Page term1
      <a href="${continuation.id}.kont?cocoon-event=action:next">Next</a>
    </jx:if>

    <jx:if test="${wizard == 'term2'}">
      Page term2
      <a href="${continuation.id}.kont?cocoon-event=action:previous">Previous</a>
      <a href="${continuation.id}.kont?cocoon-event=action:next">Next</a>
    </jx:if>

    <jx:if test="${wizard == 'sum'}">
      Page sum
      <a href="${continuation.id}.kont?cocoon-event=action:previous">Previous</a>
    </jx:if>
    <a href="${continuation.id}.kont?cocoon-event=action:exit">Exit</a>
  </body>
</html>
---------------------------------------------------

The JXTemplate is nothing out of the ordinary. It will simply hide the
pages of the wizard that are not active (as set in ${wizard}). The flow
however needs a bit of explaining. First, we create a Page object. To
this Page object we add an event handler. The functions of the event
handler are called depending on what events are triggered by the user.
Clicking a "Next" link will trigger the next continuation with the
request parameter "cocoon-event" set to "action:next". The Page object
will respond by invoking the action_next function of the event handler.
The Page object will continue showing the page as long as the event
handler function returns true. 

As you can see in the flow above a "next" event will check which wizard
is currently active and set bizData["wizard"] accordingly. 

Here is the code for the Page object. I'll discuss it further later on. 

---------------------------------------------------
function Page() {
}

Page.prototype.setEventHandler = function(eh) {
    this.eventHandler = eh;
    this.eventHandler.page = this;
}

Page.prototype.show = function(uri, models, bizData) {

    if (! bizData) {
	var bizData = {};
    }

    while (1) {
	for (key in models) {
	    cocoon.request.setAttribute(key, models[key]);
	}

	var requestProcessor = Packages.widgets.flow.RequestProcessor.getInstance(cocoon.request);
	cocoon.sendPageAndWait(uri, bizData);
	requestProcessor.process(cocoon.request);

	if (this.eventHandler == undefined) {
	    return;
	}

	var proceed = this.eventHandler["action_" + requestProcessor.event.action](requestProcessor,
models, bizData);

	if (! proceed) {
	    return;
	}
    }
}
---------------------------------------------------
		  
ADDING THE MODEL

Lets turn to how we should represent the state of our calculator. We
need to store two terms and supply logic to present the sum. We will use
a bean for this. 

---------------------------------------------------
package widgets.samples;

public class Calculator {

    String term1;
    String term2;

    public String getTerm1() {
	return term1;
    }

    public void setTerm1(String term1) {
	this.term1 = term1;
    }

    public String getTerm2() {
	return term2;
    }

    public void setTerm2(String term2) {
	this.term2 = term2;
    }

    public String getSum() {
	return String.valueOf(Integer.parseInt(term1) + Integer.parseInt(term2));
    }

}
---------------------------------------------------
		  
Note that to simplify matters we represent the terms as StringS and then
only convert them when we present the sum. 

The widget framework introduces the concept of Models. A Model is a
wrapper around your data so that the View can access it in a uniform
way. Currently there are four different type of Models: 

* BeanModel: wraps a POJO
* DOMModel: wraps a DOM
* MapModel: wraps a java.util.Map
* ResultSetModel: wraps a database query

As we decided to represent the calculator as a bean we will choose to
wrap it in a BeanModel. 

---------------------------------------------------
importClass(Packages.widgets.model.BeanModel);
importClass(Packages.widgets.samples.Calculator);
cocoon.load("resource://widgets/flow/Page.js");

function public_calculator() {

    var calculator = new Calculator();
    var calculatorModel = new BeanModel(calculator);
    var page = new Page();
    var calculatorHandler = new CalculatorHandler();
    page.setEventHandler(calculatorHandler);
    page.show("calculator.jx", {calculator: calculatorModel}, {wizard: "term1"});
    cocoon.sendPage("main.html");
}
---------------------------------------------------

Note that we send in the model to the page.show() function. The key
("calculator") of the model in the map will be used in the view to
uniquely identify which model is referenced. It will also be used when
we populate the model with values from the request. 

In order to show the model in the view we change the JXTemplate: 

---------------------------------------------------
<?xml version="1.0"?>

<html xmlns:jx="http://apache.org/cocoon/templates/jx/1.0"
  xmlns:wt="urn:widgets"
  xmlns:wi="http://widget/instance"
  >

  <body>
    <h1>Calculator wizard</h1>
    <form action="${continuation.id}.kont" method="post">
      <wt:model id="calculator">
	<jx:if test="${wizard == 'term1'}">
	  <p>
	    <b>Term 1:</b>
	    <wt:field type="text" attribute="term1"/>
	  </p>
	  <p>
	    <wt:form-button action="next">Next</wt:form-button>
	  </p>
	</jx:if>

	<jx:if test="${wizard == 'term2'}">
	  <p>
	    <b>Term 2:</b>
	    <wt:field type="text" attribute="term2"/>
	  </p>
	  <p>
	    <wt:form-button action="previous">Previous</wt:form-button>
	    <wt:form-button action="next">Next</wt:form-button>
	  </p>
	</jx:if>

	<jx:if test="${wizard == 'sum'}">
	  <p>
	    <b>Sum:</b>
	    <wt:out attribute="sum"/>
	  </p>
	  <p>
	    <wt:form-button action="previous">Previous</wt:form-button>
	  </p>
	</jx:if>
      </wt:model>
      <a href="${continuation.id}.kont?cocoon-event=action:exit">Exit</a>
    </form>
  </body>
</html>
---------------------------------------------------

The wt:model tag is used to setup the context that subsequent widget
tags will operate on. In this case we identify the model as "calculator"
(which is the key we used in the page.show() method call. This means
that any descendant tags will look up their values in our calculator
bean. It is possible to supply the view with several models. 

When the wt:field tag is executed (during taglib transformation) it will
fetch the value of attribute "term1" from the containing model (in this
case the calculator bean). Subsequent xslt transformation will turn it
into a html input text tag. 

The wt:form-button will translate into a html button element. The name
of the buttons will be "cocoon-event" and its value will be set to eg.
"action:next" so that a button press will trigger the event handling in
the Page object. 

Contrary to Cocoon Forms we can choose at render time the type of widget
used to show model values. In Cocoon Forms it is very difficult to
change how we represent data, eg. once you have decided that term1
should be rendered as a text field you cannot change that very easy.
Here we can use a text field, a select box or whatever to render term1,
we can even render the value of term1 several times using different
types of widgets. 

The flow now needs a few changes to cope with population of the bean
from the request. But first I'll talk a bit about the RequestProcessor. 


THE REQUESTPROCESSOR

Everytime the Page object show a page it will create a RequestProcessor.
All widgets that represents some sort of user input (eg. fields, select
boxes etc.) will register the expected request parameter name with the
RequestProcessor. So that when the form is submitted the
RequestProcessor knows which widgets has been rendered and can build a
population map based on that. That means that when we populate the beans
only the data that we have specifically rendered as form widgets will be
eligble for population. Because of this we can use JXTemplate (as in the
example above) to selectively hide widgets without messing up the
population procedure. 

Below is the changed flow that now populates the widget model everytime
an event is triggered. 

---------------------------------------------------
importClass(Packages.widgets.model.BeanModel);
importClass(Packages.widgets.samples.Calculator);

cocoon.load("resource://widgets/flow/Page.js");

function public_calculator() {

    var calculator = new Calculator();
    var calculatorModel = new BeanModel(calculator);
    var calculatorHandler = new CalculatorHandler();

    var page = new Page();
    page.setEventHandler(calculatorHandler);

    page.show("calculator.jx", {calculator: calculatorModel}, {wizard: "term1"});

    cocoon.sendPage("main.html");
}


function CalculatorHandler() {
}

CalculatorHandler.prototype.action_previous = function(requestProcessor, models, bizData)
{
    models["calculator"].populate(requestProcessor.getParameters("calculator:"));
    if (bizData["wizard"] == "term2") {
	bizData["wizard"] = "term1";
    } else if (bizData["wizard"] == "sum") {
	bizData["wizard"] = "term2";
    }
    return true;
}

CalculatorHandler.prototype.action_next = function(requestProcessor, models, bizData) {
    models["calculator"].populate(requestProcessor.getParameters("calculator:"));

    if (bizData["wizard"] == "term1") {
      bizData["wizard"] = "term2";
    } else if (bizData["wizard"] == "term2") {
      bizData["wizard"] = "sum";
    }
    return true;
}

CalculatorHandler.prototype.action_exit = function(requestProcessor, models, bizData) {
    return false;
}
---------------------------------------------------

The population is handled by the populate() function. Note how we call
the RequestProcessor to get the parameters. In this specific case we use
the getParameters(String prefix) function that will filter out any
parameters that does not start with prefix. The prefix "calculator:" is
added by the field tag when it renders the field widget so that we can
differentiate between parameters that belong to different models. 

In our case we would see request parameters named "calculator:term1" and
"calculator:term2" depending on which wizard page was submitted. 

Now we have a functioning calculator. The only thing missing is
validation. We need to be certain that the user enters numbers and not
characters. 


VALIDATION

A very simple calculator validator can look like this: 

---------------------------------------------------
package widgets.samples;

import org.apache.commons.validator.GenericValidator;
import widgets.model.BeanModel;

public class CalculatorValidator {

    public boolean validatePhase1(BeanModel model) {

	Calculator calculator = (Calculator) model.getBean();

	if (! GenericValidator.isInt(calculator.getTerm1())) {
	    model.setError("term1", "Value must be an integer");
	    return false;
	}
	return true;
    }

    public boolean validatePhase2(BeanModel model) {

	Calculator calculator = (Calculator) model.getBean();

	if (! GenericValidator.isInt(calculator.getTerm2())) {
	    model.setError("term2", "Value must be an integer");
	    return false;
	}
	return true;
    }
}
---------------------------------------------------

It will check if the values are indeed integer and if not add errors to
the model. Every type of model supports setting userdata on the specific
nodes of the model (cf Node.setUserdata() in DOM). A special case of
this is error messages. 

In the view an error message widget will check if errors exist for the
specified attribute and if so print it out. Below is the view again with
the added error widgets. 

---------------------------------------------------
<?xml version="1.0"?>

<html xmlns:jx="http://apache.org/cocoon/templates/jx/1.0"
  xmlns:wt="urn:widgets"
  xmlns:wi="http://widget/instance"
  >

  <body>
    <h1>Calculator wizard</h1>
    <form action="${continuation.id}.kont" method="post">
      <wt:model id="calculator">
	<jx:if test="${wizard == 'term1'}">
	  <p><wt:error attribute="term1"/></p>
	  <p>
	    <b>Term 1:</b>
	    <wt:field type="text" attribute="term1"/>
	  </p>
	  <p>
	    <wt:form-button action="next">Next</wt:form-button>
	  </p>
	</jx:if>

	<jx:if test="${wizard == 'term2'}">
	  <p><wt:error attribute="term2"/></p>
	  <p>
	    <b>Term 2:</b>
	    <wt:field type="text" attribute="term2"/>
	  </p>
	  <p>
	    <wt:form-button action="previous">Previous</wt:form-button>
	    <wt:form-button action="next">Next</wt:form-button>
	  </p>
	</jx:if>

	<jx:if test="${wizard == 'sum'}">
	  <p>
	    <b>Sum:</b>
	    <wt:out attribute="sum"/>
	  </p>
	  <p>
	    <wt:form-button action="previous">Previous</wt:form-button>
	  </p>
	</jx:if>
      </wt:model>
      <p>
	<wt:button uri="${continuation.id}.kont" action="exit">Exit</wt:button>
      </p>
    </form>
  </body>
</html>
---------------------------------------------------

The flow is changed so that an if an erroneous value was entered it will
refuse to proceed to the next wizard page. We do this by creating the
validator, adding it to our eventhandler and then invoke it everytime a
"next" event is received. 

---------------------------------------------------
importClass(Packages.widgets.model.BeanModel);
importClass(Packages.widgets.samples.Calculator);
importClass(Packages.widgets.samples.CalculatorValidator);

cocoon.load("resource://widgets/flow/Page.js");

function public_calculator() {

    var calculator = new Calculator();
    var calculatorModel = new BeanModel(calculator);
    var calculatorHandler = new CalculatorHandler();
    calculatorHandler.validator = new CalculatorValidator();

    var page = new Page();
    page.setEventHandler(calculatorHandler);

    page.show("calculator.jx", {calculator: calculatorModel}, {wizard: "term1"});

    cocoon.sendPage("main.html");
}


function CalculatorHandler() {
}

CalculatorHandler.prototype.action_previous = function(requestProcessor, models, bizData)
{
    models["calculator"].populate(requestProcessor.getParameters("calculator:"));
    if (bizData["wizard"] == "term2") {
	bizData["wizard"] = "term1";
    } else if (bizData["wizard"] == "sum") {
	bizData["wizard"] = "term2";
    }
    return true;
}

CalculatorHandler.prototype.action_next = function(requestProcessor, models, bizData) {
    models["calculator"].clearErrors();
    models["calculator"].populate(requestProcessor.getParameters("calculator:"));

    if (bizData["wizard"] == "term1") {
	var valid = this.validator.validatePhase1(models["calculator"]);
	if (valid) {
	    bizData["wizard"] = "term2";
	}
    } else if (bizData["wizard"] == "term2") {
	var valid = this.validator.validatePhase2(models["calculator"]);
	if (valid) {
	    bizData["wizard"] = "sum";
	}
    }
    return true;
}

CalculatorHandler.prototype.action_exit = function(requestProcessor, models, bizData) {
    return false;
}
---------------------------------------------------
		  
SAMPLES

I have provided a few samples of how to use the widget framework. 

Login sample
Uses the MapModel to implement a login page. A validator makes sure that
the user exist and has provided the correct password 

Calculator
Described in detail above 

Personnel Database
Use the ResultSetModel to edit information in a database. It also shows
some advanced uses of JXTemplate. 

Personnel DOM
Similar to above but instead uses a DOM as backend. 




-- 
---------------------------------------------------------------
Address  :   Rackarbergsg 74, 752 32 UPPSALA
Phone    :   018-50 69 28, 0768-767 747



Mime
View raw message