JSF 学习之 编写自定义控件
参考资料:
1.Sun的 Java EE 5 turorial的第十三章Creating Custom UI Components
http://java.sun.com/javaee/5/docs/tutorial/doc/bnavg.html
2. http://www.coreservlets.com/JSF-Tutorial/ 上的section 14
3. 《core JavaServer™ Faces》Chapter 9. Custom Components
Prentice.Hall.PTR.Core.JavaServer.Faces.Jun.2004.eBook-DDU.chm
4. 《Java.Server.Faces编程(中文).pdf》 第十二,十三章
5. Apache 的子项目myfaces 的源代码。
http://myfaces.apache.org/
myfaces是一个开源的JSF实现项目。Tomahawk 是myfaces的一个子项目,里面包含像日历选择,popup弹出框,javascript菜单,tree树形控件,html在线编辑器等常用控件。对 jsf基本控件是一个很好的扩展。两者的代码都可以在 http://apache.mirror.phpchina.com/myfaces/source/ 找到。高手说源码之前了无秘密,想自己写自定义控件的,可以下载下来看看,看别人高手是怎么做的吧。
6. Java Enterprise Edition 5 API Documentation
可以在这里下载到http://www.allimant.org/javadoc/index.php
我摸索了好一段时间终于实现了一个用户登录状态条控件。一个简单的jsf自定义控件,主要是想看看是怎么实现自定义控件的属性,事件等功能的。
在JSF中编写一个自定义控件(自定义tag ?)大概要实现一个 component类、一个tag 类、还有定义控件属性的 *.tld文件。 如果你要把控件显示处理部分独立出来的话,可能还要实现一个Rendererr类。
以下是详细代码,所有代码基于JSF1.2 :
1. component类 ,在文件 UIUserBar.java 中实现。
package widebright;
import javax.faces.component.*;
import javax.faces.context.*;
import java.io.*;
import java.util.*;
import javax.faces.context.FacesContext;
import javax.el.ValueExpression ;
import javax.el.MethodExpression;
import javax.faces.event.*;
import javax.el.ELException;;
//继承 UIComponentBase 这个face控件基本类,如果想支持action 这个常用jsf控件属性,那么要实现ActionSource2 这个接口才行。
public class UIUserBar extends UIComponentBase // implements ActionSource2
{
//不是实现 id 等基本的属性了,因为jsf里面默认已经实现了id 等基本的属性的支持了
private ArrayList<URL> urls = null;
private Boolean userlogin = false;
MethodExpression _userLeaveListener =null;
@Override
public String getFamily() {
//Specifying the Component Family
//If your custom component class delegates rendering,
//it needs to override the getFamily method of UIComponent
//to return the identifier of a component family, which is used to
//refer to a component or set of components that can be rendered by a
//renderer or set of renderers. The component family is used along
//with the renderer type to look up renderers that can render the component.
//如果没有另外单独实现renderer,返回null就行了。 如果要自己提供renderer ,
//必须返回一个在配置文件中component-family 节注册的renderer
return(null);
}
//decode 中处理从用户那里传过来的 值
@Override
public void decode(FacesContext context) {
super.decode(context);
//这里可以获取用户输入数据了, 因为这个控件没有输入内容的,就不实现了
String clientId = this.getClientId(context);
Map paramMap = context.getExternalContext().getRequestParameterMap();
// if (paramMap.containsKey(clientId) ){
this.queueEvent(new UserLeaveEvent(this));
// }
}
//需要在encodeBegin 或者encodeEnd 中输出 html标签
//不过好像说是带 输入 的就要在encodeEnd中实现实现,
@Override
public void encodeBegin(FacesContext context) throws IOException {
if (context == null)
{
throw new NullPointerException();
}
ExternalContext externalContext= context.getExternalContext();
Map sessionMap = externalContext.getSessionMap();
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", this );
writer.writeAttribute("id", this.getId(), null);
writer.startElement("nobr", this );
//输出链接
if (urls != null){
for (URL tmp : urls ){
writer.startElement("a", this );
writer.writeAttribute("href", tmp.url,null);
writer.writeAttribute("target", "_blank",null);
writer.write(tmp.text);
writer.endElement("a");
writer.write(" | ");
}
}else {
ValueExpression expression = getValueExpression("urls");
if (expression != null)
{
ArrayList<URL> tmpUrls = (ArrayList<URL>)expression.getValue(getFacesContext().getELContext());
for (URL tmp : tmpUrls ){
writer.startElement("a", this );
writer.writeAttribute("href", tmp.url,null);
writer.writeAttribute("target", "_blank",null);
writer.write(tmp.text);
writer.endElement("a");
writer.write(" | ");
}
}
}
writer.startElement("a", this );
writer.writeAttribute("href", ");
writer.writeAttribute("target", "_blank",null);
writer.write("widebright的百度空间");
writer.endElement("a");
writer.write(" | ");
if (sessionMap.containsKey("username")) // 用户已经登录
{
writer.startElement("a", this );
writer.writeAttribute("href", ");
writer.writeAttribute("target", "_blank",null);
writer.write("退出登录");
writer.endElement("a");
}
else //用户没有登录
{
writer.startElement("a", this );
writer.writeAttribute("href", ");
writer.writeAttribute("target", "_blank",null);
writer.write("登录");
writer.endElement("a");
}
writer.endElement("nobr");
writer.endElement("div");
}
//需要在encodeBegin 或者encodeEnd 中输出 html标签
//不过好像说是带 输入 的就要在这里实现,不然在encodeBegin 实现也是可以的。
@Override
public void encodeEnd(FacesContext context) throws IOException {
//都在 encodeBegin 里面 实现了,这里就不写了, 也可以留一部分到这里再做也可以得
}
//saveState 和 restoreState 是为了保持持久状态,一般不用做这个都可以的了。如果你不需要这种功能的话
@Override
public Object saveState(FacesContext facesContext)
{
Object[] values = new Object[30];
values[0] = super.saveState(facesContext);
values[1] = urls;
values[2] = _userLeaveListener;
values[3] = userlogin ;
return values;
}
@Override
public void restoreState(FacesContext facesContext, Object state)
{
Object[] values = (Object[])state;
super.restoreState(facesContext,values[0]);
urls = (ArrayList<URL>)values[1];
_userLeaveListener = (MethodExpression ) values[2] ;
userlogin= (Boolean)values[3];
}
public MethodExpression getUserLeaveListener()
{
return _userLeaveListener ;
}
public void setUserLeaveListener(MethodExpression userLeaveListener )
{
_userLeaveListener = userLeaveListener;
}
public void broadcast(FacesEvent event) throws AbortProcessingException
{
super.broadcast(event);
MethodExpression userLeaveListenerExpression = getUserLeaveListener();
if (userLeaveListenerExpression != null && event instanceof UserLeaveEvent )
{
try
{
userLeaveListenerExpression.invoke(getFacesContext().getELContext(), new Object[]{event});
}
catch (ELException e)
{
Throwable cause = e.getCause();
if (cause != null && cause instanceof AbortProcessingException)
{
throw (AbortProcessingException)cause;
}
else
{
throw e;
}
}
}
}
public ArrayList<URL> getUrls(){
if (urls!= null){
return urls;
}
ValueExpression expression = getValueExpression("urls");
if (expression != null)
{
return (ArrayList<URL>)expression.getValue(getFacesContext().getELContext());
}
return null;
}
public void setUrls(ArrayList<URL> urls) {
this.urls = urls;
}
public boolean isUserlogin() {
if (userlogin != null) return userlogin.booleanValue();
ValueExpression expression = getValueExpression("userlogin");
Boolean v = expression != null ? (Boolean) expression.getValue(getFacesContext().getELContext()) : null;
return v != null ? v.booleanValue() : false;
}
public void setUserlogin(boolean userlogin) {
this.userlogin = Boolean.valueOf(userlogin);
}
}
getFamily 函数定义类型。
decode 函数处理用户输入,典型的可以在整理判断用户返回里面有没有自己控件id,然后使用queueEvent 函数触发事件。
encodeBegin,encodeEnd 生成html编码,如果没有打算自己边学Renderer类的,就是在这里生成自己控件的呈现html代码了。这两个函数选择一个就可以完成功能了,不过有特殊情况一定要使用encodeEnd才行。
saveState 和restoreState 是保持控件状态。
broadcast 应该是事件处理的,不了解到自己看一下jsf的生命周期图。我自己是通过MethodExpression 的 invoke方法来调用事件的方法了。
剩下的都是为了支持 控件 属性/方法 的代码了。为了自己的属性支持JSF表达式可以使用ValueExpression 和MethodExpression 类型的变量了。具体看我前面列出来的参考资料。
2. Tag 类。
Tag类负责把jsp页面上的属性映射到 前面的componet 类对应的属性。 就是做componet 类 和jsp文件的中介了。主要是为了实现空间属性而写的getter和setter方法。可以看到大量使用ValueExpression和MethodExpression ,都是为了支持JSF表达式属性的的。SetProperties 方法就是把 属性映射到componet 类 的了。注意: id 等基本属性,默认在jsf中已经实现支持了,所以就不用写额外的get/set代码了,只要在 *.tld中文件中定义就行了。
UserBarTag.java文件内容:
package widebright;
import java.util.ArrayList;
import javax.faces.webapp.*;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.el.ValueExpression ;
import javax.el.MethodExpression;
//因为UIComponentELTag 也实现了jsp的支持,所以要引用 jsp-api.jar el-api.jar 等包才行,在Tomcat6.0的lib目录下找的到,
//所以要在buildpath里面添加tomcat的lib目录, 然后就可以编译成功了
//另外jsf文档建议 JSF1.2 里面使用UIComponentELTag 而不是以前的UIComponentTag 类了 (UIComponentTag 是for jsf 1.1的)
//Tag 类负责创建 omponent 类(组件类) 并且把 页面书写的标签对应的属性值传给 omponent 类对应的属性。
public class UserBarTag extends UIComponentELTag {
//attributes id, rendered and binding are handled by UIComponentTag
//UIComponentTag 中默认已经提供了对 id ,binding等基本属性的支持了。所以自己写的UserBarTag 子类
//里面就不用再多做一遍了。
private ValueExpression urls =null;
private ValueExpression userlogin =null;
private MethodExpression userleave;
// public boolean getIsUserLogin(){
// return this.isUserLogin;
// }
public MethodExpression getUserleave() {
return userleave;
}
public void setUserleave(MethodExpression userleave) {
this.userleave = userleave;
}
public ValueExpression getUrls() {
return urls;
}
public void setUrls(ValueExpression urls) {
this.urls = urls;
}
public String getComponentType() {
// Associates tag with component in faces-config.xml
return("UserBar");
}
public String getRendererType() {
// Component renders itself in encodeBegin,
// so return null.
return(null);
}
public void release() {
// always call the superclass method
super.release();
// divId = null;
urls = null;
}
public void setProperties(UIComponent component) {
// always call the superclass method
super.setProperties(component);
// component.getAttributes().put("isUserLogin", isUserLogin);
// 在jsp页面上的id属性,默认已经有实现了,所以在自己的UserBarTag,UIUserBar类里面就不用 写 id属性的实现了。
// String a = this.getId();
// String b = component.getId();
if (urls !=null)
{
if(urls.isLiteralText()) { //看有没有包含表达式
String text =urls.getExpressionString();
ArrayList<URL> arrayUrls = new ArrayList<URL>();
String [] rows = text.split(";");
for (int i =0 ;i< rows.length ; i++){
String [] url = rows[i].split(",");
arrayUrls.add(new URL(url[0],url[1], url[2]) );
}
component.getAttributes().put("urls", arrayUrls);
} else {
//可以直接在这里就获取ValueExpression的值,然后就可以不用在UIuserBar中用getValueExpression("urls")来获取值了
//自己选择用什么方法把
// ArrayList<URL> arrayUrls = new ArrayList<URL>();
// arrayUrls = ( ArrayList<URL>) urls.getValue(getFacesContext().getELContext());
// component.getAttributes().put("urls", arrayUrls);
//可以直接用setValueExpression 传过去,但这样要在UIuserBar中用getValueExpression("urls"); 来处理
component.setValueExpression("urls", urls);
}
}
if ( userlogin !=null)
{
if (userlogin .isLiteralText())
{
component.getAttributes().put("userlogin", Boolean.valueOf(userlogin.getExpressionString()));
}
else
{
component.setValueExpression("userlogin", userlogin);
}
}
if ( userleave !=null)
{
if (userleave .isLiteralText())
{
}
else
{
((UIUserBar)component ).setUserLeaveListener(userleave);
}
}
}
public ValueExpression getUserlogin() {
return userlogin;
}
public void setUserlogin(ValueExpression userlogin) {
this.userlogin = userlogin;
}
}
3. UserBar.tld 文件,声明控件属性,主要是给jsp编辑器等提供自动化帮助等作用的吧。注意以下属性,方法是怎么定义的, <uri> 那个也很有意思,使用的时候就知道了,呵呵。可以参考http://java.sun.com/javaee/5/docs/tutorial/doc/bnamu.html 来获取tld文件的说明。Tld文件最后应该放在 web-inf目录下的吧。
<?xml version="1.0" encoding="UTF-8" ?>
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>2.1</jsp-version>
<short-name>userbar</short-name>
<uri>http://hi.baidu.com/widebright</uri>
<tag>
<description>用于在网页开头输出用户登录等链接</description>
<name>userBar</name>
<tag-class>widebright.UserBarTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>id</name>
</attribute>
<attribute>
<description>
The ValueExpression linking this component to a property in a backing bean
</description>
<name>
binding
</name>
<required>
false
</required>
<deferred-value>
<type>
widebright.UIUserBar
</type>
</deferred-value>
</attribute>
<attribute>
<name>urls</name>
<description>Additional links to display</description>
<required>false</required>
<deferred-value>
<type>java.util.ArrayList</type>
</deferred-value>
</attribute>
<attribute>
<name>userlogin</name>
<description>If the user has logined .</description>
<required>false</required>
<deferred-value>
<type>java.lang.String</type>
</deferred-value>
</attribute>
<attribute>
<name>userleave</name>
<required>false</required>
<description>User leave event handler</description>
<deferred-method>
<method-signature>
void userLeaveListener(widebright.UserLeaveEvent)
</method-signature>
</deferred-method>
</attribute>
</tag>
</taglib>
4. 其他的还有一个自己写的辅助类url.java
package widebright;
import javax.faces.component.UIComponent;
import javax.faces.event.FacesEvent;
import javax.faces.event.FacesListener;
import javax.faces.event.PhaseId;
public class UserLeaveEvent
extends FacesEvent
{
private static final long serialVersionUID = -2514646300612129099L;
public UserLeaveEvent (UIComponent component)
{
super(component);
setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
}
public boolean isAppropriateListener(FacesListener listener)
{
return listener instanceof UserLeaveListener;
}
public void processListener(FacesListener listener)
{
((UserLeaveListener)listener).processUserLeave(this);
}
}
为了提供事件而写的Event 和listener类
UserLeaveEvent.java
package widebright;
import javax.faces.component.UIComponent;
import javax.faces.event.FacesEvent;
import javax.faces.event.FacesListener;
import javax.faces.event.PhaseId;
public class UserLeaveEvent
extends FacesEvent
{
private static final long serialVersionUID = -2514646300612129099L;
public UserLeaveEvent (UIComponent component)
{
super(component);
setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
}
public boolean isAppropriateListener(FacesListener listener)
{
return listener instanceof UserLeaveListener;
}
public void processListener(FacesListener listener)
{
((UserLeaveListener)listener).processUserLeave(this);
}
}
UserLeaveListener.jave:
package widebright;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesListener;
public interface UserLeaveListener {
public abstract void processUserLeave(UserLeaveEvent tabChangeEvent)
throws AbortProcessingException;
}
如何使用这个自定义控件,参考第二篇文章