在 ADF Faces 富客户端中实现自动提示功能

没有哪个组件能比自动提示组件更好地体现 Ajax 和 Web 2.0 的理念。自动提示(又被称作自动完成)是指根据用户在文本域中的输入在一个下拉列表中显示一系列相关值的功能。但自动提示组件本身并不能代表新一代的 Web,不过其行为却很好地展示了局部页面更新功能。本文将详细介绍在 Oracle JDeveloper 11g 的 ADF Faces 富客户端中实现自动提示的方法。

作者:Oracle Corporation 的 Frank Nimphius 和 Maroun Imad
2009 年 4 月 9 日

简介

ADF Faces 富客户端自带的自动提示组件是针对 Oracle JDeveloper 的将来版本规划的新特性之一。但正如前面所述,本文关注的不是这个组件,而是这个功能。此功能可通过 ADF Faces 富客户端现有的手段实现。本文将详细介绍第一个原型的架构和 JDeveloper 11g 工作区,我们计划在以后某个时间点将这个原型增强为一个声明式组件。我们决定了通过本文发布我们的原型,以指导 ADF Faces 客户手动实现提示功能。

架构

自动提示功能需要客户端实现和服务器端实现,这两者使用异步通信通道彼此进行通信。在服务器端,我们使用逻辑来提供值列表并通过用户的每次击键来筛选列表。所选择的值列表将字符串显示为标签和值。在客户端,当然是使用 JavaScript 来监听用户键盘输入,从而显示提示列表并将用户输入的字符作为有效载荷发送到服务器以筛选数据。

一个可怜人的流程图

在该版本的自动提示中,使用 af:inputTextField 作为数据输入组件。只要用户开始在域中进行输入,af:clientListener 组件就会捕获一个 JavaScript 事件,然后将该事件传递给 JavaScript 函数以处理请求。JavaScript 函数查看传入的键代码,如果是有效字符,则在文本域下方打开弹出式对话框(如果尚未打开)。列表框通过一个托管 bean 查询列表数据,我们将这个托管 bean 添加为视图和模型之间的一个抽象层,从而允许通过 ADF、POJO 及类似对象读取列表数据。事实上,我们需要这个抽象层来保持提示功能的通用性,因为我们希望开发人员就所获取数据的缓存策略做出决定。在该示例中,我们使用 ADF 业务组件来查询数据,在这里,我们可选择让该组件从内存执行后续查询,而不是进行数据库查询。列表数据刷新后,会将列表组件作为局部目标添加,以便在请求结束时刷新。输入焦点又回到输入文本域中,以便用户为我们提供下一个字符。

ADF 业务组件

由 ADF 业务组件应用程序模块上提供的一个客户端方法来检索提示列表。该方法用字符串作为输入参数来查询作业视图对象,然后向客户端返回一个字符串列表。

托管 Bean

这个原型中使用了两个托管 bean:SuggestList 和 DemoAdfBcSuggestModel。DemoAdfBcSuggestModel 抽象化 ADF 业务组件查询,并将字符串列表返回给 SuggestList bean 以向客户端提供结果。如果您打算根据您的需求对此示例进行自定义,那么您需要将 DemoAdfBcSuggestModel bean 替换为您的实现。SuggestList 提供两个主要方法:

public ArrayList<SelectItem> getJobList()

该方法填充提示列表。通过 f:selectItems 元素引用提示列表,该元素是 af:popup 组件用来向用户提示值的 af:selectOneListbox 元素的子元素。

  
                              
public ArrayList<SelectItem> getJobList() {
//first time query is against database
if (jobList == null){
jobList = new ArrayList<SelectItem>();
List<String> filteredList = null;
filteredList = suggestProvider.filteredValues(srchString, false);
jobList = populateList(filteredList);
}
return jobList;
}






private ArrayList<SelectItem> populateList(List<String> filteredList) {
ArrayList<SelectItem> _list = new ArrayList<SelectItem>();
for(String suggestString : filteredList){
SelectItem si = new SelectItem();
si.setValue(suggestString);
si.setLabel(suggestString);
_list.add(si);
}
return _list;
}
public void doFilterList
doFilterListaf:serverListener
  
                              
public void doFilterList(ClientEvent clientEvent) {
// set the content for the suggest list
srchString = (String)clientEvent.getParameters().get("filterString");
List<String> filteredList = suggestProvider.filteredValues(srchString, true);
jobList = populateList(filteredList);
RequestContext.getCurrentInstance().addPartialTarget(suggestList);
}

