19.4. 复合组件Scope-在同一RequestBean的页面里使用多个同源复合组件

19.4.1. 前言

AOM3.1以前,复合组件中所用到的ManagedBean都是通过name一个属性去从reuqet中获取,这样产生的一个问题是,如果我一个页面同时用了多个复合组件的话则会造成这多个复合组件引用的都是同一个ManagedBean,这样所造成的问题就是一个复合组件在后台改动某一处会造成多处一起动,或者是只是最后一个动(后者将前面的覆盖掉了。)于是复合组件的发展受到了限制。

在 request中维护一个Map,对每一次尝试从request中获取managedbean的请求进行分析,如果是复合组件的请求则以这个复合组件的id(如果是嵌套则以多个嵌套的复合组件id按一定规则拼凑成的字符串)和managedbean的name两者共同作为key向这个map里获取,如果没有获取到则新建一个再以两者组成的key向map里put。从而就保证了每个复合组件都有自己的独立的managedbean。

以前开发的复合组件不需要做任何改变还是可以用的,只是没有新的特性而已。

详细原理请访问:http://oldwiki.apusic.net/pages/viewpage.action?pageId=8880567,本文主要介绍新机制下如何去开发一个复合组件。

19.4.2. 如何开发复合组件

下面将结合例子讲解如何开发一个复合组件。本文所有的例子都可以在附件中下载

一. 获取复合组件里的某个组件的值

开发过复合组件的同学知道,在以前的机制下,如果在一个页面放置了多个复合组件,抛开几个managedbean互相干扰不说,要从使用复合组件的页面得到某一个复合组件里的某一个具体的原生组件的值几乎是不可能的,因为不能给这些组件指定id(会造成id重复),也不能指定jsvar(后面的jsvar会把前面的jsvar给覆盖掉),更加不能绑定到managedbean的某一个属性(几个复合组件共用一个managedbean,谁也不知道最后绑定的是哪个组件的值)。而在新机制下要获取这个值就显得容易多了。看下面的例子:

首先,定义一个简单复合组件的页面comp_a.xhtml,我把它放在WEB-INF/fcs/下

<?xml version="1.0" encoding="UTF-8"?>
	<!DOCTYPE HTML PUBLIC "" "">
	<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
	    xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
	    xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
	    renderKitId="AJAX" xmlns:ui="http://java.sun.com/jsf/facelets">
	    <w:head>
	        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	    </w:head>
	    <w:page>
	        <ui:component>
	            <w:textField></w:textField>
	        </ui:component>
	    </w:page>
	</f:view>

够简单了吧?不过此处不得不提到一点是,定义复合组件只能使用<ui:component>标签而不能使用<ui:composition>,这一点请切记!当然,为了让应用能够感知到这个复合组件的存在,需要在WEB-INF或者其子目录下建一个描述文件,我将之命名为comp.taglib.xml,其内容如下:

<!DOCTYPE facelet-taglib PUBLIC
	  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
	  "http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
	<facelet-taglib>
	  <namespace>http://www.apusic.com/jsf/complex</namespace>
	  <tag>
	    <tag-name>comp_a</tag-name>
	    <source>fcs/comp_a.xhtml</source>
	  </tag>
	</facelet-taglib>

然后在web.xml中注册这个描述文件

    <context-param>
	    <param-name>facelets.LIBRARIES</param-name>
	    <param-value>
	      /WEB-INF/comp.taglib.xml
	    </param-value>
	  </context-param>

现在只要在页面中添加 xmlns:cp="http://java.sun.com/jsf/complex"(描述文件中的namespace)来引入这个标签库就可以通过<cp:comp_a>来使用这个复合组件了:

    <w:page title="Insert title here">
	        <w:form>
	            <cp:comp_a id="a"></cp:comp_a>
	            <cp:comp_a id="b"></cp:comp_a>
	        </w:form>
	    </w:page>

大家可以看到,在使用这个组件的时候我分别给它们指定了一个id属性。我觉得很容易理解这一点,要拿到复合组件里面的东西,肯定要使里面的原生组件都有不同点。

