用动态元素自动更新 Web 页面
 

2010-01-25 作者:Li Li Lin 来源:IBM

 
本文内容包括:
您可能知道如何在标准 JSF 组件内使用 JavaScript 和 Cascading Style Sheets (CSS) 来隐藏和显示可选的 JavaServer Faces (JSF) 组件。为了实现这个目的,您需要首先识别所有的 JSF 组件并将它们写入到 JSF 页面。但是,如果开发的是包含动态元素的 Web 页面并且这些动态元素直到运行时才知道,那么就不可能这么做。在本文中,了解如何在自动更新 Web 页面的动态元素的同时,清除旧的 UI 元素,以及如何使用 Java™ 代码来添加新的元素并将它们放入到 Web 页面中的适当位置。您还将了解如何将不同的事件处理程序绑定到 Web 页面的不同动态元素、如何注册一个侦听器来侦听服务器端数据的变更以调用页面刷新,以及如何使用 Asynchronous JavaScript and XML (Ajax) 技术来只刷新 Web 页面的动态部分。

简介

有时,您可能需要用动态元素自动更新一个 Web 页面。比如,对于一个投票网站,它需要在数据库接收到新的投票时就立刻更新投票结果,又比如,一个股票网站需要能够定期地更新证券的实时交易数据。投票结果以及实时的交易数据都是动态元素,直到运行时才能知道,但是当服务器端发出信号时,这些元素就应该能被添加或更新。那么,在一个 JSF 应用程序中如何做到这一点呢?

此前的一篇 developerWorks 文章 “联合使用 CSS、JavaScript 和 JSF 精心打造 Ajax 应用程序,第 2 部分: 动态 JSF 表单” 介绍了如何在刷新 Web 页面的同时隐藏和显示可选的 JSF 组件。但是,这篇文章中所介绍的方法并不能解决上述问题。因为该方法需要您标识出所有的 JSF 组件并将它们写入到您的 JSF 页面。如果这些动态元素直到运行时都无法被识别,该怎么办呢?

直到现在,JSF 对这个问题都没有好的解决办法。虽然,您可能熟悉 Java Swing 应用程序,比如 “clock”,其中数据更改会引起 GUI 更新,或者您可能已经在一些基础的 Swing 开发指南中读到过有关如何实现这类应用程序的详细信息,但是这种方法对于我的场景不会奏效。Swing 已经提供了一种成熟的方式来仅仅基于内部的数据状态自动更新 GUI,但是 JSF 并不支持基于服务器端的请求刷新 GUI。如果研究一下 JSF 的标准生命周期,不难发现用户通常需要在 Web 页面上生成一个事件(比如,通过单击一个按钮)来调用 GUI 刷新。这意味着即使动态元素能在运行时创建并被添加到一个 Web 页面,如果没有来自用户的交互,这个 Web 页面仍不能自动刷新。

那么,该如何用动态元素自动更新一个 Web 页面呢?在本文中,我将探讨如下的解决方案:

  • 清除旧的 UI 组件并将新的组件添加到 Web 页面的适当位置
  • 将不同的事件处理程序绑定到 Web 页面的不同元素
  • 注册一个能侦听服务器端变更的侦听器
  • 使用 Ajax 技术只刷新 Web 页面的动态部分,而不是整个页面

监视服务器端的数据变更

为了更好地解释我的解决方案,我将在整篇文章中只采用一个简单的例子。此应用程序是一个进行在线书籍销售的网站。诸如书的类别以及每个类别中的书的数量等库存信息将在这个网站的主页(参见图 1)中显示。

图 1. 在线售书网站的主页
显示在线售书库存的窗口。每个类别中的书的数量显示在类别旁边。

为了反映正确的信息,需要实时同步库存信息与数据库端的数据。由于从库存中添加或删除书的操作会导致服务器端数据的变更,所以必须要监视这些动作。监视变更的方法是添加一个侦听器来查询服务器端数据的变更,并让服务器端在变更发生后通知侦听器。清单 1 显示了该如何将侦听器注册到一个类以及如何从一个类中解除侦听器的注册。

