【问题标题】:Dynamic ajax navigation with <ui:include>使用 <ui:include> 的动态 ajax 导航
【发布时间】:2012-12-03 09:17:45
【问题描述】:

假设我想在我的应用程序中导航,并动态包含不同的 facelet 页面。我有一个这样的 commandLink:

<h:commandLink value="Link" action="#{navigation.goTo('someTest')}">
    <f:ajax render=":content" />
</h:commandLink>

这就是我包含 facelet 的地方:

<h:form id="content">
    <ui:include src="#{navigation.includePath}" />
</h:form>

导航类:

public class Navigation {
    private String viewName;

    public void goTo(String viewName) {
        this.viewName = viewName;
    }

    public String getIncludePath() {
        return resolvePath(viewName);
    }
}

我见过类似的例子,但这当然行不通。由于ui:include 是一个标记处理程序,包含在我的导航侦听器被调用之前很久就发生了。包括旧的 facelet,而不是新的。到目前为止,我明白了。

现在到头疼的部分:如何基于 actionListener 动态包含 facelet?我试图将 facelet 包含在 preRender 事件中,并在 RENDER_RESPONSE 之前包含一个 phaseListener。两者都有效,但在事件侦听器中,我不能包含包含其他 preRender 事件的 facelet,并且在 phaseListener 中,在包含的 facelet 中单击一些后,我会得到重复的 Id。但是,检查组件树告诉我,根本没有重复的组件。也许这两个想法一点都不好..

我需要一个解决方案,其中带有ui:include 的页面或包含facelet 的Java 类不必知道将被包含的页面,也不必知道确切的路径。以前有人解决过这个问题吗?我该怎么做?


我正在使用 JSF 2.1 和 Mojarra 2.1.15


重现问题所需的只是这个 bean:

@Named
public class Some implements Serializable {
    private static final long serialVersionUID = 1L;
    private final List<String> values = new ArrayList<String>();

    public Some() {
        values.add("test");
    }

    public void setInclude(String include) {
    }
    public List<String> getValues() {
        return values;
    }
}

这在您的索引文件中:

<h:head>
    <h:outputScript library="javax.faces" name="jsf.js" />
</h:head>

<h:body>
    <h:form id="topform">
        <h:panelGroup id="container">
            <my:include src="/test.xhtml" />
        </h:panelGroup>
    </h:form>
</h:body>

这在 text.xhtml 中

<ui:repeat value="#{some.values}" var="val">
    <h:commandLink value="#{val}" action="#{some.setInclude(val)}">
        <f:ajax render=":topform:container" />
    </h:commandLink>
</ui:repeat>

这足以产生这样的错误:

javax.faces.FacesException: Cannot add the same component twice: topform:j_id-549384541_7e08d92c

