Как создать динамические поля формы 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".

2 ответа

Решение

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


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

  1. Используйте 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 для свойства боба все еще "плохая практика".

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

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


  2. Создайте компоненты программно в 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's easiest if it's 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} находится в более широкой области видимости, чем область запроса (так что вы можете безопасно использовать, например, здесь компонент с областью видимости), и сохраняет компонент свободным от UIComponent свойства, которые, в свою очередь, позволяют избежать потенциальных проблем сериализации и утечки памяти, когда компонент содержится в качестве свойства сериализуемого компонента.

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

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

    А потом лениво заселить его в получатель вида

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

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


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


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

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

Если источником является 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 есть небольшая кривая обучения, но IMO - это правильный инструмент для работы, поскольку источником является XML, а местом назначения также является XML.

Чтобы сделать отображение между формой и управляемым бином, просто используйте 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, так далее.

Другие вопросы по тегам