JSF2 - 为啥渲染响应不重新渲染组件设置?

Posted

技术标签:

【中文标题】JSF2 - 为啥渲染响应不重新渲染组件设置?【英文标题】:JSF2 - Why does render response not rerender component setting?JSF2 - 为什么渲染响应不重新渲染组件设置? 【发布时间】:2011-03-01 01:06:05 【问题描述】:

我遇到以下问题:

在我的视图恢复后,字段验证会导致 JSF 跳到渲染响应阶段(因为必填字段为空)。但是即使呈现当前值(空字符串)以向用户显示他/她没有填写任何内容,也不会执行以下语句:

 <c:if test="#empty cc.attrs.fieldValue">
     <f:attribute name="style" value="background-color: yellow;"/>
 </c:if>

这是错误还是功能?请帮忙。

完整的测试示例(Netbeans 6.8 项目)在这里:http://www.221b.cz/so/JSFTester.zip

来自教程:“如果请求是回发,并且在应用请求值阶段、处理验证阶段或更新模型值阶段遇到错误,则在呈现响应阶段呈现原始页面”(http://java.sun.com/javaee/5/docs/tutorial/doc/bnaqq.html)

这是否意味着如果视图在“恢复视图”阶段恢复,然后任何应用请求/验证/更新模型阶段失败并跳到“渲染响应”,渲染响应仅通过恢复的视图而不对客户端进行任何更改?

托管 Bean (TesterBean.java):

package cz.test;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

@ManagedBean
@RequestScoped
public class TesterBean 

// Simple DataStore (in real world EJB)
private static String storedSomeValue = null;

private String someValue;

public TesterBean() 


public String storeValue() 
    storedSomeValue = someValue;
    return "index";


public String eraseValue() 
    storedSomeValue = null;
    return "index";


public String getSomeValue() 
    someValue = storedSomeValue;
    return someValue;


public void setSomeValue(String someValue) 
    this.someValue = someValue;
    


复合组件(field-component.xhtml):

<?xml version='1.0' encoding='ISO-8859-1' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:composite="http://java.sun.com/jsf/composite"
  xmlns:c="http://java.sun.com/jsp/jstl/core">

<!-- INTERFACE -->
<composite:interface>
    <composite:attribute name="currentBehaviour" type="java.lang.String" required="true"/>
    <composite:attribute name="fieldValue" required="true"/>
</composite:interface>

<!-- IMPLEMENTATION -->
<composite:implementation>
    <h:panelGrid columns="3">
        <c:choose>
            <c:when test="#cc.attrs.currentBehaviour == 'READONLY'" >
                <h:outputText id="fieldValue" value="#cc.attrs.fieldValue">
                </h:outputText>
            </c:when>
            <c:when test="#cc.attrs.currentBehaviour == 'MANDATORY'" >
                <h:inputText id="fieldValue" value="#cc.attrs.fieldValue" required="true">
                    <f:attribute name="requiredMessage" value="Field is mandatory"/>
                    <c:if test="#empty cc.attrs.fieldValue">
                        <f:attribute name="style" value="background-color: yellow;"/>
                    </c:if>
                </h:inputText>&nbsp;*
            </c:when>
            <c:when test="#cc.attrs.currentBehaviour == 'OPTIONAL'" >                    
                <h:inputText id="fieldValue" value="#cc.attrs.fieldValue">                        
                </h:inputText>                    
            </c:when>
        </c:choose>
        <h:message for="fieldValue" style="color:red;" />
    </h:panelGrid>
</composite:implementation>

页面(index.xhtml):

<?xml version='1.0' encoding='UTF-8' ?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:h="http://java.sun.com/jsf/html"      
   xmlns:ez="http://java.sun.com/jsf/composite/components">

<h:head>
    <title>Testing page</title>
</h:head>
<h:body>
    <h:form>                        
        <h:outputText value="Some value:"/>
        <ez:field-component currentBehaviour="MANDATORY" fieldValue="#testerBean.someValue"/>           
        <h:commandButton value="Store" action="#testerBean.storeValue"/>
        <h:commandButton value="Erase" action="#testerBean.eraseValue" immediate="true"/>
    </h:form>

    <br/><br/>
    <b>Why is field's background color not set to yellow?</b>
    <ol>
        <li>NOTICE: Field has yellow background color (mandatory field with no value)</li>
        <li>Fill in any value (eg. "Hello") and press Store</li>
        <li>NOTICE: Yellow background disappeared (as mandatory field has value)</li>
        <li>Clear text in the field and press Store</li>
        <li><b>QUESTION: Why is field's background color not set to yellow?</b></li>
        <li>Press Erase</li>
        <li>NOTICE: Field has yellow background color (mandatory field with no value)</li>
    </ol>
</h:body>

按照 Brian 的建议进行编辑 (field-component.xhtml)

<?xml version='1.0' encoding='ISO-8859-1' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:composite="http://java.sun.com/jsf/composite">

<!-- INTERFACE -->
<composite:interface>
    <composite:attribute name="currentBehaviour" type="java.lang.String" required="true"/>
    <composite:attribute name="fieldValue" required="true"/>
</composite:interface>

<!-- IMPLEMENTATION -->
<composite:implementation>
    <h:panelGrid columns="3">
        <h:outputText rendered="#cc.attrs.currentBehaviour == 'READONLY'" id="fieldValue1" value="#cc.attrs.fieldValue" />

        <h:inputText rendered="#cc.attrs.currentBehaviour == 'MANDATORY'" id="fieldValue2" title="#cc.attrs.fieldValue" value="#cc.attrs.fieldValue" required="true" style="#empty cc.attrs.fieldValue ? 'background-color: yellow;' : ''">
            <f:attribute name="requiredMessage" value="Field is mandatory"/>
        </h:inputText>&nbsp;*

        <h:inputText rendered="#cc.attrs.currentBehaviour == 'OPTIONAL'" id="fieldValue3" value="#cc.attrs.fieldValue"/>

        <h:message for="fieldValue" style="color:red;" />
    </h:panelGrid>
</composite:implementation>

但即使我摆脱了 JSTL 仍然无法正常工作:-( 似乎只有 value 属性使用 h:inputText 中 http 请求的新值进行了更新,但其余属性并未在 Render Response 阶段重新评估。

【问题讨论】:

【参考方案1】:

您使用的&lt;c:choose&gt;&lt;c:when&gt; 标签是JSTL 标签,而不是JSF 标签。这意味着它们是在构建时评估的,而不是在渲染时。当您回帖时,不会重新构建组件树,而是重新渲染它,并且不会重新评估 &lt;c: 标记。

使用&lt;h:panelGroup rendered="#"&gt; 标记而不是&lt;c: 标记再次尝试您的示例。

查看这篇文章了解更多详情: http://drewdev.blogspot.com/2008/03/build-time-vs-render-time.html

记住您不能让组件“重新出现”在 JSF 表单的回发上,这一点非常重要。这是因为 JSF 组件树不应该在保存其状态和恢复其状态之间进行更改。这一点非常重要,所以让我再说一遍,JSF 组件树不应该在保存状态和恢复状态之间进行更改。

【讨论】:

谢谢布赖恩。我尝试按照您的建议实施 field-component.xhtml。我还阅读了您的博客文章(刚刚对我来说很好但很难)。我用你的建议编辑了这个问题,但它仍然不起作用:-( 我将 title 属性添加到 h:inputText 以表明在渲染响应阶段新值 (cc.attrs.fieldValue) 未应用于 h:inputText 属性。第一次填写“Hello”->Store。字段内容是“你好”,标题是“你好”。然后尝试清除内容->存储。字段内容为空但标题仍然是“你好”。这让我很困惑。 似乎在 http 请求中发送的值仅适用于 h:inputText 的“value”属性,而不适用于 h:inputText 标签的任何其他属性。我对吗?这是正确的行为吗? &lt;f:attribute&gt; 标签设置了 inputText 元素的属性,而不是自定义组件的属性。我认为您可以通过使用 `' 标签以标准方式实现所需的效果。【参考方案2】:

这两天我做了一些调查和调试,这是我的结果。

首先,我简化了示例以省略复合组件并使其尽可能简单。

托管 bean (TesterBean2.java)

package cz.test;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

@ManagedBean
@RequestScoped
public class TesterBean2 

// Simple DataStore (in real world EJB)
private static String storedSomeValue = null;

private String someValue;

public TesterBean2() 


public String storeValue() 
    storedSomeValue = someValue;
    return "index";


public String eraseValue() 
    storedSomeValue = null;
    return "index";


public String getSomeValue() 
    someValue = storedSomeValue;
    return someValue;


public void setSomeValue(String someValue) 
    this.someValue = someValue;


测试页面(index.xhtml)

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html">

<h:head>
    <title>Testing page</title>
</h:head>
<h:body>
    <h:form>
        <h:inputText id="fieldValue" requiredMessage="Field is mandatory" title="#testerBean2.someValue" value="#testerBean2.someValue" required="true" style="#empty testerBean2.someValue ? 'background-color: yellow;' : ''"/>
        <h:commandButton value="Store" action="#testerBean2.storeValue"/>
        <h:commandButton value="Erase" action="#testerBean2.eraseValue"/>
    </h:form>
</h:body>

问题出在哪里? 我认为这是 com.sun.faces.renderkit_html_basic.TextRenderer 及其方法 getEndTextToRender 的问题:

protected void getEndTextToRender(FacesContext context,
                                  UIComponent component,
                                  String currentValue)
      throws IOException 

    ResponseWriter writer = context.getResponseWriter();
    assert(writer != null);
    boolean shouldWriteIdAttribute = false;
    boolean isOutput = false;

    String style = (String) component.getAttributes().get("style");
    String styleClass = (String) component.getAttributes().get("styleClass");
    String dir = (String) component.getAttributes().get("dir");
    String lang = (String) component.getAttributes().get("lang");
    String title = (String) component.getAttributes().get("title");
    if (component instanceof UIInput) 
        writer.startElement("input", component);
        writeIdAttributeIfNecessary(context, writer, component);
        writer.writeAttribute("type", "text", null);
        writer.writeAttribute("name", (component.getClientId(context)),
                              "clientId");

        // only output the autocomplete attribute if the value
        // is 'off' since its lack of presence will be interpreted
        // as 'on' by the browser
        if ("off".equals(component.getAttributes().get("autocomplete"))) 
            writer.writeAttribute("autocomplete",
                                  "off",
                                  "autocomplete");
        

        // render default text specified
        if (currentValue != null) 
            writer.writeAttribute("value", currentValue, "value");
        

   // Rest of code omitted 

一个 currentValue 参数被显式传递到从 com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.encodeEnd 调用的方法中

@Override
public void encodeEnd(FacesContext context, UIComponent component)
      throws IOException 

   rendererParamsNotNull(context, component);

    if (!shouldEncode(component)) 
        return;
    

    ResponseWriter writer = context.getResponseWriter();
    assert(writer != null);

    // NOTICE currentValue getter
    String currentValue = getCurrentValue(context, component);
    if (logger.isLoggable(Level.FINE)) 
        logger.log(Level.FINE,
                   "Value to be rendered 0",
                   currentValue);
    
    // NOTICE currentValue
    getEndTextToRender(context, component, currentValue);


如果我们仔细看看 getCurrentValue() 方法

/**
 * @param context the FacesContext for the current request
 * @param component the UIComponent whose value we're interested in
 *
 * @return the value to be rendered and formats it if required. Sets to
 *  empty string if value is null.
 */
protected String getCurrentValue(FacesContext context,
                                 UIComponent component) 

    if (component instanceof UIInput) 
        Object submittedValue = ((UIInput) component).getSubmittedValue();
        if (submittedValue != null) 
            // value may not be a String...
            return submittedValue.toString();
        
    

    String currentValue = null;
    Object currentObj = getValue(component);
    if (currentObj != null) 
        currentValue = getFormattedValue(context, component, currentObj);
    
    return currentValue;


getSubmittedValue() 返回的属性在Restore View 阶段填充(如果Process 验证阶段跳到Render response 阶段)。结果,我们得到了“更新”的值,该值仅从用户传递给 value 属性,其余的保持不变。

如果流程验证阶段成功,不会导致直接跳到渲染响应阶段(如果用户填写任何非空值),则会调用 HtmlInputText 的新构造函数并 样式、标题等。 属性是从头开始填充的。这些属性由托管 bean 填充,该 bean 在阶段更新模型值中使用适当的数据进行了更新。

好的,这不是错误,而是功能。它只是肯定了我的论点,即句子中有异味:“如果请求是回发,并且在应用请求值阶段、流程验证阶段或更新模型值阶段遇到错误,则在渲染响应阶段呈现原始页面” .

如果我真的渴望必填字段的黄色背景,任何线索如何解决这种情况?

更新项目在这里:http://www.221b.cz/so/JSFTester2.zip

【讨论】:

【参考方案3】:

我终于设法让验证工作了。

我使用了可以访问 UIComponent 的验证器。如果验证失败,组件会应用特殊样式。在渲染响应阶段也会考虑这种风格。

那么它的行为如何?

    视图已恢复,包括样式 style="#empty testerBean2.someValue ? 'background-color: yellow;' :''" 验证未通过。因此 testerBean2.someValue 没有更新(因为跳过了更新模型值阶段),但是使用 RequiredValidator 将常量样式设置为 h:inputText - component.setValueExpression("style", new ValueExpressionLiteral("background-color: yellow;", String.班级)); 在渲染响应中,即使 testerBean.someValue 尚未更新,也会应用黄色背景,因为必需的验证器已经设置了常量 new ValueExpressionLiteral("background-color: yellow;", String.class)

我已经实现了自己需要的验证器(灵感来自 http://www.codereye.com/2009/12/validating-empty-text-field-using-jsf.html 的 Bashan 验证器)。

RequiredValidator.java

package cz.test;

import javax.faces.application.FacesMessage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import org.apache.el.ValueExpressionLiteral;

public class RequiredValidator implements Validator 

public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException 
    if (value == null || "".equals(value.toString().trim())) 
        FacesMessage message = new FacesMessage();
        String messageStr = (String) component.getAttributes().get("message");
        if (messageStr == null) 
            messageStr = "Please enter data";
        
        message.setDetail(messageStr);
        message.setSummary(messageStr);
        message.setSeverity(FacesMessage.SEVERITY_ERROR);
        component.setValueExpression("style", new ValueExpressionLiteral("background-color: yellow;", String.class));
        throw new ValidatorException(message);
     else 
        component.setValueExpression("style", new ValueExpressionLiteral("", String.class));
    


我添加的行: component.setValueExpression("style", new ValueExpressionLiteral("background-color: yellow;", String.class));

强制对空字段 (web.xml) 进行 JSF 触发器验证:

....
<context-param>
    <param-name>javax.faces.VALIDATE_EMPTY_FIELDS</param-name>
    <param-value>true</param-value>
</context-param>
....

向 JSF 注册验证器(faces-config.xml):

<validator>
    <validator-id>RequiredValidator</validator-id>
    <validator-class>cz.test.RequiredValidator</validator-class>
</validator>

以及使用所需验证器和 TesterBean2 (index.xhtml) 的网页:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:f="http://java.sun.com/jsf/core">

<h:head>
    <title>Testing page</title>
</h:head>
<h:body>
    <h:form>
        <h:messages/>
        <h:inputText id="fieldValue"                          
                     title="#testerBean2.someValue"
                     value="#testerBean2.someValue"                         
                     style="#empty testerBean2.someValue ? 'background-color: yellow;' : ''">
            <f:validator validatorId="RequiredValidator"/>
            <f:attribute name="message" value="Field is mandatory"/>
        </h:inputText>

        <h:commandButton value="Store" action="#testerBean2.storeValue"/>
        <h:commandButton value="Erase" action="#testerBean2.eraseValue"/>
    </h:form>
</h:body>
</html>

注意:在 h:inputText 中不能使用必需的属性。它会超过所需的验证器。

【讨论】:

以上是关于JSF2 - 为啥渲染响应不重新渲染组件设置?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 vue 组件不重新渲染?

为啥 `useContext` 不重新渲染我的组件?

Vue之重新渲染组件的正确方式

为啥组件在状态更改后不重新渲染。在 react-native 函数组件中

为啥在 React 中,当父组件重新渲染时子组件不会重新渲染(子组件没有被 React.memo 包裹)?

为啥 useState 不触发重新渲染?