现在我们要分别从两个地方去拿到这复合组件里的textField的值了,首先来看一下从客户端怎样获取。客户端无非就是设法获取到每个组件的jsvar值,在平时写原生组件的时候通常我们会给一个原生组件指定一个jsvar,使之使用的不是随机生成的值,比如<w:button jsvar="btnJsvar"/>,这样在客户端就可以通过这个btnJsvar来获取这个button的ext对象引用,然而写过复合组件的同学知道,如果给一个复合组件里的原生组件指定jsvar,势必会造成后面的jsvar把前面的给覆盖掉,这样怎么办呢?这时候就不得不提到复合组件页面内的一个内置对象了,它就是#{complex_var},在定义一个复合组件页面的时候,同学们可以任意的使用这个内置对象。这个内置对象是一个字符串,它是根据复合组件的id生成的,如果你使用复合组件的时候指定了一个id="a",那么这个内置对象便是a,如果不指定,则是一个随机的值(下文还会说明在复合组件嵌套的情况下complex_var是个什么东西)。现在我们对复合组件进行一个小小的调整,即给<w:textField></w:textField>添加一个jsvar属性<w:textField jsvar="#{complex_var}_field"></w:textField>,这样一来,两个复合组件里的文本框就分别有一个jsvar为a_field,b_field,然后怎样去获取它们的值就简单了吧?运行例子一试:

那么从服务器端又要怎样去得到呢?首先我们要说的比较遗憾的一点是,复合组件的managedBean现在还不支持iovc。现在我们给这个复合组件comp_a编写一个managedBean,它的代码如下:

@ManagedBean(name = "comp_aBean", scope = ManagedBeanScope.REQUEST)
	public class Comp_aBean {
	    private String filedValue;
	    public String getFiledValue() {
	        return filedValue;
	    }
	    public void setFiledValue(String filedValue) {
	        this.filedValue = filedValue;
	    }
	}
	

然后再修改一下复合组件页面,为textfield再添加一个value="#{comp_aBean.filedValue}",然后再在test_a.xhtml所对应的managedBean里我们只要得到某个复合组件所对应的Comp_aBean不就解决问题了吗?那么如何去得呢?上文我们提到过,复合组件所对应的managedBean都是以name和复合组件的id(嵌套则是多个拼凑)两者共同作为key存放在request中的一个map里的,我们在使用的时候已经给复合组件指定了id属性,所以要获取是比较容易的。通过以下代码:

    @Action
	    public void serviceGet() {
	        Comp_aBean ca = (Comp_aBean) ComponentContext.getCurrentInstance().get("comp_aBean", "a");
	        Comp_aBean cb = (Comp_aBean) ComponentContext.getCurrentInstance().get("comp_aBean", "b");
	        if(ca != null && cb != null) {
	            Browser.execClientScript(String.format("alert('from service : A:%s ;  B:%s')", ca.getFiledValue(), cb.getFiledValue()));
	        }
	    }

运行结果如下:

二. 复杂一点的复合组件。

如果在复合组件里放置一个DataGrid或者是一棵Tree能不能正常运行呢?我们现在按照上文说的方法再定义一个新的复合组件,对应的页面和managedBean的代码如下:

<ui:component>
	    <h:outputText value="#{fcs.comp_gridBean.outputText}"></h:outputText>
	    <w:dataGrid jsvar="#{complex_var}_grid" height="200" width="400" paged="true" rows="5"
	        value="#{fcs.comp_gridBean.data}" binding="#{fcs.comp_gridBean.grid}" var="test">
	        <w:outputColumn header="A" value="#{test.a}"></w:outputColumn>
	        <w:outputColumn header="B" value="#{test.b}"></w:outputColumn>
	        <ajax:action event="onrowselect" action="#{fcs.comp_gridBean.onrowselect}"></ajax:action>
	    </w:dataGrid>
	</ui:component>
	--------------------------------不华丽但很实用的分割线---------------------------------
	//为了节省篇幅,略去了getters and setters,因为不支持iovc,所以必须提供这两个方法
	@ManagedBean(name = "fcs.comp_gridBean", scope = ManagedBeanScope.REQUEST)
	public class Comp_gridBean {
	    private String outputText;
	    private UIDataGrid grid;
	//getters and setters ......
	    public List<Test> getData() {
	          List<Test> data = new ArrayList<Test>();
	            for (int i = 1; i <= 20; i++) {
	                data.add(new Test("a" + i, "b" + i));
	            }
	        return data;
	    }
	    public void onrowselect(RowSelectEvent event) {
	        Object[] objs = event.getSelectedValues();
	        if (objs.length > 0) {
	            outputText = "行选中事件(来自event.getSelectedValues()):";
	            for (int i = 0; i < objs.length; i++) {
	                Test test = (Test) objs[i];
	                outputText += test;
	            }
	        }
	        objs = grid.getSelectedValues();
	        if (objs.length > 0) {
	            outputText += "\n行选中事件(来自绑定的grid.getSelectedValues()):";
	            for (int i = 0; i < objs.length; i++) {
	                Test test = (Test) objs[i];
	                outputText += test;
	            }
	        }
	    }
	    public class Test {
	        private String a;
	        private String b;
	        public Test(String a, String b) {
	            super();
	            this.a = a;
	            this.b = b;
	        }
	//getter and setters ....
	        @Override
	        public String toString() {
	            return "[a=" + a + ",b=" + b + "];";
	        }
	    }
	}