每次对 doFilterList 方法的调用结束时都会刷新 af:selectOneListbox 以填充新的列表值。

JSF 页面标记

提示域分配了两个 af:clientListener 组件来监听 keyUp 和 keyDown 事件。提示域上的所有键盘事件都会发送到 handleKeyUpOnSuggestField JavaScript 函数,该函数采用事件对象作为输入参数。事件对象提供有关事件源、接收击键的组件以及所按下的键代码的信息。使用 ADF Faces RC 客户端 JavaScript API,我们无需应对浏览器差异,从而大大减轻了我们的工作。

<!-- START Suggest Field -->
<af:inputText id="suggestField"
  clientComponent="true"
  value="#{bindings.JobId.inputValue}"
  label="#{bindings.JobId.hints.label}"
  required="#{bindings.JobId.hints.mandatory}"
  columns="#{bindings.JobId.hints.displayWidth}"
  maximumLength="#{bindings.JobId.hints.precision}"
  shortDesc="#{bindings.JobId.hints.tooltip}">
 <f:validator binding="#{bindings.JobId.validator}"/>
  
                              
<af:clientListener method="handleKeyUpOnSuggestField"
type="keyUp"/>
<af:clientListener method="handleKeyDownOnSuggestField"
type="keyDown"/>
</af:inputText> <!-- END Suggest Field -->
The suggest list is defined as follows
<!-- START Suggest Popup --> 
<af:popup id="suggestPopup" contentDelivery="immediate" animate="false" clientComponent="true">
<af:selectOneListbox id="joblist" contentStyle="width: 250px;"
                     size="15" valuePassThru="true"
                     binding="#{SuggestListBean.suggestList}">
  <f:selectItems value="#{SuggestListBean.jobList}"/>
  
                              
<af:clientListener method="handleListSelection" type="keyUp"/>
<af:clientListener method="handleListSelection" type="click"/>
</af:selectOneListbox>
<af:serverListener type="suggestServerListener"
method="#{SuggestListBean.doFilterList}"/>
</af:popup> <!-- END Suggest Popup -->

af:selectOneListbox 定义了两个 af:clientListener,它们监听 KeyUP 和 click 事件,以确定用户是选择一个值(Enter 键,单击)还是取消操作(Esc 键)。af:serverListener 是从某个 JavaScript 中调用的,当用户在提示输入域中输入新字符时便会执行该 JavaScript。af:serverListener 调用 SuggestList bean 中的 doFilterList 方法。

JavaScript

JavaScript 受到 Ajax 的欢迎,并且也是一种与 ADF Faces 富客户端一起使用的选择。不同于先前版本的 ADF Faces,ADF Faces 富客户端具有一个大体上是在内部使用的真实客户端框架,但是也提供公共函数以防止用户攻击 DOM 树。

 

免责声明开始

在进行 JavaScript 编程之前,我们应注意,在 ADF Faces 富客户端中使用 JavaScript 是次佳解决方案。最佳解决方案始终是使用 JavaServer Faces 编程 API,因为这样我们可以有很好的机会看到从一个版本迁移到另一个版本,其中还包括目前我们不知道的要出现的新浏览器版本。但是某些用例(如提示功能)需要 JavaScript 在需要的地方放入逻辑,在可能的地方减少网络忘返次数。

免责声明结束

 

 