清单 1. 向库存添加和删除侦听器
 
				
public class Inventory{
……
   private Map<String, InventoryListner> listeners = 
                    new HashMap<String,InventoryListner>();
……
   public void register(String id, InventoryListner listener){
	  listeners.put(id, listener);
   }	    
   public void deregister(String id){
	  listeners.remove(id);			               
   }
……
}            

库存侦听器可以通过两种 Java 方法从清单 1 中所示的 Inventory 类中添加和删除。假设所有的库存更改都是添加或删除书籍这类动作的结果,那么可以在每次发生这些动作的时候通知所有注册到 Inventory 类的侦听器。清单 2 显示了如何在发生变更时通知侦听器。

清单 2. 发生变更时通知侦听器
 
				
public class Inventory{
……
     public void addBookItem(String bookName,String auther,String price,
                                                                   String category){
	     //codes for adding books
	    categoryChanged();
     }
	   
      public void removeBookItem(String bookName,String auther,
                                          String price,String category){
	     //codes for deleting books
	     categoryChanged(); 	
      }

      private synchronized void categoryChanged(){
	     for (InventoryListner listener : listeners.values()) {
		  listener.categoryChanged();
	    } 			               
      }
}            

接下来,让受管 bean InventoryBean 实现 InventoryListener 并将其注册到库存数据,以便该 bean 可以在库存数据变更时获得通知。清单 3 显示了如何将这个受管 bean 注册到 Inventory 类。

清单 3. 将受管 bean 注册到 Inventory
 
				
public interface InventoryListner {
	 public abstract void categoryChanged();
   }

   public class InventoryBean implements InventoryListner{
   ……
          private String m_clientId ;
          private InventoryNotifier m_notifier;
          public InventoryBean(){
	        m_notifier = InventoryNotifier.getInstance();
	        if(m_clientId == null) {
          		m_clientId = "bookstore";
        		m_notifier.register(m_clientId, this);
	        }		
         }
	public void categoryChanged() {
	refresh();
	//code for refresh dynamic part via ajax
	}	
……
}            

使用清单 1 到清单 3 中所列的这些方法,可以建立一个面向受管 bean 的框架来监视服务器端数据的变更。工作流程是:当此受管 bean 获得服务器端数据已经变更的通知后,就会调用 InventoryBeancategoryChanged() 方法,数据模型也会被更新。图 2 展示了此框架在数据库和 “Bean 部分” 之间建立了连接。想要在服务器端监视数据变更或从服务器端接收事件的任何应用程序都可以使用这个框架作为模板。

图 2. 业务处理模型
显示 Web 页面从 Bean 到数据库的处理过程的一个工作流图表

更新数据模型和创建动态 GUI 元素

构建了能监视服务器端数据变更的框架之后,如果需要这个 bean 在发生任何变更时都能被通知到,那么就要找到一种方法来更新数据模型和创建动态 GUI 元素。这个过程会在这个受管 bean 的内部发生(参见图 2 的 Bean 层),并且能够被分成两个子过程:更新数据模型和创建 GUI 元素。

更新数据模型

这个子过程由之前在清单 3 中所示的 refresh() 方法调用。清单 4 展示了更新数据模型的方法。使用 refresh() 方法来重新组织库存以确保书籍被分配到正确的类别。因此,在更新了数据模型之后,就可以确保其下没有书的那些类别已被删除并且所有新的类别已被添加。

为了让您能更好地理解 refresh() 方法,我先来简单介绍一下我所使用的自定义的数据结构。我使用 Category 类来保存库存信息。Category 类包含类别名以及以 ArrayList<BookItem>.BookItem 类形式表示的书籍的元数据,这些元数据包括书名、作者、价格和类别。清单 4 展示了该如何更新数据模型。

清单 4. 更新数据模型
 
				
public class InventoryBean implements InventoryListner{
...
       private Inventory m_notifier;
       private Category[] m_category;
       public InventoryBean(){
	      m_notifier = Inventory.getInstance();	
       }
       private void refresh(){
            //reorganize the data model
	      ArrayList<Category> categoryList = m_notifier.reorgnizeCategory();
            // code for converting data to the type used in this bean,
           // ArrayList<Category> to Category[]	
       }
...
}            

