I would like to know how to render a composite component, through Java,
I mean I have:
<myowntags:selectOneRadio>
<f:selectItem itemValue="value0" itemLabel="This is the value 0" />
<f:selectItem itemValue="value1" itemLabel="This is the value 1" />
<f:selectItem itemValue="value2" itemLabel="This is the value 2" />
</myowntags:selectOneRadio>
or
<myowntags:selectOneRadio>
<f:selectItems value="#{controller.items}" />
</myowntags:selectOneRadio>
and I would like to create a Java class to render it.
I know how to render a custom component without using composite, but since, to render a component I have to specify some values on the block:
<renderer>
<component-family>javax.faces.SelectBoolean</component-family>
<renderer-type>javax.faces.Checkbox</renderer-type>
<renderer-class>com.myapp.CustomCheckboxRenderer</renderer-class>
</renderer>
then I get lost, because I don't know the values of those parameters
inside the render tag for a composite component.
Thanks in advance,
Angel.
First, you need to have a backing component which implements NamingContainer and returns "javax.faces.NamingContainer" as component family. This is required by composite components, you can't change that part. The UINamingContainer implementation already does that, so if you can just extend from it.
#FacesComponent("mySelectOneRadio")
public class MySelectOneRadio extends UINamingContainer {
// ...
}
Or if you rather want to extend from UISelectOne, then you'd have to implement the NamingContainer interface and make sure that you return UINamingContainer.COMPONENT_FAMILY in the getFamily() override.
Then, you need to specify it in <cc:interface componentType>.
<cc:interface componentType="mySelectOneRadio">
Note that at this step you can already perform the rendering (encoding) through Java. Just override the encodeChildren() method.
#FacesComponent("mySelectOneRadio")
public class MySelectOneRadio extends UINamingContainer {
#Override
public void encodeChildren(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", this);
writer.writeText("hello world", null);
writer.endElement("div");
}
}
Coming back to your concrete question, you'd thus like to have a standalone Renderer class for this. That's fine. For that you need to extend Renderer:
#FacesRenderer(componentFamily=UINamingContainer.COMPONENT_FAMILY, rendererType=MySelectOneRadioRenderer.RENDERER_TYPE)
public class MySelectOneRadioRenderer extends Renderer {
public static final String RENDERER_TYPE = "com.example.MySelectOneRadio";
#Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", component);
writer.writeText("hello world", null);
writer.endElement("div");
}
}
The backing component should be changed as follows in order to properly register this renderer as the default renderer (don't override getRendererType() method! otherwise you or anyone else would be unable to change this by <renderer> in faces-config.xml):
#FacesComponent("myComposite")
public class MyComposite extends UINamingContainer {
public MyComposite() {
// Set default renderer.
setRendererType(MySelectOneRadioRenderer.RENDERER_TYPE);
}
}
Note that thanks to the #FacesRenderer, you don't need to hassle with faces-config.xml.
Whatever way you choose to encode the children, you can get component's children just by UIComponent#getChildren(). When you're inside MySelectOneRadio component:
if (getChildCount() > 0) {
for (UICompnent child : getChildren()) {
// ...
}
}
Or when you're inside MySelectOneRadioRenderer renderer:
if (component.getChildCount() > 0) {
for (UICompnent child : component.getChildren()) {
// ...
}
}
To delegate to the component's own default rendering, invoke super.encodeChildren() or component.encodeChildren(). To delegate to child's own default rendering, invoke child.encodeAll().
See also:
What is the relationship between component family, component type and renderer type?
Related
I have a List<String> and I successfully represent it in a datatable; now I am trying to create a composite component out of it, but it seems I keep having trouble understanding how StateHelper does work.
What I want to do is, if the value attribute passed by xhtml evaluates to null, to create a new List<String> automatically. Right now, the only possible action is clicking a button that adds a new item to the list.
My component
<cc:interface componentType="testComponent">
<cc:attribute name="value" required="true" type="java.util.List"/>
</cc:interface>
<cc:implementation>
<f:event type="postAddToView" listener="#{cc.init}" />
<p:dataTable id="data" value="#{cc.data}" var="_data">
<p:column headerText="Nombre / Relación">
<h:outputText value="#{_data}" />
</p:column>
</p:dataTable>
<p:commandButton value="Añadir" process="#this" update="data"
actionListener="#{cc.addData}" ajax="true"/>
</cc:implementation>
The component bean is
#FacesComponent("testComponent")
public class TestComponent extends UIOutput implements NamingContainer {
private static final String LISTA_DATOS = "LST_DATOS";
private static final Logger log = Logger.getLogger(TestComponent.class.getName());
#Override
public String getFamily() {
return UINamingContainer.COMPONENT_FAMILY;
}
public List<String> getData() {
#SuppressWarnings("unchecked")
List<String> data = (List<String>) this.getStateHelper().get(LISTA_DATOS);
return data;
}
public void setData(List<String> data) {
this.getStateHelper().put(LISTA_DATOS, data);
}
public void addData() {
List<String> data = (List<String>)this.getData();
data.add("HOLA");
this.setData(data);
}
public void init() {
log.info("En init()");
if (this.getStateHelper().get(LISTA_DATOS) == null) {
if (this.getValue() == null) {
this.getStateHelper().put(LISTA_DATOS, new ArrayList<String>());
} else {
this.getStateHelper().put(LISTA_DATOS, this.getValue());
}
}
}
The component is called like that
<h:form>
<imas:editorTest value="#{testBean.data1}"/>
</h:form>
<h:form>
<imas:editorTest value="#{testBean.data2}"/>
</h:form>
with testBean being:
private List<String> data1 = new ArrayList<>(Arrays.asList("ONE", "TWO", "SIXTYNINE"));
private List<String> data2 = null;
public List<String> getData1() {
return this.data1;
}
public void setData1(List<String> data1) {
this.data1 = data1;
}
public List<String> getData2() {
return this.data2;
}
public void setData2(List<String> data2) {
this.data2 = data2;
}
The issue I found is that, when passing data2 (the null list), clicking the button adds a new item but only the first two times; after that, no matter how many times I click the button, no new items are added to the list (no exception shown in the log). On the opposite, there is no problem adding as many items as I wish to the component initialized with data1.
One thing that I have observed and that leads me to thinking that I am misusing getStateHelper is that, when I click the button, the init() method is executed twice and, at that time, this.getStateHelper().get(LISTA_DATOS) is null, while I expected it to be not null due to having initialized it when the component was first rendered. I expected getStateHelper to carry such state between invocations, where am I wrong?.
Oh! I am using Wildfly 8.1 (no upgrades) with JDK 7.
Digging deeper, I found some evidence of a bug, so I reported it. I'll update the answer to see if it is really a bug or some big misconception from me.
The JSF component uses binding and backing bean is of View scope. The component have validator and valueChangeListener set. And when component's value is changed partial request is sent to server. And validator and valueChangListener are called many times but not once within request.
How to make them to be called once during request?
If remove binding the methods are called correctly once.
But is it possible to do not remove binding and make listeners be called once?
Sample of used code is next:
<h:inputText id="txt"
validator="#{myBean.validateValue}"
valueChangeListener="#{myBean.valueChanged}"
binding="#{myBean.txtInput}">
<f:ajax event="valueChange"
execute="#this"
render="#this"/>
</h:inputText>
#ViewScoped
#ManagedBean
public class MyBean {
private HtmlInputText txtInput;
public void valueChanged(ValueChangeEvent ve) {
...
}
public void validateValue(FacesContext context, UIComponent component, Object value) {
...
}
public HtmlTextInput getTxtInput() {
return txtInput;
}
public void setTxtInput(HtmlTextInput txtInput) {
this.txtInput = txtInput;
}
}
The same issue takes place for actionListener and commandLink component if it uses binding and backing bean has View scope.
The possible solution could be just to override class which is used by UIInput and UICommand to store validators and listeners. The class is javax.faces.component.AttachedObjectListHolder.
This class straightforward add new listener to backing list not checking if the same listener already is there.
Thus the solution is to check if listener exists and not to add it then.
So take javax.faces.component.AttachedObjectListHolder from jsf-api-2.1.<x>-sources.jar and add it to your project to the corresponding package. Replace method add with such one:
void add(T attachedObject) {
boolean addAttachedObject = true;
if (attachedObject instanceof MethodExpressionActionListener
|| attachedObject instanceof MethodExpressionValueChangeListener
|| attachedObject instanceof MethodExpressionValidator) {
if (attachedObjects.size() > 0) {
StateHolder listener = (StateHolder) attachedObject;
FacesContext context = FacesContext.getCurrentInstance();
Object[] state = (Object[]) listener.saveState(context);
Class<? extends StateHolder> listenerClass = listener.getClass();
for (Object tempAttachedObject : attachedObjects) {
if (listenerClass.isAssignableFrom(tempAttachedObject.getClass())) {
Object[] tempState = (Object[]) ((StateHolder) tempAttachedObject).saveState(context);
if (((MethodExpression) state[0]).getExpressionString().equals(((MethodExpression)tempState[0]).getExpressionString())) {
addAttachedObject = false;
break;
}
}
}
}
}
clearInitialState();
if (addAttachedObject) {
attachedObjects.add(attachedObject);
}
}
After that validators, valueChangeListeners and actionListeners will be triggered only once even when component binding and "View", "Session" or "Application" scopes are used.
Walking further down the understanding process of the jsf 2 view scope, I am running into problems again.
There are multiple instances of my composite component's bound object created and setting values does not seem to target the "right" one.
I have the same initial setup as in Auto-instantiate session-scoped bean from view-scoped bean
Now I created a composite component:
<composite:interface componentType="helloWidget">
</composite:interface>
<composite:implementation>
<h:outputText value="Visible state of this composite component: #{cc.visibleState}"/>
</composite:implementation>
and its java counterpart
#FacesComponent(value = "helloWidget")
public class HelloWidget extends UINamingContainer implements Serializable {
private static final long serialVersionUID = 2L;
private boolean visible;
public void show() {
System.out.println("Setting visible state to true " + this);
visible = true;
}
public void hide() {
System.out.println("Setting visible state to false " + this);
visible = false;
}
public String getVisibleState() {
System.out.println("Getting visible state of " + this + "(" + visible + ")");
return String.valueOf(visible);
}
}
I then upgraded my ViewBean.java
private HelloWidget helloWidget;
private boolean visible;
public String getVisibleState() {
return String.valueOf(visible);
}
public void actionShow(ActionEvent ae) {
visible = true;
helloWidget.show();
}
public void actionHide(ActionEvent ae) {
visible = false;
helloWidget.hide();
}
public HelloWidget getHelloWidget() {
return helloWidget;
}
public void setHelloWidget(HelloWidget helloWidget) {
this.helloWidget = helloWidget;
}
and my hello.xhtml:
<f:view>
<h:form>
<h:outputText value="View-scoped bean visible value: #{viewBean.visibleState}"/>
<br/>
<mycc:helloWidget binding="#{viewBean.helloWidget}"/>
<br/>
<h:commandButton value="Show" actionListener="#{viewBean.actionShow}"/>
<h:commandButton value="Hide" actionListener="#{viewBean.actionHide}"/>
</h:form>
</f:view>
When I now hit the show / hide buttons, the value of the "visible" property in the view scoped bean changes as expected. The "visible" value of the HelloWidget property changes too, but when the page is displayed, a different HelloWidget instance is displayed, which has visible set to (default) false.
What am I doing wrong here? Is there a problem with the way I bind the composite component?
The components are instantiated during building/restoring of the view. They are essentially thus request scoped. Any state which you'd like to keep in the view scope needs to be stored in the JSF view state. The normal approach is to let the getters/setters of the properties representing the view scoped state delegate to the UIComponent#getStateHelper().
So, this should do:
#FacesComponent(value = "helloWidget")
public class HelloWidget extends UINamingContainer implements Serializable {
private static final long serialVersionUID = 2L;
public void show() {
setVisible(true);
}
public void hide() {
setVisible(false);
}
public Boolean getVisible() {
return (Boolean) getStateHelper().eval("visible");
}
public void setVisible(Boolean visible) {
getStateHelper().put("visible", visible);
}
}
You've however another potential problem not related to this issue: using binding on a view scoped bean property causes the view scoped bean itself to be recreated during every request. Use the binding attribtue with extreme care. This problem has the same grounds as already explained in JSTL in JSF2 Facelets... makes sense?.
I have situation here. I use managed bean as converter to pre populate SelectManyCheckBox from database and it works good. Here is my Converter class
#ManagedBean
#RequestScoped
public class RoleConverter implements Converter{
#EJB
private UserRoleSingleton userRoleSingleton;
#Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if(value == null){
return null;
}
return userRoleSingleton.getUserRoleById(Integer.parseInt(value));
}
#Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if(!(value instanceof Userrole) || ((Userrole) value).getRoleId() == null){
return null;
}
Userrole role = (Userrole) value;
return String.valueOf(role.getRoleId());
}
public RoleConverter() {
}
}
After it preselect checkboxes I select or deselect check boxes or do nothing and click submit button in form.
Then button's first click just executes convertor's getAsString method again and reload page.
And on second click it executes getAsObject method as expected then action.
Here is my SelectManyCheckBox:
<p:selectManyCheckbox id="orgpermission"
value="#{adminView.selectedrolesorg}" layout="pageDirection">
<f:selectItems value="#{adminView.allRolesRelatedToOrgInfo}"
var="citrole" itemValue="#{citrole}"
itemLabel="#{citrole.roledescription}" converter="#{roleConverter}" />
</p:selectManyCheckbox>
Why does it executes getAsString method again and how to solve it to fire action method on first click?
I'm using below custom component to display some kind of guidance in my page. I was asked to pass text and styleClass attributes to this component. But unfortunately styleClass attribute alone is NOT getting applied when this component gets rendered in the page. And thus I've hard coded with in my tag component. And this time style is getting applied properly. Not sure why. Can any one sugegst?
I can see styleClass attribute from css is getting passed to tag component properly. As I mentioned above component is getting rendered with out the passed style class applied.
If I hard code my style as below then it is working.
writer.writeAttribute("style", "background-color: #F1F5F2;font-size: 80%;left: 52em;position: absolute;text-align: left;width: 13.25em;z-index: 5;", null);
How is this caused and how can I solve it?
The custom component is used as follows
<mytags:guidanceBox text="#{FindPersonProps.MY_GUIDANCE_TEXT}" styleClass="guidance_css" />
The source code is below:
#FacesComponent(value="tags.guidanceBox")
public class GuidanceTag extends UIOutput {
#Override
public String getFamily() {
return "javax.faces.NamingContainer";
}
public GuidanceTag() {
}
#Override
public void encodeBegin(FacesContext context) throws IOException {
String guidanceText=(String) getAttributes().get("text");
//String styleClass=(String) getAttributes().get("styleClass");
//System.err.println("Text ["+guidanceText+"] Style ["+styleClass+"]");
ResponseWriter writer=context.getResponseWriter();
String clientId=getClientId(context);
writer.startElement("p", this);
writer.writeAttribute("style", "background-color: #F1F5F2;font-size: 80%;left: 52em;position: absolute;text-align: left;width: 13.25em;z-index: 5;", null);
writer.writeAttribute("name", clientId, "clientId");
writer.startElement("b", null);
writer.writeText("Guidance:", null);
writer.endElement("b");
writer.startElement("br", null);
writer.endElement("br");
writer.writeText(guidanceText, null);
writer.endElement("p");
}
You should implement TagHandler, that is passing the attributes to implementation of your component itself. Something like this:
public class GuidanceTagHandler extends ComponentHandler {
public GuidanceTagHandler(ComponentConfig config) {
super(config);
}
#Override
protected void onComponentCreated(FaceletContext ctx, UIComponent c, UIComponent parent) {
super.onComponentCreated(ctx, c, parent);
TagAttribute styleClassAttribute = getRequiredAttribute("styleClass");
c.getAttributes().put("styleClass", styleClassAttribute);
}
}
in taglib.xml you should have a signature of your component:
<tag>
<tag-name>guidanceTag</tag-name>
<component>
<component-type>guidance</component-type>
<handler-class>yourpackages.GuidanceTagHandler</handler-class>
</component>
</tag>
and finally in faces.config (it will bind on the component-type attribute
<component>
<component-type>guidance</component-type>
<component-class>yourpackages.GuidanceTag</component-class>
</component>
I hope this answer will help you a bit, since I was once having the same problem. So this is the solution I came up with.