JSF 2.0: How to add UIComponents and their content

2019-05-18 18:24发布

问题:

I am building a custom UIComponent and adding elements (and other stock UIComponents) inside it. The component renders ok, however it cannot be found from the ViewRoot.

Let's say I have:

ResponseWriter writer;

@Override
public void encodeBegin(FacesContext context) throws IOException {
    writer = context.getResponseWriter();
    writer.startElement("div", this);
    writer.writeText("testing", null);
    writer.writeAttribute("id", getClientId(context) + ":testDiv", null);
}

@Override
public void encodeEnd(FacesContext context) throws IOException {
    writer.endElement("div");        
}

Adding that as:

<x:myUiComponent id="myComponent" />

This renders ok, however I cannot find the component or it's child div from the ViewRoot:

context.getViewRoot().findComponent("myComponent");  // returns null
context.getViewRoot().findComponent("myComponent:testDiv");  // returns null
findComponent("myComponent:testDiv");  // called within the custom component, throws java.lang.IllegalArgumentException?

The same problem applies when I try to add other UIComponents as my custom component's children - they render succesfully, however cannot be found from the component tree as my custom component itself doesn't reside there.

What's the trick here to get the components into the component tree?

Edit: Adjusted the title to better reflect the question.

回答1:

I am not sure that getClientId(context) will return myComponent. Indeed, if your component is nested in a NamingContainer component, such as a <h:form>, his ID will be prefixed with the ID of this container.

For example, if you have the following XHTML page:

<h:form id="myForm">
    <x:myUiComponent id="myComponent" />

then you will have to retrieve your component using:

context.getViewRoot().findComponent("myForm:myComponent");

Regarding the context.getViewRoot().findComponent("myComponent:testDiv"); (or context.getViewRoot().findComponent("myForm:myComponent:testDiv");, it will return null because there is no such element in the JSF components tree on server side. The code:

writer.writeAttribute("id", getClientId(context) + ":testDiv", null);

will only set the ID attribute on the HTML generated component, ie you will have a <div id="myForm:myComponent:testDiv"> in your HTML page sent to the browser. This <div> component has no existence in your JSF component tree and thus cannot be retrieved on the Java side.



回答2:

This was caused by a stupid mistake. As one would suppose, the added UIComponent does get automatically added to ViewRoot. I was, however, calling a custom UIComponent (the one I was talking about in the question) from inside my other custom UIComponent (which I din't mention because I forgot that it was there) like this:

UICodeEditor:

@Override
public void encodeAll(FacesContext context) throws IOException {
    UIEditPanel editor = new UIEditPanel();
    editor.encodeAll(context);
}

Then called this in a template like:

<!-- codeEditor is taghandler for UICodeEditor -->
<x:codeEditor />

Here, the UICodeEditor does get automatically added to the ViewRoot, however, UIEditPanel inside it does not, because I was simply calling encodeAll(context) and so the UIEditPanel has no way of knowing its' parent. A quick fix would be to manually set the parent of the child component:

editor.setParent(this);

Alternative approach (maybe better) would be to extend from UIComponentBase (as we normally do) and not manually call encodeAll(context) but just add the component as a child with getChildren.add(...) in encodeBegin(...) and not in encodeAll(...):

@Override
public void encodeBegin(FacesContext context) throws IOException {
    UIEditPanel editor = new UIEditPanel();
    getChildren().add(editor);
}

The getChildren().add() internally adds the current component as parent of the children.

Considering the location of the child creation, it might be better to build them straight in the constructor and not override the encodeXXX methods at all, if there's no need to use ResponseWriter (if there is, then you need to override those). However, it is more flexible to override and manually call encoding, whatever you need.

Also note that a custom UIComponent cannot directly be an <f:ajax render= target, because it isn't represented by a DOM element in the DOM tree. This this the same case as with composite components, where I tend to wrap the composite component implementation into a JSF managed panelGroup so that the contents can be re-rendered later on.