Статьи

Несколько динамических включений с одним тегом JSF

Каждый разработчик JSF знает теги ui: include и ui: param. Вы можете включить фасет (файл XHTML) и передать объект, который будет доступен во включенном фасете, следующим образом:

1
2
3
<ui:include src="/sections/columns.xhtml">
    <ui:param name="columns" value="#{bean.columns}"/>
</ui:include>

Таким образом, вы можете, например, использовать его в таблице данных PrimeFaces с динамическими столбцами (p: columns)

1
2
3
4
5
6
7
8
<p:dataTable value="#{bean.entries}" var="data" rowKey="#{data.id}" ...>
    ...
    <ui:include src="/sections/columns.xhtml">
        <ui:param name="data" value="#{data}"/>
        <ui:param name="columns" value="#{bean.columns}"/>
    </ui:include>
 
</p:dataTable>

где включенный Facelet может содержать этот код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:p="http://primefaces.org/ui"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                ...>
    <p:columns value="#{columns}" var="column">
        <f:facet name="header">
            <h:outputText value="#{msgs[column.header]}"/>
        </f:facet>
 
        // place some input / select or complex composite component for multiple data types here.
        // a simple example for demonstration purpose:
        <p:inputText value="#{data[column.property]}"/>
    </p:columns>
</ui:composition>

# {bean.columns} ссылается на Список специальных объектов, которые описывают столбцы. Я назову такие объекты ColumnModel. Итак, это
Список <ColumnModel>. ColumnModel имеет, например, атрибуты header и property.

Продолжать. Теперь, если мы хотим добавить поддержку для сортировки / фильтрации, мы можем использовать динамические пути, которые ссылаются на конкретные файлы лицевых оболочек, связанные с сортировкой или / и фильтрацией. Просто привяжите атрибут src к свойству bean-компонента.

1
2
3
4
<ui:include src="#{bean.columnsIncludeSrc}">
   <ui:param name="data" value="#{data}"/>
   <ui:param name="columns" value="#{bean.columns}"/>
</ui:include>

Боб имеет что-то вроде

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
private boolean isFilterRight;
private boolean isSortRight
 
// setter / getter
 
public String getColumnsIncludeSrc() {
   if (isFilterRight && isSortRight) {
      return "/include/columnsTableFilterSort.xhtml";
   } else if (isFilterRight && !isSortRight) {
      return "/include/columnsTableFilter.xhtml";
   } else if (!isFilterRight && isSortRight) {
      return "/include/columnsTableSort.xhtml";
   } else {
      return "/include/columnsTable.xhtml";
   }
}

Различные лицевые стороны включены в зависимости от установленных логических прав. Таким образом, решение о том, какой файл будет включен, помещается в компонент. Для большей гибкости мы можем инкапсулировать таблицу в составной компонент и переместить логику принятия решений в класс компонента.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<cc:interface componentType="xxx.component.DataTable">
    <cc:attribute name="id" required="false" type="java.lang.String"
        shortDescription="Unique identifier of the component in a NamingContainer"/>
    <cc:attribute name="entries" required="true"
        shortDescription="The data which are shown in the datatable. This is a list of object representing one row."/>
    <cc:attribute name="columns" required="true" type="java.util.List"
        shortDescription="The columns which are shown in the datatable. This is a list of instances of type ColumnModel."/>
    ...
</cc:interface>
<cc:implementation>
    <p:dataTable value="#{cc.attrs.entries}" var="data" rowKey="#{data.id}" ...>
        ...
        <ui:include src="#{cc.columnsIncludeSrc}">
            <ui:param name="data" value="#{data}"/>
            <ui:param name="columns" value="#{cc.attrs.columns}"/>
        </ui:include>
 
    </p:dataTable>
</cc:implementation>

Как работает пользовательский интерфейс? Это обработчик тега, который применяется при построении представления. В JSF 2 дерево компонентов строится дважды по запросам POST, один раз в фазе RESTORE_VIEW и один раз в фазе RENDER_RESPONSE. На GET он создается один раз в фазе RENDER_RESPONSE. Это поведение указано в спецификации JSF 2 и является таким же в Mojarra и MyFaces. Создание представления в RENDER_RESPONSE необходимо в том случае, если автор страницы использует условные включения или условные шаблоны. Таким образом, вы можете быть уверены, что атрибут src в ui: include будет оценен незадолго до фазы рендеринга.

