It's a Chameleon! About JSF Pages that change their appearance mid-flight (or: programmatic manipulation of JSF UI Components)

One of the features in Java Server Faces that distinguishes it from for example JSP technology, is the ability to access UI Components programmatically, at run-time. This makes it possible not only to inspect the state of these components, but also to manipulate them. One advanced type of manipulation is the run-time creation of new components: reading some kind of meta-data that describes the components that are desired, our application can extend pages while the application is running (and only for the current session context). Our pages now consist of part static definitions, created using JSF JSP tags in JSP pages and part dynamically added or modified components.

In this article I will demonstrate how such manipulation can be implemented. The case here is a prototype for an Evaluation Application for our technische sessions. The session coordinator can predefine the questions he wants to put to the attendants. Based on these question-definitions, each session attendant will be presented with a web-page with all the questions and (multiple choice) answer options. This same approach can be used for Quiz and on Line Exam applications. The JSF implementation used is the standard JSR (Sun) Reference Implementation (1.1).

It's a Chameleon! About JSF Pages that change their appearance mid-flight (or: programmatic manipulation of JSF UI Components) cham1 

....

With a simple JSF JSP page, I create a UI where a Poll Question can be defined. The question text is entered in an InputText. Then we can specify whether the question requires an Open Answer – free text as typed by the user -, a single selection from a predefined set of multiple options or possibly multiple selections from that set of options. Next we can pick the widget to use for this question. Note: this is only relevant if the Answer Style is not Open Answer! We can choose between a tick-in-the-box widget (checkbox or radio button, depending on whether it is a single or multiple answer), a list or a menu style item.

The item Answer Options is where we define the various options that the user may pick his answers from. This is done in the format:

value,label
value,label

 

If we click the Rephrase Question button, we see the display of the question and allowable answers that we specified.

It's a Chameleon! About JSF Pages that change their appearance mid-flight (or: programmatic manipulation of JSF UI Components) chameleon2 

The JSF code that defines this page is fairly simple. The most important part probably is the part we cannot see in the screenprint above:

...
<body style="font-family:sans-serif" ><h:form id="form1">
<h:outputText value="Poll Picker - Compose a Poll Question"
id="outputText1"
style="font-family:sans-serif; font-size:x-large; font-weight:bolder; text-decoration:underline;"/>
<h:panelGrid columns="2" binding="#{pollPickerBacker.panelGrid1}"
id="panelGrid1">
<h:outputLabel value="Question"
id="outputLabel2" for="question"/>
<h:inputText id="question"
title="Type the question text that will be presented to the user"
size="80" value="#{pollPickerBacker.question}"/>
<h:outputLabel value="Answer Style"
id="outputLabel1"
title="Indicate the number of answers that can be given to the question"
for="answerStyle"/>
<h:selectOneRadio value="#{pollPickerBacker.answerStyle}"
id="answerStyle">
<f:selectItem itemLabel="Open Answer" itemValue="o"
id="selectItem0"/>
<f:selectItem itemLabel="Single Answer" itemValue="s"
id="selectItem1"/>
<f:selectItem itemLabel="Multiple Answers" itemValue="m"
id="selectItem2"/>
</h:selectOneRadio>
<h:outputLabel value="Widget style"
id="outputLabel3"
title="Note: not relevant when Open Answer is selected"/>
<h:selectOneRadio value="#{pollPickerBacker.widgetStyle}"
id="widgetStyle">
<f:selectItem itemLabel="Radio/Check" itemValue="tick"
id="selectItem3"/>
<f:selectItem itemLabel="List" itemValue="list"
id="selectItem4"/>
<f:selectItem itemLabel="Menu" itemValue="menu"
id="selectItem5"/>
</h:selectOneRadio>
<h:outputLabel value="Answer options"
id="outputLabel4"/>
<h:inputTextarea id="inputTextarea1" cols="40" rows="5"
value="#{pollPickerBacker.answerOptions}"/>
<h:commandButton value="Rephrase Question"
id="commandButton1" type="submit"
action="#{pollPickerBacker.rephraseQuestionAction}"/>
</h:panelGrid>
<hr/>
<h:outputText value="Could you please answer the following question:"
id="outputText2"
style="font-family:sans-serif; font-size:x-large; font-weight:bolder; text-decoration:underline;"/>
<p>
<h:messages/><h:panelGrid binding="#{pollPickerBacker.questionPanel}"
id="questionPanel" columns="2"/>
</p>
</h:form></body>
</html>
</f:view>
</jsp:root>
 

That most important definition is the PanelGrid underneath the heading "Could you please answer the following question:". This component has a binding defined: binding="#{pollPickerBacker.questionPanel}". This makes the PanelGrid accessible in the pollPickerBacker bean which in turns allows us to add newly created UI Components to it. Note: we could also have used FacesContext.getCurrentInstance().getViewRoot().findComponent() to get hold of this component instead of using the binding property. 

The Backing Bean 

