Как создать динамические поля формы JSF

Я нашел несколько подобных вопросов, таких как этот, однако есть так много способов сделать это, чтобы он меня смутил.

Мы получаем файл XML, который мы читаем. Этот XML содержит информацию о некоторых полях формы, которые должны быть представлены.

Итак, я создал этот пользовательский DynamicField.java, который имеет всю необходимую информацию:

public class DynamicField {
  private String label; // label of the field
  private String fieldKey; // some key to identify the field
  private String fieldValue; // the value of field
  private String type; // can be input,radio,selectbox etc

  // Getters + setters.
}

Итак, мы имеем List<DynamicField>.

Я хочу перебрать этот список и заполнить поля формы, чтобы он выглядел примерно так:

<h:dataTable value="#{dynamicFields}" var="field">
    <my:someCustomComponent value="#{field}" />
</h:dataTable>

<my:someCustomComponent> затем вернет соответствующие компоненты формы JSF (т.е. label, inputText)

Другим подходом было бы просто отобразить <my:someCustomComponent>, а затем вернуть HtmlDataTable элементы формы. (Я думаю, что это, возможно, проще сделать).

Какой подход лучше всего? Может ли кто-нибудь показать мне ссылки или код, где он показывает, как я могу это создать? Я предпочитаю полные примеры кода, а не такие ответы, как "Вам нужен подкласс javax.faces.component.UIComponent".

Ответ 1

Поскольку происхождение на самом деле не является XML, а джавабеем, а другой ответ не заслуживает редактирования в совершенно другой вкус (он может быть полезен для будущих ссылок другими), я буду добавьте еще один ответ, основанный на явавском происхождении.


Я вижу в основном три варианта, когда происхождение является джавабеем.

  • Используйте атрибут JSF rendered или даже теги JSTL <c:choose>/<c:if> для условного рендеринга или сборки желаемых компонентов. Ниже приведен пример с использованием атрибута rendered:

    <ui:repeat value="#{bean.fields}" var="field">
        <div class="field">
            <h:inputText value="#{bean.values[field.name]}" rendered="#{field.type == 'TEXT'}" />
            <h:inputSecret value="#{bean.values[field.name]}" rendered="#{field.type == 'SECRET'}" />
            <h:inputTextarea value="#{bean.values[field.name]}" rendered="#{field.type == 'TEXTAREA'}" />
            <h:selectOneRadio value="#{bean.values[field.name]}" rendered="#{field.type == 'RADIO'}">
                <f:selectItems value="#{field.options}" />
            </h:selectOneRadio>
            <h:selectOneMenu value="#{bean.values[field.name]}" rendered="#{field.type == 'SELECTONE'}">
                <f:selectItems value="#{field.options}" />
            </h:selectOneMenu>
            <h:selectManyMenu value="#{bean.values[field.name]}" rendered="#{field.type == 'SELECTMANY'}">
                <f:selectItems value="#{field.options}" />
            </h:selectManyMenu>
            <h:selectBooleanCheckbox value="#{bean.values[field.name]}" rendered="#{field.type == 'CHECKONE'}" />
            <h:selectManyCheckbox value="#{bean.values[field.name]}" rendered="#{field.type == 'CHECKMANY'}">
                <f:selectItems value="#{field.options}" />
            </h:selectManyCheckbox>
        </div>
    </ui:repeat>
    

    Пример подхода JSTL можно найти в Как создать сетку составного компонента JSF? Нет, JSTL абсолютно не является "плохой практикой". Этот миф остается в стороне от эпохи JSF 1.x и продолжается слишком долго, потому что стартеры не поняли жизненный цикл и полномочия JSTL. По сути, вы можете использовать JSTL только тогда, когда модель за #{bean.fields}, как в приведенном выше фрагменте, никогда не изменяется во время, по крайней мере, области просмотра JSF. См. Также JSTL в JSF2 Facelets... имеет смысл? Вместо этого использование binding для свойства bean по-прежнему является "плохой практикой".

    Что касается <ui:repeat><div>, на самом деле не имеет значения, какой итеративный компонент вы используете, вы можете даже использовать <h:dataTable> как в своем первоначальном вопросе, так и в компоненте, который выполняет итерацию компонента, например <p:dataGrid> или <p:dataList>. При необходимости переформатируйте большой кусок кода в файл include или tagfile.

    Что касается сбора представленных значений, #{bean.values} должен указывать на Map<String, Object>, который уже был предварительно обработан. A HashMap. Возможно, вы захотите предварительно заполнить карту в случае элементов управления, которые могут устанавливать несколько значений. Затем вы должны предварительно заполнить его значением List<Object>. Обратите внимание, что я ожидаю, что Field#getType() будет enum, поскольку это облегчает обработку в кодовой части Java. Затем вы можете использовать инструкцию switch вместо неприятного блока if/else.


  • Создайте компоненты программным способом в прослушивателе событий postAddToView:

    <h:form id="form">
        <f:event type="postAddToView" listener="#{bean.populateForm}" />
    </h:form>
    

    С

    public void populateForm(ComponentSystemEvent event) {
        HtmlForm form = (HtmlForm) event.getComponent();
        for (Field field : fields) {
            switch (field.getType()) { // It easiest if it an enum.
                case TEXT:
                    UIInput input = new HtmlInputText();
                    input.setId(field.getName()); // Must be unique!
                    input.setValueExpression("value", createValueExpression("#{bean.values['" + field.getName() + "']}", String.class));
                    form.getChildren().add(input);
                    break;
                case SECRET:
                    UIInput input = new HtmlInputSecret();
                    // etc...
            }
        }
    }
    

    (обратите внимание: НЕ создавайте HtmlForm самостоятельно! используйте созданный JSF, этот никогда не null)

    Это гарантирует, что дерево заселено ровно в нужный момент и не дает геттерам быть свободным от бизнес-логики и избегает потенциальных проблем с "повторяющимся идентификатором компонента", когда #{bean} находится в более широкой области действия, чем область запроса (так что вы можете безопасно использовать, например, область видимости bean здесь) и сохраняет bean без свойств UIComponent, что, в свою очередь, позволяет избежать возможных проблем с сериализацией и утечки памяти, когда компонент удерживается как свойство сериализуемого bean.

    Если вы все еще используете JSF 1.x, где <f:event> недоступен, вместо этого свяжите компонент формы с запросом (не session!) с областью bean через binding

    <h:form id="form" binding="#{bean.form}" />
    

    И затем лениво заселить его в getter формы:

    public HtmlForm getForm() {
        if (form == null) {
            form = new HtmlForm();
            // ... (continue with code as above)
        }
        return form;
    }
    

    При использовании binding очень важно понять, что компоненты пользовательского интерфейса в основном связаны с запросами и не должны быть назначены как свойство bean в более широкой области. См. Также Как работает атрибут привязки в JSF? Когда и как его использовать?


  • Создайте настраиваемый компонент с помощью настраиваемого средства визуализации. Я не собираюсь публиковать полные примеры, так как много кода, которое в конце концов было бы очень сложным и специфичным для приложения беспорядком.


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

