【问题标题】:h:commandButton/h:commandLink does not work on first click, works only on second clickh:commandButton/h:commandLink 在第一次单击时不起作用,仅在第二次单击时起作用
【发布时间】:2023-12-26 10:52:02
【问题描述】:

我们有一个更新动态包含的 ajax 导航菜单。包含文件有各自的形式。

<h:form>
    <h:commandButton value="Add" action="#{navigator.setUrl('AddUser')}">
        <f:ajax render=":propertiesArea" />
    </h:commandButton>
</h:form>
<h:panelGroup id="propertiesArea" layout="block">
    <ui:include src="#{navigator.selectedLevel.url}" />
</h:panelGroup>

它可以正常工作,但是包含文件中的任何命令按钮在第一次单击时都不起作用。它仅适用于第二次点击。

我发现了这个问题commandButton/commandLink/ajax action/listener method not invoked or input value not updated,我的问题在第 9 点中描述。 我了解需要在&lt;f:ajax render&gt; 的include 中明确包含&lt;h:form&gt; 的ID 才能解决。

<f:ajax render=":propertiesArea :propertiesArea:someFormId" />

然而,在我的例子中,表单 ID 是事先不知道的。此外,此表单最初在上下文中不可用。

上述情况有什么解决办法吗?

【问题讨论】:

    标签: jsf jsf-2 commandbutton commandlink


    【解决方案1】:

    您可以使用以下脚本来修复 Mojarra 2.0/2.1/2.2 错误(注意:这不会出现在 MyFaces 中)。此脚本将为在 ajax 更新后未检索到任何视图状态的表单创建 javax.faces.ViewState 隐藏字段。

    jsf.ajax.addOnEvent(function(data) {
        if (data.status == "success") {
            fixViewState(data.responseXML);
        }
    });
    
    function fixViewState(responseXML) {
        var viewState = getViewState(responseXML);
    
        if (viewState) {
            for (var i = 0; i < document.forms.length; i++) {
                var form = document.forms[i];
    
                if (form.method == "post") {
                    if (!hasViewState(form)) {
                        createViewState(form, viewState);
                    }
                }
                else { // PrimeFaces also adds them to GET forms!
                    removeViewState(form);
                }
            }
        }
    }
    
    function getViewState(responseXML) {
        var updates = responseXML.getElementsByTagName("update");
    
        for (var i = 0; i < updates.length; i++) {
            var update = updates[i];
    
            if (update.getAttribute("id").match(/^([\w]+:)?javax\.faces\.ViewState(:[0-9]+)?$/)) {
                return update.textContent || update.innerText;
            }
        }
    
        return null;
    }
    
    function hasViewState(form) {
        for (var i = 0; i < form.elements.length; i++) {
            if (form.elements[i].name == "javax.faces.ViewState") {
                return true;
            }
        }
    
        return false;
    }
    
    function createViewState(form, viewState) {
        var hidden;
    
        try {
            hidden = document.createElement("<input name='javax.faces.ViewState'>"); // IE6-8.
        } catch(e) {
            hidden = document.createElement("input");
            hidden.setAttribute("name", "javax.faces.ViewState");
        }
    
        hidden.setAttribute("type", "hidden");
        hidden.setAttribute("value", viewState);
        hidden.setAttribute("autocomplete", "off");
        form.appendChild(hidden);
    }
    
    function removeViewState(form) {
        for (var i = 0; i < form.elements.length; i++) {
            var element = form.elements[i];
            if (element.name == "javax.faces.ViewState") {
                element.parentNode.removeChild(element);
            }
        }
    }
    

    只需将其作为&lt;h:outputScript name="some.js" target="head"&gt; 包含在错误页面的&lt;h:body&gt; 中即可。如果您不能保证有问题的页面使用 JSF &lt;f:ajax&gt;,这将触发自动包含 jsf.js,那么您可能希望在 jsf.ajax.addOnEvent() 调用之前添加一个额外的 if (typeof jsf !== 'undefined') 检查,或者明确包含它由

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

    请注意 jsf.ajax.addOnEvent 仅涵盖标准 JSF &lt;f:ajax&gt; 而不是例如PrimeFaces &lt;p:ajax&gt;&lt;p:commandXxx&gt; 因为他们使用 jQuery 来完成这项工作。要同时涵盖 PrimeFaces ajax 请求,请添加以下内容:

    $(document).ajaxComplete(function(event, xhr, options) {
        if (typeof xhr.responseXML != 'undefined') { // It's undefined when plain $.ajax(), $.get(), etc is used instead of PrimeFaces ajax.
            fixViewState(xhr.responseXML);
        }
    }
    

    更新如果您使用 JSF 实用程序库 OmniFaces,很高兴知道上述内容自 1.7 以来已成为 OmniFaces 的一部分。只需在&lt;h:body&gt; 中声明以下脚本即可。另请参阅showcase

    <h:body>
        <h:outputScript library="omnifaces" name="fixviewstate.js" target="head" />
        ...
    </h:body>
    

    【讨论】:

    • 谢谢。解决方案工作得恰到好处。但我还有一点要说。我们还在一些 UI 组件中使用 primefaces(3.3)。我观察到如果我用 p:commandbutton 替换 h:commandbutton,这个问题就不会发生。不知道为什么,但我能找到的唯一区别是 h:commandbutton 呈现为 标签,而 p:commandbutton 呈现为
    • 确实,PrimeFaces 已经在其自己的 JSF ajax 引擎中内部解决了这个问题(查看core.js 文件)。因此,如果您使用 PrimeFaces ajax 组件,您将不会遇到这个 JSF API 特定的问题。 HTML 中的差异无关紧要。都是关于 ajax 响应处理的。
    • 当我将此脚本添加到我的页面时,chrome 运行良好,但在 IE9 中,我在var viewState = data.responseXML.getElementById("javax.faces.ViewState").firstChild.nodeValue 上收到了这条 javascript 消息 SCRIPT438: Object doesn't support property or method 'getElementById。我看,但似乎可以在网上找到解决方法。你知道如何在 IE9 中处理这个,BalusC 吗?
    • @Thang: 是的,但是setAttribute("name", value) 不适用于 IE6-8 中的输入元素,这正是这个 hack 首先出现的原因。另见例如webbugtrack.blogspot.com/2007/10/…
    • @MateusViccari:我自己为 Mojarra 2.3 github.com/javaserverfaces/mojarra/commit/… 修复了它,并使其成为 JSF 规范的必需部分 github.com/javaee/javaserverfaces-spec/issues/790
    【解决方案2】:

    感谢 BalusC,因为他的回答真的很棒(和往常一样 :))。但我必须补充一点,这种方法不适用于来自 RichFaces 4 的 ajax 请求。他们有几个关于 ajax 的问题,其中之一是没有调用 JSF-ajax-handlers。因此,当使用 RichFaces-components 对包含表单的某个容器进行重新渲染时,不会调用 fixViewState-function 并且 ViewState 会丢失。

    RichFaces Component Reference 中,他们说明了如何为“他们的”ajax 请求注册回调(实际上他们正在使用 jQuery 来挂钩所有 ajax 请求)。 但是使用它,我无法获得上面 BalusC 脚本用来获取 ViewState 的 ajax-response。

    所以基于 BalusC 的修复,我制定了一个非常相似的修复。在浏览器处理 ajax 请求之前,我的脚本将当前页面上所有表单的所有 ViewState 值保存在地图中。 DOM更新后,我尝试恢复之前保存的所有ViewState(对于现在缺少ViewState的所有表单)。

    继续:

    jQuery(document).ready(function() {
        jQuery(document).on("ajaxbeforedomupdate", function(args) {
            // the callback will be triggered for each received JSF AJAX for the current page
            // store the current view-states of all forms in a map
            storeViewStates(args.currentTarget.forms);
        });
        jQuery(document).on("ajaxcomplete", function(args) {
            // the callback will be triggered for each completed JSF AJAX for the current page
            // restore all view-states of all forms which do not have one
            restoreViewStates(args.currentTarget.forms);
        });
    });
    
    var storedFormViewStates = {};
    
    function storeViewStates(forms) {
        storedFormViewStates = {};
        for (var formIndex = 0; formIndex < forms.length; formIndex++) {
            var form = forms[formIndex];
            var formId = form.getAttribute("id");
            for (var formChildIndex = 0; formChildIndex < form.children.length; formChildIndex++) {
                var formChild = form.children[formChildIndex];
                if ((formChild.hasAttribute("name")) && (formChild.getAttribute("name").match(/^([\w]+:)?javax\.faces\.ViewState(:[0-9]+)?$/))) {
                    storedFormViewStates[formId] = formChild.value;
                    break;
                }
            }
        }
    }
    
    function restoreViewStates(forms) {
        for (var formIndexd = 0; formIndexd < forms.length; formIndexd++) {
            var form = forms[formIndexd];
            var formId = form.getAttribute("id");
            var viewStateFound = false;
            for (var formChildIndex = 0; formChildIndex < form.children.length; formChildIndex++) {
                var formChild = form.children[formChildIndex];
                if ((formChild.hasAttribute("name")) && (formChild.getAttribute("name").match(/^([\w]+:)?javax\.faces\.ViewState(:[0-9]+)?$/))) {
                    viewStateFound = true;
                    break;
                }
            }
            if ((!viewStateFound) && (storedFormViewStates.hasOwnProperty(formId))) {
                createViewState(form, storedFormViewStates[formId]);
            }
        }
    }
    
    function createViewState(form, viewState) {
        var hidden;
    
        try {
            hidden = document.createElement("<input name='javax.faces.ViewState'>"); // IE6-8.
        } catch(e) {
            hidden = document.createElement("input");
            hidden.setAttribute("name", "javax.faces.ViewState");
        }
    
        hidden.setAttribute("type", "hidden");
        hidden.setAttribute("value", viewState);
        hidden.setAttribute("autocomplete", "off");
        form.appendChild(hidden);
    }
    

    由于我不是 JavaScript 专家,我想这可能会进一步改进。但它绝对适用于 FF 17、Chromium 24、Chrome 12 和 IE 11。

    此方法的两个附加问题:

    • 再次使用相同的 ViewState-value 是否可行? IE。 JSF 是否为每个请求/响应的每个表单分配相同的 ViewState 值?我的方法是基于这个假设(我没有找到任何相关信息)。

    • 是否有人预计此 JavaScript 代码会出现任何问题,或者已经在使用任何浏览器时遇到了一些问题?

    【讨论】:

    • 您能否更具体一点地使用您的 js 代码?我在richfaces 4.5.13中用一个简单的脚本标签尝试了这个,它在命令按钮(“h:”或“a4j:”)上的结果相同(我必须单击两次);你也没有提到你的 jquery/richfaces 版本
    最近更新 更多