Как мне реализовать NamingContainer? Все дети получают одинаковый идентификатор клиента
Я пытаюсь написать свой собственный компонент дерева. Узел дерева отображается как div, содержащий дочерние компоненты компонента дерева, например:
<my:tree id="extendedTree"
value="#{controller.rootNode}"
var="node">
<h:outputText id="xxx" value="#{node.name}" />
<h:commandLink value="Test" actionListener="#{controller.nodeSelectionActionListener}" />
</my:tree>
Пока все хорошо - все работает как положено, но h:outputText
получает один и тот же идентификатор несколько раз.
Итак, я реализовал свой компонент javax.faces.NamingController
перезаписывая getContainerClientId()
:
@Override
public String getContainerClientId(FacesContext context) {
String clientId = super.getClientId(context);
String containerClientId = clientId + ":" + index;
return containerClientId;
}
index
устанавливается и обновляется во время итерации по узлам. Но getContainerClientId()
вызывается только один раз для каждого потомка (не для каждой итерации и не для каждого потомка, как я ожидал). Это приводит к тому, что каждому дочернему идентификатору присваивается одинаковый идентификатор контейнера:
form:treeid:0:xxx
То же самое для перезаписи getClientId()
,
Что я упустил?
2 ответа
Ответ скрыт в нижней части главы 3.1.6 спецификации JSF 1.2:
3.1.6 Идентификаторы клиента
...
Значение, возвращаемое этим методом, должно быть одинаковым на протяжении всего времени существования экземпляра компонента, если только
setId()
вызывается, и в этом случае он будет пересчитан при следующем вызовеgetClientId()
,
Другими словами, результат getClientId()
по умолчанию кэшируется компонентом JSF, как это реализовано в UIComponentBase#getClientId()
(см. также проверку нуля в строке 275, как в Mojarra 1.2_15), и этот кэш сбрасывается, когда UIComponentBase#setId()
называется (см. также строку 358, как в Мохарре 1.2_15). Пока вы не сбросите кэшированный идентификатор клиента, он будет возвращать одно и то же значение на каждом getClientId()
вызов.
Таким образом, при оказании детей в encodeChildren()
реализация вашего компонента или средства визуализации, которое, скорее всего, будет выглядеть следующим образом,
for (UIComponent child : getChildren()) {
child.encodeAll(context);
}
ты должен за каждого ребенка звонить UIComponent#setId()
с результатом UIComponent#getId()
сбросить внутренний кэшированный идентификатор клиента перед кодированием дочернего элемента:
for (UIComponent child : getChildren()) {
child.setId(child.getId());
child.encodeAll(context);
}
UIData
класс позади <h:dataTable>
реализация делает это, кстати, также (см. строку 1382, как в Мохарре 1.2_15). Обратите внимание, что это не специфично для JSF 1.x, это же относится и к JSF 2.x (а также к UIRepeat
класс позади Facelets <ui:repeat>
).
Стоит упомянуть, что если у дочерних элементов вашего компонента есть дочерние элементы, то также может потребоваться обновить их кешированные идентификаторы. С этой разметкой, немного адаптированной из оригинала:
<my:tree id="extendedTree"
value="#{controller.rootNode}"
var="node">
<h:panelGroup layout="block" id="nodeBlock">
<h:outputText id="xxx" value="#{node.name}" />
<h:commandLink value="Test" actionListener="#{controller.nodeSelectionActionListener}" />
</h:panelGroup>
</my:tree>
Идентификатор <panelGroup>
выходит ОК после применения исправления BalusC, описанного выше, но все подкомпоненты получают значение 0 в итераторе.
Чтобы исправить это, также переберите все уровни дочерних элементов и обновите их кешированные идентификаторы. Так:child.setId(child.getId());
становится uncacheId(child);
где uncacheId
функция определяется:
private void uncacheId(UIComponent el) {
el.setId(el.getId());
el.getChildren().forEach(this::uncacheId);
}
Это может быть очевидно, но мне потребовалось время, чтобы понять, так что...
h:outputText id дает вам то же самое, что вы не сделали его динамическим. Вы можете создать это как:
<h:outputText id="xxx_#{node.id}" value="#{node.name}" />
Предположим, что узел имеет атрибут id, который является уникальным.