【问题讨论】:

    标签: ajax jsf jsf-2 navigation uiinclude


    【解决方案1】:

    对于OmniFaces,我也曾尝试过创建&lt;o:include&gt; 作为UIComponent,而不是TagHandler,后者在encodeChildren() 方法中执行FaceletContext#includeFacelet()。这样,在恢复视图阶段会记住正确包含的 facelet,并且包含的​​组件树仅在渲染响应阶段发生变化,这正是我们想要实现的构造。

    这是一个基本的启动示例:

    @FacesComponent("com.example.Include")
    public class Include extends UIComponentBase {
    
        @Override
        public String getFamily() {
            return "com.example.Include";
        }
    
        @Override
        public boolean getRendersChildren() {
            return true;
        }
    
        @Override
        public void encodeChildren(FacesContext context) throws IOException {
            getChildren().clear();
            ((FaceletContext) context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY)).includeFacelet(this, getSrc());
            super.encodeChildren(context);
        }
    
        public String getSrc() {
            return (String) getStateHelper().eval("src");
        }
    
        public void setSrc(String src) {
            getStateHelper().put("src", src);
        }
    
    }
    

    .taglib.xml注册如下:

    <tag>
        <tag-name>include</tag-name>
        <component>
            <component-type>com.example.Include</component-type>
        </component>
        <attribute>
            <name>src</name>
            <required>true</required>
            <type>java.lang.String</type>
        </attribute>
    </tag>
    

    这适用于以下视图:

    <h:outputScript name="fixViewState.js" />
    
    <h:form>
        <ui:repeat value="#{includeBean.includes}" var="include">
            <h:commandButton value="Include #{include}" action="#{includeBean.setInclude(include)}">
                <f:ajax render=":include" />
            </h:commandButton>
        </ui:repeat>
    </h:form>
    
    <h:panelGroup id="include">
        <my:include src="#{includeBean.include}.xhtml" />
    </h:panelGroup>
    

    以及以下支持 bean:

    @ManagedBean
    @ViewScoped
    public class IncludeBean implements Serializable {
    
        private List<String> includes = Arrays.asList("include1", "include2", "include3");
        private String include = includes.get(0);
    
        private List<String> getIncludes() {
            return includes;
        }
    
        public void setInclude(String include) {
            return this.include = include;
        }
    
        public String getInclude() { 
            return include;
        }
    
    }
    

    (此示例需要包含文件 include1.xhtmlinclude2.xhtmlinclude3.xhtml 与主文件位于同一基本文件夹中)

    fixViewState.js 可以在这个答案中找到:h:commandButton/h:commandLink does not work on first click, works only on second click。这个脚本是强制性的,以修复JSF issue 790,当有多个 ajax 表单更新彼此的父级时,视图状态会丢失。

    另请注意,这样每个包含文件都可以在必要时拥有自己的&lt;h:form&gt;,因此您不一定需要将它放在包含周围。

    这种方法在 Mojarra 中运行良好,即使回发请求来自包含内的表单,但它在 MyFaces 中很难失败,初始请求期间已经出现以下异常:

    java.lang.NullPointerException
        at org.apache.myfaces.view.facelets.impl.FaceletCompositionContextImpl.generateUniqueId(FaceletCompositionContextImpl.java:910)
        at org.apache.myfaces.view.facelets.impl.DefaultFaceletContext.generateUniqueId(DefaultFaceletContext.java:321)
        at org.apache.myfaces.view.facelets.compiler.UIInstructionHandler.apply(UIInstructionHandler.java:87)
        at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:49)
        at org.apache.myfaces.view.facelets.tag.ui.CompositionHandler.apply(CompositionHandler.java:158)
        at org.apache.myfaces.view.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:57)
        at org.apache.myfaces.view.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:48)
        at org.apache.myfaces.view.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:394)
        at org.apache.myfaces.view.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:448)
        at org.apache.myfaces.view.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:426)
        at org.apache.myfaces.view.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:244)
        at com.example.Include.encodeChildren(Include.java:54)
    

    MyFaces 基本上在视图构建结束时释放 Facelet 上下文,使其在视图渲染期间不可用,从而导致 NPE,因为内部状态有几个无效属性。但是,可以在渲染期间添加单个组件而不是 Facelet 文件。我真的没有时间调查这是我的错还是 MyFaces 的错。这也是为什么它还没有出现在 OmniFaces 中的原因。

    如果您仍然在使用 Mojarra,请随意使用它。但是,我强烈建议在同一页面上对所有可能的用例进行彻底测试。 Mojarra 有一些与状态保存相关的怪癖,可能在使用此构造时会失败。

    【讨论】:

    • 我在包含 facelet 时遇到了一些麻烦。只需getChildren().clear() 就足够了吗?我得到很多很多重复的ID。上一个请求中包含的所有组件都存储在StateContext 中。我通过从dynamicComponentsdynamicActions 中删除这些问题解决了这个问题。但我担心我会遇到更多问题,不是吗?
    • 您的状态保存是如何配置的?是否全部设置为默认值?我很想看到重现此问题的最小视图/bean 示例。我只在每个包含中使用非常简单的&lt;h:form&gt;&lt;h:inputText required="true"/&gt;&lt;h:commandButton value="submit"/&gt;&lt;h:messages/&gt;&lt;/h:form&gt; 对此进行了测试(也使用&lt;f:ajax&gt; 和操作方法)。
    • PARTIAL_STATE_SAVING 已启用,STATE_SAVING_METHOD 设置为服务器。一个简单的例子可能是不可能的,我在包含的 facelet 中动态创建一些组件,在包含的 facelet 中包含其他 facelet 等等。是的,疯狂 ;-) 我会尝试解决它。 getChildren().clear() 呢?应该够吗?
    • 我尝试了您的方法,但不幸的是,我遇到了与 PhaseListener 实验相同的问题。身份证重复。通过在 StateContext#dynamicActions 中摸索,我设法把它们中的大多数都弄出来了,但这感觉不仅仅是肮脏,而且不适用于ui:repeat 你试过这个了吗? stackoverflow.com/a/5755972/1551204 好像效果不错。
    • 您是否有机会发布重现 ID 重复问题的最小可能代码 sn-p?
    猜你喜欢
    • 2014-01-20
    • 1970-01-01
    • 2017-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-12
    相关资源
    最近更新 更多