创建动态的 GUI 元素

接下来,我将介绍另外的一个子程序:创建动态 GUI 元素。本例中的动态 GUI 元素是一些类别链接(参见图 1)。如果用户单击主页中的一个特定类别,他就会被重定向到一个包含此类别内所有书籍的新的页面。图 3 展示了属于侦探类别的所有书籍。

图 3. 侦探类别的细节
显示属于侦探类的书籍的详细信息。其中包含书名以及书的价钱。

要让这些类别链接发挥作用,需要删除旧的链接、将新的链接插入到网页中的合适位置,并将不同的类别细节信息绑定到不同的类别链接。

插入和删除链接

有两种方式可以删除或插入链接。一种方法是查找 JSF 组件树中的动态元素的父组件,然后删除或插入元素。如果这些动态元素的父组件正在发生改变,就应该采用这种方法。另一种方法是直接将动态元素绑定到 Web 页面。这种方法比第一种方法简单,因为无需寻找 JSF 组件树中的父节点。不过,由于太过简便,这种方法具有一些限制:它只能用在所要删除或插入的元素具有固定的父组件并且在运行时之前父组件就已知的情况下。我之所以选用了这种方法(参见清单 5)是因为在本例中这些类别链接的父组件是固定的,并且也是预先定义了的。

清单 5. 创建/更新 GUI 组件并将不同的组件绑定到不同的动作处理程序
 
				
category.jsp
……
<f:view> 
    <h:form id="helloForm"> 
     ……
      <h:panelGrid id="title">	   
	 <h:outputText id = "hello_title" value="Inventory"/>
	    <a4j:outputPanel  id = "book" 
                           binding = "#{InventoryBean.categorygrid}"/>
     ……
      </h:panelGrid>
    </h:form>
</f:view>

public class InventoryBean implements InventoryListner {
……
   private Category[] m_category;
   public HtmlAjaxOutputPanel getCategorygrid() {
	updateGUI();
	return categorygrid;
   }

   public void setCategorygrid(HtmlAjaxOutputPanel categorygrid) {
	this.categorygrid = categorygrid;
}

   private void updateGUI(){
	categorygrid.getChildren().clear();
	if (m_category != null) {
	    int num = m_category.length;
	    for (int index = 0; index < num; index++) {
		HtmlPanelGrid categorySubgrid = 
                      JSFUtil.getLinkgrid("Bookstore_sublink" + index,
		     "#{InventoryBean.category[" +index+ "].categoryLabel}",
		     "#{InventoryBean.category[" +index+ "].onClickAction}");
		categorygrid.getChildren().add(categorySubgrid);
	    }
	}
   }
……
}            

如您所见,category.jsp 文件内的 updateGUI() 代码行就是要绑定受管 bean 中的动态元素。它清除了之前所创建的所有动态元素、基于新的数据模型创建了新的动态元素并将它们添加到预定义的父元素中。

将不同的行为绑定给不同的链接

现在,让我们探讨一下该如何将不同的类别细节信息绑定到不同的类别链接。我对一个数组进行了迭代,将每个元素转变为一个 GUI 组件,并将它插入到 JSF 组件树。我的思路是把所有类别插入到一个数组,其中每个类别作为数组中的一个元素。每个元素都有一个方法来返回其类别的标签,用另一个方法来绑定单击动作。通过让每个元素都具有可以将它与其他元素区分开来的独有的类别信息,我就能确保对于每个元素均只有一个惟一的行为被绑定到 “onclick” 动作。

updateGUI() 内,"Bookstore_sublink" + index 是此类别链接的 ID。"#{InventoryBean.category[" + index+ "].categoryLabel}" 是此类别链接的标签。"#{InventoryBean.category[" + index+ "].onClickAction}" 是绑定到此类别链接的动作。getCategoryLabel() 方法被用来返回链接标签,onClickAction() 绑定单击动作(参见清单 6)。

清单 6. 值和动作绑定方法
 
				
public class Category {
……
  private String category;
  private ArrayList<BookItem> bookitems;