Отмечено, что существует абсолютно ничего, что возможно только на Java (путь №2) и невозможно в XHTML + XML (путь №1). Все возможно в XHTML + XML так же хорошо, как в Java. Многие начинающие недооценивают XHTML + XML (особенно <ui:repeat> и JSTL) в динамическом создании компонентов и неправильно думают, что Java будет "единственным и единственным" способом, в то время как это обычно заканчивается хрупким и запутанным кодом.

Ответ 2

Если источником является XML, я предлагаю перейти на совершенно другой подход: XSL. Facelets - основанный на XHTML. Вы можете легко использовать XSL для перехода от XML к XHTML. Это выполнимо с немного приличным Filter, который срабатывает до того, как JSF выполняет работы.

Вот пример запуска.

persons.xml

<?xml version="1.0" encoding="UTF-8"?>
<persons>
    <person>
        <name>one</name>
        <age>1</age>
    </person>
    <person>
        <name>two</name>
        <age>2</age>
    </person>
    <person>
        <name>three</name>
        <age>3</age>
    </person>
</persons>

persons.xsl

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html">

    <xsl:output method="xml"
        doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
        doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>

    <xsl:template match="persons">
        <html>
        <f:view>
            <head><title>Persons</title></head>
            <body>
                <h:panelGrid columns="2">
                    <xsl:for-each select="person">
                        <xsl:variable name="name"><xsl:value-of select="name" /></xsl:variable>
                        <xsl:variable name="age"><xsl:value-of select="age" /></xsl:variable>
                        <h:outputText value="{$name}" />
                        <h:outputText value="{$age}" />
                    </xsl:for-each>
                </h:panelGrid>
            </body>
        </f:view>
        </html>
    </xsl:template>
</xsl:stylesheet>

JsfXmlFilter, который отображается на <servlet-name> FacesServlet и предполагает, что сам FacesServlet отображается на <url-pattern> в *.jsf.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException
{
    HttpServletRequest r = (HttpServletRequest) request;
    String rootPath = r.getSession().getServletContext().getRealPath("/");
    String uri = r.getRequestURI();
    String xhtmlFileName = uri.substring(uri.lastIndexOf("/")).replaceAll("jsf$", "xhtml"); // Change this if FacesServlet is not mapped on `*.jsf`.
    File xhtmlFile = new File(rootPath, xhtmlFileName);

    if (!xhtmlFile.exists()) { // Do your caching job.
        String xmlFileName = xhtmlFileName.replaceAll("xhtml$", "xml");
        String xslFileName = xhtmlFileName.replaceAll("xhtml$", "xsl");
        File xmlFile = new File(rootPath, xmlFileName);
        File xslFile = new File(rootPath, xslFileName);
        Source xmlSource = new StreamSource(xmlFile);
        Source xslSource = new StreamSource(xslFile);
        Result xhtmlResult = new StreamResult(xhtmlFile);

        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer(xslSource);
            transformer.transform(xmlSource, xhtmlResult);
        } catch (TransformerException e) {
            throw new RuntimeException("Transforming failed.", e);
        }
    }

    chain.doFilter(request, response);
}

Запустите http://example.com/context/persons.jsf, и этот фильтр начнет работать и преобразует persons.xml в persons.xhtml с помощью persons.xsl и, наконец, persons.xhtml там, где ожидает JSF.

Правда, у XSL есть немного кривой обучения, но это ИМО - правильный инструмент для задания, так как источником является XML, а место назначения - XML, как хорошо.

Чтобы сделать сопоставление между формой и управляемым bean, просто используйте Map<String, Object>. Если вы так называете поля ввода

<h:inputText value="#{bean.map.field1}" />
<h:inputText value="#{bean.map.field2}" />
<h:inputText value="#{bean.map.field3}" />
...

Представленные значения будут доступны клавишами Map field1, field2, field3 и т.д.