带有 <ui:include> 的动态 ajax 导航 [英] Dynamic ajax navigation with &lt;ui:include&gt;

查看:20
本文介绍了带有 <ui:include> 的动态 ajax 导航的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Suppose I want to navigate in my application, and include different facelet pages dynamically. I have a commandLink like this:

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

这就是我包含 facelet 的地方:

And this is where I include the 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 是一个标记处理程序,包含发生在我的导航侦听器被调用之前很久.包括旧小面,而不是新小面.到目前为止,我明白了.

I have seen similar examples, but this doesn't work of course. As ui:include is a taghandler, the include happens long before my navigation listener is invoked. The old facelet is included, instead of the new. So far I get it.

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

Now to the headache part: How can I dynamically include a facelet, based on an actionListener? I tried to include the facelet in a preRender event, and a phaseListener before RENDER_RESPONSE. Both work, but in the event listener I can't include a facelet which contains an other preRender event, and in the phaseListener I get duplicate Id's after some clicks in the included facelet. However, inspecting the component tree tells me, there are no duplicate components at all. Maybe these two ideas were not to good at all..

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

I need a solution, where the page with the ui:include, or the Java class which includes the facelet, doesn't have to know the pages, which will be included, nor the exact path. Did anybody solve this problem before? How can I do it?

我使用的是 JSF 2.1 和 Mojarra 2.1.15

I am using JSF 2.1 and Mojarra 2.1.15

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

All you need to reproduce the Problem is this 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 中

And this in 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>

这足以产生这样的错误:

That's enough to produce an error like this:

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

推荐答案

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

For OmniFaces, I've also ever experimented with this by creating an <o:include> as UIComponent instead of a TagHandler which does a FaceletContext#includeFacelet() in the encodeChildren() method. This way the right included facelet is remembered during restore view phase and the included component tree only changes during render response phase, which is exactly what we want to achieve this construct.

这是一个基本的开球示例:

Here's a basic kickoff example:

@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:

And the following backing 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 与主文件)

(this example expects include files include1.xhtml, include2.xhtml and include3.xhtml in the same base folder as the main file)

fixViewState.js 可以在这个答案中找到:h:commandButton/h:commandLink 在第一次单击时不起作用,仅在第二次单击时起作用.该脚本是强制性的,以修复 JSF 问题 790有多个 ajax 表单可以更新彼此的父表单.

The fixViewState.js can be found in this answer: h:commandButton/h:commandLink does not work on first click, works only on second click. This script is mandatory in order to fix JSF issue 790 whereby the view state get lost when there are multiple ajax forms which update each other's parent.

另请注意,这样每个包含文件都可以在必要时拥有自己的 ,因此您不一定需要将其放在包含文件中.

Also note that this way each include file can have its own <h:form> when necessary, so you don't necessarily need to put it around the include.

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

This approach works fine in Mojarra, even with postback requests coming from forms inside the include, however it fails hard in MyFaces with the following exception during initial request already:

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 中的原因.

MyFaces basically releases the Facelet context during end of view build time, making it unavailable during view render time, resulting in NPEs because the internal state has several nulled-out properties. It's however possible to add individual components instead of a Facelet file during render time. I didn't really have had the time to investigate if this is my fault or MyFaces' fault. That's also why it didn't end up in OmniFaces yet.

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

If you're using Mojarra anyway, feel free to use it. I however strongly recommend to test it thoroughly with all possible use cases on the very same page. Mojarra has some state saving related quirks which might fail when using this construct.

这篇关于带有 <ui:include> 的动态 ajax 导航的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