在此版本的 Oracle JDeveloper 11g 中,将 JavaScript 添加到 af:document 元素的 metacontainer facet。一旦我们了解了 JDeveloper 11g R1 的高效性,代码的此部分即可更改为新的 af:resource 元素。

                               
<f:facet name="metaContainer">
<af:group>
<![CDATA[
<script>
//******************************************************************
//PROTOTYPE: AUTO SUGGEST
//STATUS: 1.0
//authors: Frank Nimphius, Maroun Imad
//Functionality
//====================================
// Implemented in v 1.0
//1 - Suggest window opens JOB_ID field
//2 - Initial query is to database, follow up queries are in memory
//3 - Enter list with down arrow
//4 - Enter key and mouse click to select
//5 - ESC to close
//******************************************************************



function handleKeyUpOnSuggestField(evt){



// start the popup aligned to the component that launched it
suggestPopup = evt.getSource().findComponent("suggestPopup");
inputField = evt.getSource();



//don't open when user "tabs" into field
if (suggestPopup.isShowing() == false &&
evt.getKeyCode()!= AdfKeyStroke.TAB_KEY){


//We reference a DOM area by using getClientId()+"::content" to position the suggest list. Shame



//on use that we are doing this, but there is no other option we found. Keep in mind that using
//direct DOM references may break the code in the future if the rendering of the component changes
//for whatever reason. For now, we want to feel safe and continue using it

hints = {align:AdfRichPopup.ALIGN_AFTER_START, alignId:evt.getSource().getClientId()+"::content"};
suggestPopup.show(hints);


//disable popup hide to avoid popup to flicker on key press. Note that we override the default framework
//behavior of the popup component for this instance only. When hiding the popup, we re-implement the default
//functionality. By all means, never change the framework functionality on the prototype level as this could
//have an unpredictable impact on all nstances of this type
suggestPopup.hide = function(){}
}



//suppress server access for the following keys
//for better performance
if (evt.getKeyCode() == AdfKeyStroke.ARROWLEFT_KEY ||
evt.getKeyCode() == AdfKeyStroke.ARROWRIGHT_KEY ||
evt.getKeyCode() == AdfKeyStroke.ARROWDOWN_KEY ||
evt.getKeyCode() == AdfKeyStroke.SHIFT_MASK ||
evt.getKeyCode() == AdfKeyStroke.END_KEY ||
evt.getKeyCode() == AdfKeyStroke.ALT_KEY ||
evt.getKeyCode() == AdfKeyStroke.HOME_KEY) {
return false;
}


//close the popup when teh ESC key is pressed
if (evt.getKeyCode() == AdfKeyStroke.ESC_KEY){
hidePopup(evt);
return false;
}

// get the user typed values
valueStr = inputField.getSubmittedValue();


// query suggest list on the server. The custom event references the af:serverListener component
// which calls a server side managed bean method. The payload we send is the current string entered
// in the suggest field




AdfCustomEvent.queue(suggestPopup,"suggestServerListener",
// Send single parameter
{filterString:valueStr},true);




// put focus back to the input text field. We set the timout to 400 ms, which is not the final answer but a
// value that worked for us. In a next version, we may improve this
setTimeout("inputField.focus();",400);
}



//TAB and ARROW DOWN keys navigate to the suggest popup we need to handle this in the key down event as otherwise
//the TAB doesn't work. Note that a TAB key cannot be used to navigate in the suggest list. Use the arrow keys for



//this. The TAB key can be done to navigate the list, but this requires some more JavaScript that we didn't complete
//in time



function handleKeyDownOnSuggestField(evt){

if (evt.getKeyCode() == AdfKeyStroke.ARROWDOWN_KEY) {
selectList = evt.getSource().findComponent("joblist");
selectList.focus();
return false;
}
else{
return false;
}
}



//method called when pressing a key or a mouse button on the list. The ENTER key or a mosue click select
//the list value and write it back to the input text field (the suggest field)


function handleListSelection(evt){

if(evt.getKeyCode() == AdfKeyStroke.ENTER_KEY ||
evt.getType() == AdfUIInputEvent.CLICK_EVENT_TYPE){
var list = evt.getSource();
evt.cancel();
var listValue = list.getProperty("value");
hidePopup(evt);
inputField = evt.getSource().findComponent("suggestField");
inputField.setValue(listValue);
}
//cancel dialog
else if (evt.getKeyCode() == AdfKeyStroke.ESC_KEY){
hidePopup(evt);
}
}



//function that re-implements the node functionality for the popup to then call it
//Please do as if you've never seen this code ;-)


function hidePopup(evt){

var suggestPopup = evt.getSource().findComponent("suggestPopup");
//re-implement close functionality
suggestPopup.hide = AdfRichPopup.prototype.hide;
suggestPopup.hide();
}
</script>
]]>
</af:group>
</f:facet>

下一步规划

这是自动提示功能可通过 ADF Faces 富客户端实现的第一个证据。我们计划继续改进此原型,在示例的页面源代码中提供了少量路线图。最重要的改进是通过此示例创建声明式组件,该示例可参数化并且允许开发人员在单个页面上使用多个提示实例,而不会导致任何 Java 或 JavaScript 冲突。目前,我们欢迎有关新特性和错误报告的反馈,并且乐于收到改进了实现和修正了错误的代码示例。

下载示例

Oracle JDeveloper 11g 示例使用了 HR 数据库模式且可从这里下载。


阅读本文的英文版本

前往 Java 中文社区论坛,发表您对本文的看法。