  public String getCategoryLabel(){
	if(bookitems.size() <2){
		return bookitems.size() + " " + category;
	}else{
		return bookitems.size() + " " + category+"(s)";
	}
  }
	
public String onClickAction(){		
	HttpSession session =
               (HttpSession)JSFUtil.getFacesContext().
                         getExternalContext().getSession(true);
	        session.setAttribute("CATEGORY", this);
	        return "success";    
	}
……
}            

重定向 Web 页面

本节介绍了如何基于所单击的链接将用户重定向到一个新的页面。我使用 JSF 导航规则来重定向页面。OnClickAction() 方法返回 “success” 以开始这个动作。通过发送到 Httpsession 的数据来为新页面提供内容。数据由受管 bean DetailBean 从新页面的 Httpsession 检索。之后,DetailBean 再相应地创建其 GUI 组件。

清单 7 给出了这些详细的实现。“detail.jsp” 是用户将被重定向到的新页面。getDetailgrid() 是 detail.jsp 内的 DetailBean 的一部分,它被绑定到一个方法,该方法能创建此页面内的动态元素。在这个方法中,首先获得应该显示的类别数据,然后再使用 populate() 方法相应创建 GUI 内容。您可以研究 populate() 了解如何实时创建动态 GUI 元素,甚至进行页面布局。所有的页面信息都将由类别数据从 Httpsession 传递过来,所以,理论上讲,放入 Httpsession 的数据决定了新页面的外观。

清单 7. 将用户重定向到详细信息页面
 
				
detail.jsp
……
<f:view> 
    <h:form id="detailForm">
        <h:panelGrid id="list">
	   <h:outputText id = "book_list" value="#{DetailBean.title}"/>
		<h:panelGrid id = "detail" binding = "#{DetailBean.detailgrid}"/>
        </h:panelGrid>
        <h:commandButton id="back" value="Back" action="success"/> 
    </h:form>
</f:view>

public class DetailBean {
……
    private HtmlPanelGrid detailgrid = null;
    private Category cat;
    public HtmlPanelGrid getDetailgrid() {
	if(detailgrid == null){
		detailgrid = new HtmlPanelGrid();
	}
	detailgrid.getChildren().clear();
	HttpSession session =
        (HttpSession)JSFUtil.getFacesContext().getExternalContext().getSession(true);
	cat = (Category)session.getAttribute("CATEGORY");
	session.removeAttribute("CATEGORY");
	populate(detailgrid);	
	return detailgrid;
    }
    public void setDetailgrid(HtmlPanelGrid detailgrid) {
	this.detailgrid = detailgrid;
    }