Но подойди к делу! То, что я написал до сих пор, было введением для мотивации расширения пользовательского интерфейса: include. Недавно у меня появилась задача использовать ap: dataTable с динамическими столбцами и p: rowEditor. Как этот в витрине PrimeFaces . Проблема только в том, что такая функция редактирования не поддерживает столбцы p :. Я хотел добавить теги p: column несколько раз, динамически, но с разными параметрами контекста. Вы можете представить это как ui: include с ui: param в цикле. В приведенном выше примере мы собираемся перебрать List <ColumnModel>. Каждая итерация цикла должна сделать экземпляр типа ColumnModel доступным во включенном фасете. Итак, я написал собственный обработчик тегов, который включал бы любой фейслет несколько раз.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
package xxx.taghandler;
 
import xxx.util.VariableMapperWrapper;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.component.UIComponent;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributeException;
import javax.faces.view.facelets.TagConfig;
import javax.faces.view.facelets.TagHandler;
 
/**
 * Tag handler to include a facelet multiple times with different contextes (objects from "value").
 * The attribute "value" can be either of type java.util.List or array.
 * If the "value" is null, the tag handler works as a standard ui:include.
 */
public class InlcudesTagHandler extends TagHandler {
 
    private final TagAttribute src;
    private final TagAttribute value;
    private final TagAttribute name;
 
    public InlcudesTagHandler(TagConfig config) {
        super(config);
 
        this.src = this.getRequiredAttribute("src");
        this.value = this.getAttribute("value");
        this.name = this.getAttribute("name");
    }
 
    @Override
    public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
        String path = this.src.getValue(ctx);
        if ((path == null) || (path.length() == 0)) {
            return;
        }
 
        // wrap the original mapper - this is important when some objects passed into include via ui:param
        // because ui:param invokes setVariable(...) on the set variable mappper instance
        VariableMapper origVarMapper = ctx.getVariableMapper();
        ctx.setVariableMapper(new VariableMapperWrapper(origVarMapper));
 
        try {
            this.nextHandler.apply(ctx, null);
 
            ValueExpression ve = (this.value != null) ? this.value.getValueExpression(ctx, Object.class) : null;
            Object objValue = (ve != null) ? ve.getValue(ctx) : null;
 
            if (objValue == null) {
                // include facelet only once
                ctx.includeFacelet(parent, path);
            } else {
                int size = 0;
 
                if (objValue instanceof List) {
                    size = ((List) objValue).size();
                } else if (objValue.getClass().isArray()) {
                    size = ((Object[]) objValue).length;
                }
 
                final ExpressionFactory exprFactory = ctx.getFacesContext().getApplication().getExpressionFactory();
                final String strName = this.name.getValue(ctx);
 
                // generate unique Id as a valid Java identifier and use it as variable for the provided value expression
                final String uniqueId = "a" + UUID.randomUUID().toString().replaceAll("-", "");
                ctx.getVariableMapper().setVariable(uniqueId, ve);
 
                // include facelet multiple times
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < size; i++) {
                    if ((strName != null) && (strName.length() != 0)) {
                        // create a new value expression in the array notation and bind it to the variable "name"
                        sb.append("#{");
                        sb.append(uniqueId);
                        sb.append("[");
                        sb.append(i);
                        sb.append("]}");
 
                        ctx.getVariableMapper().setVariable(strName,
                            exprFactory.createValueExpression(ctx, sb.toString(), Object.class));
                    }
 
                    // included facelet can access the created above value expression
                    ctx.includeFacelet(parent, path);
 
                    // reset for next iteration
                    sb.setLength(0);
                }
            }
        } catch (IOException e) {
            throw new TagAttributeException(this.tag, this.src, "Invalid path : " + path);
        } finally {
            // restore original mapper
            ctx.setVariableMapper(origVarMapper);
        }
    }
}

Самый важный вызов — ctx.includeFacelet (parent, path). Метод includeFacelet (…) из JSF API включает разметку лицевой стороны по некоторому пути относительно текущей разметки. Класс VariableMapperWrapper используется для отображения имени на значение через ui: param. Для примера со столбцами переменная column будет отображаться в выражения # {columns [0]}, # {columns [1]} и т. Д. Перед каждым вызовом includeFacelet (…). Ну, не совсем в этих выражениях, вместо столбцов должно быть уникальное имя, снова сопоставленное с объектом столбцов (чтобы избежать возможных конфликтов имен). Класс Mapper выглядит следующим образом

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package xxx.util;
 