The Backing Bean not only holds the values for Question, AnswerStyle, WidgetStyle and AnswerOptions, it also binds the panelGrid and implements the ActionListener method activated by the CommandButton rephrase question. The method rephraseQuestionAction on this bean is where all the action is:

    public String rephraseQuestionAction() {
Application application =
FacesContext.getCurrentInstance().getApplication();
List children = getQuestionPanel().getChildren();
children.clear();
// create ques tion text
HtmlOutput Label questionPrompt =
(HtmlOutputLabel)application.createComponent(HtmlOutputLabel.COMPONENT_TYPE);
questionPrompt.setValue("Question:");
HtmlOutputText question =
(HtmlOutputText)application.createComponent(HtmlOutputText.COMPONENT_TYPE);
question.setValue(getQuestion());
HtmlOutputLabel answerPrompt =
(HtmlOutputLabel)application.createComponent(HtmlOutputLabel.COMPONENT_TYPE);
answerPrompt.setValue("Answer:");
children.add(questionPrompt);
children.add(question);
children.add(answerPrompt);
// create answer elements (either InputText or a SelectOne or SelectMany)
UIComponent answer = null;
ValueBinding answerBinding = null;
if ("o".equalsIgnoreCase(getAnswerStyle())) {
answer = (HtmlInputText)application.createComponent(HtmlInputText.COMPONENT_TYPE);
LengthValidator lengthValidator =
(LengthValidator)application.createValidator(LengthValidator.VALIDATOR_ID);
lengthValidator.setMinimum(2); // an answer must have at least two characters
lengthValidator.setMaximum(200); // an answer must be no longer than 200 characters
((HtmlInputText)answer).addValidator(lengthValidator);
// bind the value property to the answer property in the pollPickerBacker bean
answerBinding = application.createValueBinding("#{pollPickerBacker.answer}");
} else {
if ("s".equalsIgnoreCase(getAnswerStyle())) {
if ("tick".equalsIgnoreCase(getWidgetStyle())) {
answer = (HtmlSelectOneRadio)application.createComponent(HtmlSelectOneRadio.COMPONENT_TYPE);
}
if ("list".equalsIgnoreCase(getWidgetStyle())) {
answer = (HtmlSelectOneListbox)application.createComponent(HtmlSelectOneListbox.COMPONENT_TYPE);
}
if ("menu".equalsIgnoreCase(getWidgetStyle())) {
answer = (HtmlSelectOneMenu)application.createComponent(HtmlSelectOneMenu.COMPONENT_TYPE);
}
answerBinding = application.createValueBinding("#{pollPickerBacker.answer}");
} else if ("m".equalsIgnoreCase(getAnswerStyle())) {
if ("tick".equalsIgnoreCase(getWidgetStyle())) {
answer = (HtmlSelectManyCheckbox)application.createComponent(HtmlSelectManyCheckbox.COMPONENT_TYPE);
}
if ("list".equalsIgnoreCase(getWidgetStyle())) {
answer = (HtmlSelectManyListbox)application.createComponent(HtmlSelectManyListbox.COMPONENT_TYPE);
}
if ("menu".equalsIgnoreCase(getWidgetStyle())) {
answer = (HtmlSelectManyMenu)application.createComponent(HtmlSelectManyMenu.COMPONENT_TYPE);
}
answerBinding = application.createValueBinding("#{pollPickerBacker.answers}");
}
answer.getChildren().add(getAnswerOptionItems());
}
answer.setId("answer");

answer.setValueBinding("value", answerBinding);
children.add(answer);

HtmlCommandButton submitButton =
(HtmlCommandButton)application.createComponent(HtmlCommandButton.COMPONENT_TYPE);
submitButton.setValue("Post your answer");

children.add(submitButton);
return null;
}

 

Note how we create a new ValueBinding for the expression #{pollPickerBacker.answers} for multiple answers and #{pollPickerBacker.answer} for singular answers. These bindings ensure that any value entered by the user in the answer item – being it an InputText or a RadioGroup or a List – is stored on the answer or answers property of the backing bean (and is also read from that property’s getter method when the page is rendered).

If we now for example change the question definition:

It's a Chameleon! About JSF Pages that change their appearance mid-flight (or: programmatic manipulation of JSF UI Components) chameleon3 

And click on the Rephrase Question button, the rephraseQuestionAction clears the PanelGrid, creates three OutputLabels – two for prompts, one for the question text- and adds either an InputText for an open answer, a radiogroup or select for a single answer or a set of checkboxes or a list for multiple answers:

It's a Chameleon! About JSF Pages that change their appearance mid-flight (or: programmatic manipulation of JSF UI Components) chameleon4 

Below you find the code of the utility method that takes the Answer Options textarea’s content and parses it into a UISelectItems component:

    private UISelectItems getAnswerOptionItems() {
Application application =
FacesContext.getCurrentInstance().getViewRoot().findComponent();

UISelectItems selectItems =
(UISelectItems)application.createComponent(UISelectItems.COMPONENT_TYPE);
ArrayList options = new ArrayList();
// split answerOptions on Carriage Return into label,value pairs
StringTokenizer st = new StringTokenizer (getAnswerOptions());
while (st.hasMoreTokens ()) {
// split every row into label and key
StringTokenizer st2 = new StringTokenizer (st.nextToken(),",");
options.add(new SelectItem(st2.nextToken(), st2.nextToken()));
}
selectItems.setValue(options);
return selectItems;
}
 

The importance of this functionality to manipulate the pages in our own code lies largely in the options open to us to adopt an application to user preferences and even more interestingly: to drive part of our application from run-time meta-data. In this example, the user provides the meta-data inside the application. However, it seems more meaningful to have an application administrator or even a developer define such meta-data in some sort of maintenance application and use a more extended version of our backingbean in this code example as pre-render processor.

 

Resources

I am still a happy reader and user of Java Server Faces in Action by Kito Mann (Manning Publishing).

The sources for this article can be downloaded as JDeveloper 10.1.3 Application. Note that the sources can be used in any IDE, if you configure a project yourself: Chameleon.zip.

11 Comments

  1. Slip October 7, 2011
  2. Stewart October 26, 2007
  3. Stewart October 25, 2007
  4. Heny January 9, 2007
  5. Jeanne May 17, 2006
  6. giuseppe May 16, 2006
  7. Lucas Jellema May 16, 2006
  8. giuseppe May 15, 2006
  9. Lucas Jellema May 12, 2006
  10. Lucas Jellema May 12, 2006
  11. Dmitry May 11, 2006