    private void populate(HtmlPanelGrid parent) {
	if (cat != null) {
	    String category = cat.getCategory();
	    ArrayList<BookItem> items = cat.getBookitems();
	    if (category.equals("News paper")) {
                   //create GUI for News paper category.
             }else if (category.equals("Magazine")) {
                   //create GUI for Magazine category.
             }else{
                   //create GUI for other categories.
             }
    
}            

到目前为止,您已经了解了如何更新数据模型以及如何创建动态 GUI 元素。讨论了三个方面的内容 — 如何插入元素以及如何从 Web 页面的合适位置删除元素、如何将不同的行为绑定到不同的元素以及如何重定向到一个 Web 页面。您不妨试着理解三者之间的关系,并针对自己的开发场景选择您所需要的部分。

使用 Ajax 刷新 Web 页面的动态元素

在本节中,为了刷新页面的动态部分,我在图 2 所示的 “Bean” 和 “GUI” 层之间建立起了一种联系。我使用 RichFaces 的 Ajax4jsf 进行刷新。RichFaces 是一种开源框架,可以无需借助 JavaScript 即可将 Ajax 功能添加到现有的 JSF 应用程序。通过 Ajax4jsf,我克服了当前 JSF 不支持任何服务器端页面刷新的限制,而且我能满足只刷新所需内容的要求。

注册 RichFaces

安装 RichFaces 之后,通过添加清单 8 中的这些代码行可以更改 web.xml 文件来注册 RichFaces。

清单 8. 注册 RichFaces
 
				
<!-- Plugging the "Blue Sky" skin into the project -->
<context-param>
   <param-name>org.richfaces.SKIN</param-name>
   <param-value>blueSky</param-value>
</context-param>
<!-- Making the RichFaces skin spread to standard HTML controls -->
<context-param>
      <param-name>org.richfaces.CONTROL_SKINNING</param-name>
      <param-value>enable</param-value>
</context-param>
<!-- Defining and mapping the RichFaces filter -->
<filter>
   <display-name>RichFaces Filter</display-name>
   <filter-name>richfaces</filter-name> 
   <filter-class>org.ajax4jsf.Filter</filter-class> 
</filter>
<filter-mapping> 
   <filter-name>richfaces</filter-name>
   <servlet-name>Faces Servlet</servlet-name>
   <dispatcher>REQUEST</dispatcher>
   <dispatcher>FORWARD</dispatcher>
   <dispatcher>INCLUDE</dispatcher>
</filter-mapping>            

在 Web 页面上所做的变更

注册了 RichFaces 之后,需要将来自清单 9 的那些标记添加到这个 category.jsp 文件以实现 “逆向 ajax”,即将数据从服务器端推到客户端并使用 Ajax 技术来刷新此页面。

清单 9. 将数据推到 Web 页面
 
				
...
<f:view> 
    <h:form id="helloForm"> 
        <a4j:region>
       	   <a4j:push reRender="book" eventProducer="#{InventoryBean.addListener}"/>
     	</a4j:region>
    	<h:panelGrid id="title">
	   <h:outputText id = "hello_title" value="Inventory"/>
	   <a4j:outputPanel  id = "book" binding ="#{InventoryBean.categorygrid}"/>
	   <h:outputText id = "summary" 
                    value="#{InventoryBean.categoryNumber}"></h:outputText>
	</h:panelGrid>	
    </h:form>
</f:view>            

请注意 a4j:push 标记。利用 eventProducer="#{InventoryBean.addListener}",此 Web 页面将一个侦听器注册到这个受管 bean,以便这个受管 bean 能够在需要的时候刷新 Web 页面。reRender = "book" 意味着在服务器端数据被推到页面后,只有 ID 为 “book” 的组件才被刷新。a4j:outputPanel 允许对页面区域进行标记,该页面区域通过 Ajax 响应被更新。

在受管 bean 内所做的更改

在这个受管 bean 内,应该注册 PushEventListener 以便一有推出事件发生就能将服务器端数据推出到客户端。借助 eventProducer 属性,可以将这个方法绑定到 Web 页面。推出事件由 categoryChanged() 方法中的 this.listener.onEvent(new EventObject(this)); 生成,每当服务器端数据发生改变时都会调用该方法。我之前谈论过 categoryChanged(),清单 10 给出了它的具体实现。

清单 10. 注册 eventProducer 并推出数据
 
				
public class InventoryBean implements InventoryListner{
……
     public void addListener(EventListener listener) {
	synchronized (listener) {
	   if (this.listener != listener) {
		this.listener = (PushEventListener) listener;
	   }
	}
     }

     public void categoryChanged() {
	refresh();
	//code for refresh dynamic part via ajax
	this.listener.onEvent(new EventObject(this));
     }
}            

现在,就可以从服务器端进行 Ajax 刷新了。将这种技巧与之前讨论的那些技巧结合起来,就能够将图 2 中所示的 “Database”、“Bean” 和 “GUI” 层连接起来。正如我已经讨论过的所有其他方法一样,这种方法也可以在任何适当的场合独立使用。

结束语

JSF 是一种十分方便的 Web 框架,可用来生成 HTML 页面、接收用户输入以及管理导航流。要在 JSF 内刷新一个页面,用户通常需要在 Web 页面上执行一些动作来生成 HTTP 请求,由 HTTP 响应回复该请求,进而导致页面的刷新。由服务器端触发 Web 页更新在 JSF 内并非易事。本文提供了这样一个解决方案,不仅能基于服务器端的请求自动更新 Web 页面,并且还可以同步服务器数据与 Web 页面的动态元素,这些动态元素在运行时创建并不断更改。

参考资料

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。

资源网站: UML软件工程组织