import java.util.HashMap;
import java.util.Map;
import javax.el.ELException;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
 
/**
 * Utility class for wrapping a VariableMapper. Modifications occur to the internal Map instance.
 * The resolving occurs first against the internal Map instance and then against the wrapped VariableMapper
 * if the Map doesn't contain the requested ValueExpression.
 */
public class VariableMapperWrapper extends VariableMapper {
 
    private final VariableMapper wrapped;
 
    private Map<String, ValueExpression> vars;
 
    public VariableMapperWrapper(VariableMapper orig) {
        super();
        this.wrapped = orig;
    }
 
    @Override
    public ValueExpression resolveVariable(String variable) {
        ValueExpression ve = null;
        try {
            if (this.vars != null) {
                // try to resolve against the internal map
                ve = this.vars.get(variable);
            }
 
            if (ve == null) {
                // look in the wrapped variable mapper
                return this.wrapped.resolveVariable(variable);
            }
 
            return ve;
        } catch (Throwable e) {
            throw new ELException("Could not resolve variable: " + variable, e);
        }
    }
 
    @Override
    public ValueExpression setVariable(String variable, ValueExpression expression) {
        if (this.vars == null) {
            this.vars = new HashMap<String, ValueExpression>();
        }
 
        return this.vars.put(variable, expression);
    }
}

Зарегистрируйте обработчик тега в XML-файле taglib, и все готово.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<tag>
    <tag-name>includes</tag-name>
    <handler-class>xxx.taghandler.InlcudesTagHandler</handler-class>
    <attribute>
        <description>
            <![CDATA[The relative path to a XHTML file to be include one or multiple times.]]>
        </description>
        <name>src</name>
        <required>true</required>
        <type>java.lang.String</type>
    </attribute>
    <attribute>
        <description>
            <![CDATA[Objects which should be available in the included XHTML files. This attribute can be either
            of type java.util.List or array. If it is null, the tag handler works as a standard ui:include.]]>
        </description>
        <name>value</name>
        <required>false</required>
        <type>java.lang.Object</type>
    </attribute>
    <attribute>
        <description>
            <![CDATA[The name of the parameter which points to an object of each iteration over the given value.]]>
        </description>
        <name>name</name>
        <required>false</required>
        <type>java.lang.String</type>
    </attribute>
</tag>

Теперь я смог использовать его в составном компоненте как

1
2
3
4
5
6
7
<p:dataTable value="#{cc.attrs.entries}" var="data" rowKey="#{data.id}" ...>
    ...
    <custom:includes src="#{cc.columnsIncludeSrc}" value="#{cc.attrs.columns}" name="column">
        <ui:param name="data" value="#{data}"/>
    </custom:includes>
 
</p:dataTable>

Обычно файл facelet (и дерево компонентов) содержит довольно обычный тег p: column, что означает, что мы можем использовать все функции DataTable!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:p="http://primefaces.org/ui"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                ...>
    <p:column headerText="#{msgs[column.header]}">
        <p:cellEditor>
            <f:facet name="output">
                <custom:typedOutput outputType="#{column.outputTypeName}"
                   typedData="#{column.typedData}"
                   value="#{data[column.property]}"
                   timeZone="#{cc.timeZone}"
                   calendarPattern="#{cc.calendarPattern}"      
                   locale="#{cc.locale}"/>
            </f:facet>
 
            <f:facet name="input">
                <custom:typedInput inputType="#{column.inputTypeName}"
                   typedData="#{column.typedData}"
                   label="#{column.inputTypeName}"
                   value="#{data[column.property]}"
                   onchange="highlightEditedRow(this)"
                   timeZone="#{cc.timeZone}"
                   calendarPattern="#{cc.calendarPattern}"
                   locale="#{cc.locale}"/>
            </f:facet>
        </p:cellEditor>
    </p:column>
</ui:composition>

Примечание : этот подход может быть применен к другим компонентам и вариантам использования. InlcudesTagHandler работает вселенно. Например, я могу представить себе создание динамического компонента Menu в PrimeFaces без лежащей в основе MenuModel. Конечно же, список или массив некоторого модельного класса все еще нужен.