I have a form with a variable number of input elements, like this:
<ui:repeat var="_lang" value="#{myBean.languages}">
<h:inputTextarea value="${_lang.title}" id="theTitle" />
<h:messages for="theTitle"/>
</ui:repeat>
When a certain method in the backing bean is triggered, I want to add a message to, say, the second iteration of the ui:repeat
, but not the other ones.
I've seen different variations of this question around here, and all problems appear to be due to the ui:repeat
's iterations not being available in the JSF component tree.
What I have tried so far:
Bind the
h:inputTextarea
s to aMap<String,UIComponent>
in the bean. (a) ...Using...binding="#{myBean.uiMap[_lang.id]}"
(where_lang.id
is a unique String). This produced JBWEB006017: Target Unreachable, ''BracketSuffix'' returned null. (I dumped a corresponding map of strings using the ids, the same syntax works just fine outside ofbinding
) (b) ...or using...binding="#{myBean.uiMap.get()}"
. This renders the page fine, but when I push the button for my method, the setter does not get called, and thus theUIComponent
s never get added to theMap
.Bind the
h:inputTextarea
s to an arrayUIComponent[]
in the bean, prepopulating it with the right number of nulls and then using the row counter ofui:repeat
as an index in the xhtml file. Got Null Pointer exceptions, the setter of the array was never called and thus the array was never populated with the actualUIComponent
s.Bind an outer
h:panelGroup
to the bean and try to find the input elements recursively amongst its children in the JSF tree. Only one of the inputs is ever found, see the "iterations not available" problem above.I also tried replacing the
ui:repeat
withc:forEach
and generate row numbers manually (so that they'd hopefully be available in the JSF tree), but there I didn't get any rendered output at all.
(Note: The goal is to display a validation error message, but they have to come from the backing bean. Using an f:validator
or the likes, even custom ones, is not really an option because I need to validate against data in the backing bean.)
Frankly, I'm out of ideas. This can't be so difficult, can it?
Edit:
For my third attempt, binding to an outer h:panelGroup
, here's my JSF finder function:
private List<UIComponent> findTitleComponents(UIComponent node) {
List<UIComponent> found = new ArrayList<UIComponent>();
for (UIComponent child : node.getChildren()) {
if (child.getId().equals("theTitle")) {
found.add(child);
log.debug("have found "+child.getClientId());
} else {
found.addAll(findTitleComponents(child));
log.debug("recursion into "+child.getClientId());
}
}
return found;
}
I'm calling this on node
, which is the binding UIComponent
of the h:panelGroup
around the ui:repeat
. (I'm using recursion because my live application has a slightly more nested structure) This, I thought, should give me all "theTitle" textareas, so that I then could add messages and read attributes as I pleased. Alas, the method only returns one "theTitle" component, and the log messages show why:
In the DOM of the generated page, the ids are like "myform:myPanelGroup:0:theTitle" (including the iteration counter of the ui:repeat
) while the bean only sees getClientId()s like myform:myPanelGroup:theTitle
- and that only exist once, for the last (I guess?) iteration.
Your attempts to bind the input component to a map/array failed because there are not multiple of those components in the JSF component tree, but only one. The <ui:repeat>
doesn't run during view build time producing the JSF component tree. Instead, it runs during view render time producing the HTML output. In other words, the child components of <ui:repeat>
are reused everytime during generating HTML output of each iteration.
The particular exception, "Target Unreachable, ''BracketSuffix'' returned null" is been thrown because the variable #{_lang}
isn't available during view build time, that moment when the UI component tree is constructed and all id
and binding
attributes are evaluated. It's only available during view render time.
Those binding attempts would have succeeded if you used <c:forEach>
instead. It runs during view build time producing the JSF component tree. You would then end up with physically multiple instances of the child components which in turn produce each their own HTML output without being reused multiple times.
Putting in a panel group and attempting to find all children obviously won't work for the reasons mentioned before. The <ui:repeat>
doesn't generate physically multiple JSF components in the component tree. Instead, it reuses the same component to produce the HTML output multiple times depending on the state of the current iteration round.
Replacing by <c:forEach>
should have worked. Perhaps you were facing a timing issue because it runs during view build time and you're preparing the model during e.g. preRenderView
instead of @PostConstruct
or so.
All of above is easier to understand if you carefully read JSTL in JSF2 Facelets... makes sense?
As to your concrete functional requirement, you would normally use a Validator
for the job. If you register it on the input component, then it would be invoked for every iteration round. You would immediately have the right input component with the right state at hands as 2nd argument of validate()
method and the submitted/converted value as 3rd argument.
If you really need to perform the job afterwards, for example because you need to know about all inputs, then you should be programmatically iterating over the <ui:repeat>
yourself. You can do that with help of UIComponent#visitTree()
this allows you to collect the input component's state of every iteration round.
E.g.
final FacesContext facesContext = FacesContext.getCurrentInstance();
UIComponent repeat = getItSomehow(); // findComponent, binding, etc.
repeat.visitTree(VisitContext.createVisitContext(facesContext), new VisitCallback() {
@Override
public VisitResult visit(VisitContext context, UIComponent target) {
if (target instanceof UIInput && target.getId().equals("theTitle")) {
String clientId = target.getClientId(facesContext);
Object value = ((UIInput) target).getValue();
// ...
facesContext.addMessage(clientId, message);
}
return VisitResult.ACCEPT;
}
});
See also:
There is another option: Make your own replacement of the whole FacesMessages shebang. With blackjack. And...
Anyway, based on discussions with Johannes Brodwall we opted to avoid the whole visitTree mess and build our own messages mechanism. This included:
1) A ViewScoped bean containing a Map of Multimaps:
private Map<Object, Multimap<String, String>> fieldValidationMessages = new HashMap<>();
This takes an Object
as a field identifier (could be the respective bean itself, a UI component or even a String
generated at runtime inside the ui:repeat
. That identifier then can have an arbitrary number of String
messages on another arbitrary number of sub-fields. Very flexible.
The bean also had convenience methods for getting and setting messages on fields and subfields, and for checking whether any messages are stored at all (i.e. whether there were validation errors).
2) A simple xhtml include that displays error messages for a given field, replacing h:messages for...
And that's already it. The catch is that this runs during the application and rendering phase of the lifecycle instead of JSF's own validation phase. But since our project already decided to do bean validation instead of lifecycle validation, this was not an issue.
来源:https://stackoverflow.com/questions/19131583/how-do-i-attach-a-facesmessage-from-the-backing-bean-to-a-specific-field-in-a-ui