从上面的代码我们可以得知,复合组件内部的原生组件也可以拥有ajax事件,并且也可以使用组件的binding属性将它绑定到managedBean的对应component属性上(例子中的UIDataGrid grid不为空),引擎会自动判断应该绑定到哪一个managedBean。打开附件中的例子,点击某一行,或者使用Ctrl键选择多行,对应的textArea会展示出选择的记录的信息。运行效果如下:

同样,复合组件里也可以放置一个tree,详请查看附件中例子的/complex/c/test_tree.faces。

三. 嵌套的复合组件

上文已经说过,新机制中不仅支持一个页面放置多个复合组件,而且支持开发一个复合组件的时候使用另一个复合组件,这在一定程度上增加了代码的可重用性。接下来我们就写一个复合组件,它的左边是棵树,右边是一个复合组件,即我们上文所提到的那个datagrid的复合组件(为了能够在外面控制grid的数据,做了少量的修改)。

首先,编写一个复合组件,它使用了另一个复合组件<cp:comp_grid id="aaa"></cp:comp_grid>:

        <ui:component>
	            <layout:panelGrid columns="2">
	                <layout:cell colspan="1" rowspan="1" style="height: 500px;width: 200px">
	                    <w:tree rootVisible="false" value="#{rcs.comp_t_dBean.provider}" width="200" height="500"
	                        expandAll="true" border="false" autoScroll="true" binding="#{rcs.comp_t_dBean.tree}">
	                        <ajax:action event="onselect" action="#{rcs.comp_t_dBean.tree_onselect}"></ajax:action>
	                    </w:tree>
	                </layout:cell>
	                <layout:cell colspan="1" rowspan="1" style="width: 400px;">
	                    <cp:comp_grid id="aaa"></cp:comp_grid>
	                </layout:cell>
	            </layout:panelGrid>
	        </ui:component>
	
	--------------------------------不华丽但很实用的分割线---------------------------------
	
	    public void tree_onselect() {
	        String text = tree.getSelectedNode().getText();
	        Comp_gridBean cgb = (Comp_gridBean) ComponentContext.getCurrentInstance().get("fcs.comp_gridBean", "aaa", this);
	        if(cgb != null) {
	            cgb.setOutputText(text);
	            cgb.resetData(text);
	        }
	    }

由上面的代码可以看出,我们给tree添加了一个onselect的ajax事件,在这个事件当中,尝试去从ComponentContext中获取对应的Comp_gridBean。眼尖的同学可能已经发现了,此处获取复合组件managedBean的方式与上文不同,而是将本身作为一个参数传入。

这是因为我们是在一个复合组件内部去获取一个嵌套复合组件的managedBean,引擎需要从当前复合组件的managedbean中去获取整个命名空间的信息。当然,本文提到的获取ManagedBean的方式只是为了便于理解,在实际开发过程中请调用FacesUtils.getComplexManagedBean(beanName, nsp, parent)来获取,其底层也是调用了本文中所提到的方法。

程序的运行效果如下,点击左边的树,对应的右边的grid的数据会更新。

四。最后重新提一下开发复合组件要注意的事项:

1.复合组件所对应的managedBean暂不支持iovc;

2.为复合组件内部的的原生组件定义页面内比较重要的属性比如jsvar或者是id的时候,使用#{complex_var}_xxx而不要直接使用某个固定的字符串;

3.在任何使用到复合组件的地方(包括开发复合组件和使用复合组件开发普通页面的时候)最好指定id,且复合组件内部的复合组件在指定id的时候不需要使用#{complex_var}前缀。当然如果不指定的话会使用一个按照一定规则生成的值,但这样一来复合组件就失去了可控性。

五. 附件

复合组件例子