非常悲剧,刚才写了一点后,点删除键,结果后退了...悲剧啊...只能重头写了...
作为实战
的大结局,一上来我们就做一下弊,看看这节内容最后做出的这个简单的客户
信息记录系统最终
是什么样子的?
和xGrid相比,我们的客户信息记录系统,
在左下角
多了3个icon,它们分别代表添加,
编辑和删除,我们用一连贯的图片向大家展示一下,这些按钮
如何工作的:
OK,就像我说的一样,这个系统真的很简单的,但是看起来也还不错,这是因为它基于一个不错的CC,如果要说xGrid是否很简单的话,我觉得这个问题就
不是那么肯定了,而如果再追问,xGrid的支持框架jqGrid是否简单的话,我觉得可能答案就是,不是那么简单了,所以我们这个简单的应用很简单,但
它的后面的支持却很强大,另外它是一个实实在在地在Victor的
公司部
署实施的一个简单应用,尽管使用这个应用的只是一个单独的部门(这个部门的客户信息和SAP里的不一样,而他们又不想让其他部门看到他们的客户信息,因此
提出了要做一个共享的access,Victor了解到之后,通过简单地解释,对方就欣然接受使用一个可以浏览器直接访问,并且能够提供多种查询功能的小
系统,这就是我们这个Demo的由来),尽管这个应用看起来实在是太简单了,甚至有点简陋,但其实业务需求本身就是很简单,我们不得不承认,有时候业务部
门想要的就是一个这么简单,但却好用的小系统。
至于这个小系统有多简单,呵呵呵,看看下面的这些设计元素的截图吧:
去掉图片和JS,整个NSF的
Lotus设计元素是7个,1个forms,1个views,1个我们的主角ccxGrid,4个
XPages,4个
XPages中其实xDelete和xEdit还可以合成1个,而它们俩和xJsonCustom共3个XPages其实
都是XAgent,因此实际上只有1张XPages,而有3个XAgent(其实,可以简化为2个,而至于什么是XAgent请温习上一节的内容)
forms和views都是简单至极的设计,没有任何花哨的东西,非常常规的设计方式,我们直接跳过,反正最后会给大家一个Demo的NSF,而
ccxGrid和之前我们说的xGrid.nsf里的ccxGrid稍有不同,Victor基于jqGrid做了一些扩展,我们现在放出代码,看看到底多
出来的3个操作按钮是哪里来的?
$().ready(
function(){
var lastsel;
jQuery("#listxGrid").jqGrid({
url:'#{javascript:compositeData.url}',
datatype: "json",
colNames:#{javascript:compositeData.colNames},
colModel:#{javascript:compositeData.colModel},
jsonReader: {
repeatitems: false,
id: '#{javascript:compositeData.colID}',
root: function (obj) {
if ($.isArray(obj)) return obj;
if ($.isArray(obj.items)) return obj.items;
return [];
},
page: function () { return 1; },
total: function () { return 1; },
records: function (obj) {
if ($.isArray(obj)) return obj.length;
if ($.isArray(obj.items)) return obj.items.length;
return 0;
}
},
gridview: true,
loadonce: #{javascript:compositeData.loadOnce},
ignoreCase: #{javascript:compositeData.ignoreCase},
rowNum: #{javascript:compositeData.rowNum},
rowList: #{javascript:compositeData.rowList},
rownumbers: #{javascript:compositeData.showRowNumbers},
height: #{javascript:compositeData.height},
caption: '#{javascript:compositeData.caption}',
pager: '#pagerxGrid',
viewrecords: true,
emptyrecords: '#{javascript:compositeData.emptyRecords}',
sortable:#{javascript:compositeData.allowReorder},
sortname: '@position'
});
if (#{javascript:compositeData.showFilterToolbar} == true){
jQuery("#listxGrid").filterToolbar({stringResult: false, defaultSearch:
'cn', searchOnEnter: false});
$("#listxGrid")[0].toggleToolbar();
}
jQuery("#listxGrid").jqGrid('navGrid','#pagerxGrid',{edit:true,
add:true, del:true,refresh:true,refreshstate:'current',
beforeRefresh: function() {
$("#listxGrid").jqGrid('setGridParam',{datatype:'json'}).trigger('reloadGrid');
}},
{url: '#{javascript:compositeData.editUrl}'}, // default settings for edit
{url: '#{javascript:compositeData.addUrl}'}, // default settings for add
{url: '#{javascript:compositeData.deleteUrl}'}, // delete
{closeOnEscape: true,
multipleSearch: true, closeAfterSearch: true, showQuery: false,
refreshstate:'current'} // search options
);
jQuery.extend(jQuery.jgrid.edit, {
// some other settings
closeAfterAdd: true,
closeAfterEdit: true,
recreateForm: true,
position: "last",
reloadAfterSubmit: true,
afterSubmit: function(response) {
if (response.responseText == "") {
$("#listxGrid").jqGrid('setGridParam',{datatype:'json'}).trigger('reloadGrid');
return [true, response.responseText, false]
}else {
return [false, "提交失败,请您重新登录系统!"]
}
}
});
if (#{javascript:compositeData.showFilterToolbar} == true){
jQuery("#listxGrid").filterToolbar({stringResult: false, defaultSearch:
'cn', searchOnEnter: false});
jQuery("#listxGrid").jqGrid('navButtonAdd', "#pagerxGrid",{
caption: "",
title: "快速查询",
buttonicon: 'ui-icon-pin-s',
onClickButton: function () {
$("#listxGrid")[0].toggleToolbar()
}
});
}
jQuery("#listxGrid").jqGrid('navButtonAdd','#pagerxGrid',{
caption: "",
title: "筛选列",
onClickButton : function (){
jQuery("#listxGrid").jqGrid('columnChooser');
}
});
function editLinkFmatter(cellvalue, options, rowObject) {
return "<a
target='#{javascript:compositeData.target}'
href='./#{javascript:compositeData.xpName}?documentId=" +
rowObject["@unid"] +
"&action=#{javascript:compositeData.action}' class='doclink'>" +
cellvalue + "";
}
而Victor做的扩展部分主要是这些,其他的和xGrid.nsf里的没有太大区别,正如大家看到的,Victor在第一行使用edit:true,
add:true, del:true开启了添加、编辑和删除按钮,这个是jqGrid的标准API,更多的API内容请参考:
http://www.trirand.com/blog/jqgrid/jqgrid.html,另外请注意,Victor下载了最新的jqGrid.js的代码,并替换了自己应用中的该js文件,和xGrid相比,更新后的jqGrid有更强大的功能,比如多级分类,相关的内容同样和可以查询jqGrid的API。
在第二行中,Victor在beforeRefresh事件中中定义了一个简单那的函数,就是reload一下当前
数据,这个作用在于当你开启数据的加载方式loadonce的值为true时,仍保证刷新按钮时好用的,且可以从
数据库中获取最新的数据,为什么把loadonce设置为ture?理由有很多,比如性能啊,特别是查询性能啊,但最关键的是,xGrid或者说是jqGrid提供了前台的查询搜索功能,这个功能已经非常强大了(远远比Notes的搜索强大),基本上可以不用自己再
开发搜索的代码,而且由于是一次加载完之后不用再和
服务器交
互,所以查询的速度非常快(远远比SQL快),因此这个功能在我们这个简单的系统中,实在没有放弃的理由,但不放弃查询的功能,就必须设置
loadonce为true(如果哪位能设置为false,还保留前台查询功能的话,请一定联系我,谢谢),而这样一来首当其冲的问题就是,除非你在浏览
器里重新打开或者刷新.xsp页面,否则任何按钮都不会讲后台的数据重新取到前台来,包括刷新按钮,表现是按下去后根本没有反应,而我们在
beforeRefresh中的代码就是为了解决这个问题,让数据强行reload。
接下来的三行代码非常重要,它们分别是一个url,这3个url对应的分别是编辑、添加、删除会执行什么操作,在前一节中,Victor已经说明了如何结
合XAgent这个设计理念来完成你想要执行的操作,不明白如何让url执行XAgent操作的同学可以温习一下上一节,或者你有一些开发经验的话,也应
该了解agent其实也是可以通过url呼叫的的,道理差不多,只不过XAgent强大和方便了更多。
jQuery("#listxGrid").jqGrid('navGrid','#pagerxGrid',{edit:true, add:true, del:true,refresh:true,refreshstate:'current',
beforeRefresh: function() {
$("#listxGrid").jqGrid('setGridParam',{datatype:'json'}).trigger('reloadGrid');
}},
{url: '#{javascript:compositeData.editUrl}'}, // default settings for edit
{url: '#{javascript:compositeData.addUrl}'}, // default settings for add
{url: '#{javascript:compositeData.deleteUrl}'}, // delete
{closeOnEscape: true,
multipleSearch: true, closeAfterSearch: true, showQuery: false,
refreshstate:'current'} // search options
);
jQuery.extend(jQuery.jgrid.edit, {
// some other settings
closeAfterAdd: true,
closeAfterEdit: true,
recreateForm: true,
position: "last",
reloadAfterSubmit: true,
afterSubmit: function(response) {
if (response.responseText == "") {
$("#listxGrid").jqGrid('setGridParam',{datatype:'json'}).trigger('reloadGrid');
return [true, response.responseText, false]
}else {
return [false, "提交失败,请您重新登录系统!"]
}
}
});
if (#{javascript:compositeData.showFilterToolbar} == true){
jQuery("#listxGrid").filterToolbar({stringResult: false, defaultSearch:
'cn', searchOnEnter: false});
jQuery("#listxGrid").jqGrid('navButtonAdd', "#pagerxGrid",{
caption: "",
title: "快速查询",
buttonicon: 'ui-icon-pin-s',
onClickButton: function () {
$("#listxGrid")[0].toggleToolbar()
}
});
}
jQuery("#listxGrid").jqGrid('navButtonAdd','#pagerxGrid',{
caption: "",
title: "筛选列",
onClickButton : function (){
jQuery("#listxGrid").jqGrid('columnChooser');
}
});
在这段代码里还有一个重点是这部分
jQuery.extend(jQuery.jgrid.edit, {
// some other settings
closeAfterAdd: true,
closeAfterEdit: true,
recreateForm: true,
position: "last",
reloadAfterSubmit: true,
afterSubmit: function(response) {
if (response.responseText == "") {
$("#listxGrid").jqGrid('setGridParam',{datatype:'json'}).trigger('reloadGrid');
return [true, response.responseText, false]
}else {
return [false, "提交失败,请您重新登录系统!"]
}
}
});这是jqGrid的一种扩展方式,稍微设计到一些jQuery的语法问题,不过不是重点,还是那句话,在你没有功夫研究jQuery或者
jqGrid之前,你知道可以这么用就可以了,你可以看到Victor对多个方法进行了扩展的设置,但重点是最后那个afterSubmit。
在做完这个应用之后,Victor遇到了一个麻烦的问题,就如我们前面说的,前台查询功能导致我必须开启loadonce为true,这不仅导致原生的刷新按钮不好使了(稍靠前的位置里Victor介绍了如何解决这个问题),最麻烦还是,当
用户提
交了一个操作后(添加,编辑,删除中的任何一种),没法即可刷新当前的数据,这个问题最严重的一种后果是,一个人录了100条记录,然后其实他已经登录超
时了,但因为没有重新获取后台数据,所以除非他用浏览器刷新一下url,否则他自己还是接着录入,那第二天他肯定是拿着板砖来砸Victor的,因为他前
一天的工作等于全白做了,这种事情绝对是不能容忍的,所以在用户提交后,即刻reload数据的这个功能必须有!同理可以推出类似于一堆数据已经删除,但
有人因为没有刷新而继续在上面修改等等...
因此在afterSubmit之后,我们获取一下返回的response里是否有内容,一般有内容的话通常就是我们熟悉的404,405等等,不写额外的
代码的话,那返回的就是什么都没有,接下来Victor会立马告诉大家,如何通过XAgent操作request和response的,不用急,现在我们
还是关注afterSubmti这块,我们可以看到里面是一个分支,如果response为空,则reload当前页面,并且return [true,
response.responseText, false] ,否则就return [false, "提交失败,请您重新登录系统!"]
,在afterSubmit里return什么值是jqGrid的API的事,你可以查询一下API,会比我说的详细,在这里的话,说得白点就是一个是成
功提交并reload数据,另一个是返回错误提示"提交失败,请您重新登录系统!"。实际效果的话,大家下载了Demo库之后,可以试一下,比较方便的方
式是吧登录超时时间设短点,然后等用户超时后,添加或修改一条数据试试就能看到报错效果的提示了。
Victor不准备讲更多的如何在XPages中设置ccxGrid的属性,从而满足自己的业务的问题,也不再讲一遍如何从视图里获取值并生成一个
JSON数据返回,这些是上节和上上节的内容,请大家已经在看完之前的内容后再来看本节的内容,这样才是最节省时间的办法。(由于Victor的这个业务
不可能出现大量数据条目,所以Victor发布JSON数据的时候,并没有采取分页的方式,有兴趣或有需求的同学可以试试,并发布到论坛上
来,Victor会给与金币奖励的,算是留给大家的一个小实验吧,如果确实没有人做的话,Victor过一段时间也会再来完善这块内容)
我们接下来以xEditCustomer.xsp为例,来讲讲如何在XAgent中操作response和request,以及如何结合domino的方
法来实现添加或者编辑的效果,我们先看看xEditCustomer.xsp的核心代码,这些代码是在这个XPages的
beforeRenderResponse事件中的,为什么加在这里同样不重要,知道XAgent的代码可以加在这里就可以了...(Victor发现自
己老是说这种句式,希望大家没有烦,但事实情况就是这样,在初始阶段如果你花大力气去弄明白每一件事情的话,可能会严重
影响进度,而且没有一定的经验积累,一些内容你并不可能真正的理解)
try {
var exCon:javax.faces.context.ExternalContext = facesContext.getExternalContext();
var response:com.ibm.xsp.webapp.XspHttpServletResponse = exCon.getResponse();
var request:com.sun.faces.context.MyHttpServletRequestWrapper = exCon.getRequest();
var map:java.util.Map = request.getParameterMap();
//print(map);这句在Debug的时候可以不注释掉
var it:java.util.Iterator = map.entrySet().iterator();
if(map.get("@unid").toString().length>0){
var doc:NotesDocument = database.getDocumentByUNID(map.get("@unid").toString());
}else{
var doc:NotesDocument = database.createDocument();
doc.replaceItemValue("form","fmCustom");
}
var $CustomName = map.get("$CustomName").toString();
var $CustomType = map.get("$CustomType").toString();
var $CustomArea = map.get("$CustomArea").toString();
var $EndUser = map.get("$EndUser").toString();
var $ContactName = map.get("$ContactName").toString();
var $ContactPhone = map.get("$ContactPhone").toString();
var $ContactFax = map.get("$ContactFax").toString();
var $Remark = map.get("$Remark").toString();
doc.replaceItemValue("M_CustomName",$CustomName);
doc.replaceItemValue("M_CustomType",$CustomType);
doc.replaceItemValue("M_CustomArea",$CustomArea);
doc.replaceItemValue("M_EndUser",$EndUser);
doc.replaceItemValue("M_ContactName",$ContactName);
doc.replaceItemValue("M_ContactPhone",$ContactPhone);
doc.replaceItemValue("M_ContactFax",$ContactFax);
doc.replaceItemValue("M_Remark",$Remark);
doc.computeWithForm(false,false);
doc.save(true,false);
facesContext.responseComplete()
} catch (e) {
print ("error: " + e.toString() );
}
先说说前4句:
var exCon:javax.faces.context.ExternalContext = facesContext.getExternalContext();
var response:com.ibm.xsp.webapp.XspHttpServletResponse = exCon.getResponse();
var request:com.sun.faces.context.MyHttpServletRequestWrapper = exCon.getRequest();
var map:java.util.Map = request.getParameterMap();
//print(map);这句在Debug的时候可以不注释掉
第3节的内容中我们说了如何取得exCon和response,并保证在第4节中会告诉大家如何使用request,好了,现在Victor兑现了,我们
可以request是想http服务器发出的申请,而response是http服务器进行的反馈,而
request.getParameterMap()里都有什么?你不如print一下试试?保证会让你满意,关于这四句的底层理论,你可以通过Java
Servlet或JSF学习到,有兴趣的同学可以了解一下,但就一般的XPages开发中的XAgent来说,有这4句作为开头,基本大多数的任务你都能
完成了,总之print一下map,然后到domino的
控制台看一下print出来的内容,这样你自己都能写出接下来的代码,并且可能写得更好:
var it:java.util.Iterator = map.entrySet().iterator();
if(map.get("@unid").toString().length>0){
var doc:NotesDocument = database.getDocumentByUNID(map.get("@unid").toString());
}else{
var doc:NotesDocument = database.createDocument();
doc.replaceItemValue("form","fmCustom");
}
var $CustomName = map.get("$CustomName").toString();
var $CustomType = map.get("$CustomType").toString();
var $CustomArea = map.get("$CustomArea").toString();
var $EndUser = map.get("$EndUser").toString();
var $ContactName = map.get("$ContactName").toString();
var $ContactPhone = map.get("$ContactPhone").toString();
var $ContactFax = map.get("$ContactFax").toString();
var $Remark = map.get("$Remark").toString();
doc.replaceItemValue("M_CustomName",$CustomName);
doc.replaceItemValue("M_CustomType",$CustomType);
doc.replaceItemValue("M_CustomArea",$CustomArea);
doc.replaceItemValue("M_EndUser",$EndUser);
doc.replaceItemValue("M_ContactName",$ContactName);
doc.replaceItemValue("M_ContactPhone",$ContactPhone);
doc.replaceItemValue("M_ContactFax",$ContactFax);
doc.replaceItemValue("M_Remark",$Remark);
doc.computeWithForm(false,false);
doc.save(true,false);
facesContext.responseComplete()
这段代码里我们对map进行了遍历,并根据遍历的结果判断了是一个添加操作还是一个编辑操作(判断方法并不是最好的,在print了一下map后
Victor发现有更好的方式,但是当时系统已经上线了,所以就没改了,但是不知道你发现了这个更好的方式了吗?提示1,通过该方法可以把delete操
作也合并进来,提示2,注意map里的一个值...),之后我们获取request传递过来的所有值,并对应到我们客户信息
表单上去,按域录入,然后compute一下,然后保存,最后告诉facesContext,response完成了,OK,代码就这么多,一点都不难对不对?删除操作更简单,这个地方Victor不讲了。
最后,在Victor准备上传实战demo的NSF时发现一个很严肃的问题,由于当前这个NSF里面的签名会泄露公司的信息,因此Victor必须明天到
公司用另外一台机器重新签名后,才能给大家传上来,大家安心,Victor说了会共享那肯定就会共享,明天一上班就给大家传上来。
好了,到此为止,随着这个简单的客户信息录入系统的开发和介绍完毕,我们的实战内容《xGrid,来自jQuery的神器》就到此为止了,Victor的
文字功底不是很好,所以可能无法将自己所想讲的清晰完整地全部说给大家听,但通过这个系列4节的内容,Victor相信一方面能够为大家介绍到一些
XPages以及相关
Web技术的
概念,特别是如何在XPages中结合使用这些Web技术,另一方面,既然是实战,最后也通过一个共享的demo给大家抛一块砖,用一个实际业务问题的解
决来说明xGrid这个控件的确很有用处,如果是以前的传统开发方式,不使用jqGrid或者说是xGrid的话,完成同样的功能将是一个非常耗时的问
题,搜索的功能无法这样强大,性能无法这样出色,界面无法做到如此顺眼,无法兼容所有的浏览器,最关键的是,无法让这个这么费力开发出来的应用,被快速地
使用到另外一个业务上,而这些正是一个简单的demo和一个可以发布的应用之间的无法跨越的那种区别!这种区别不是靠1-2个人的付出就可以解决的,因此
Victor提出了这种解决思路:使用现有的Web技术,特别是现有插件来改为自己的CC,从而解决这一系列问题,从而使得开发人员更关注于业务的实现,
而不是一些功能的实现。性能的提升和兼容性的解决,这些Lotuser们可以做一部分,但不能都是自己做,这点在Mobile支持方面显得特别突出!
最后,不得不说jqGrid以及由此产生的xGrid都是非常值得我们敬佩的设计,像两位作者致敬,并且xGrid的适用范围远远比Victor已经提到
的还要广阔,所有有兴趣的同学都可以将自己的对xGrid的扩展共享出来,Victor虽然论坛币不多,但绝对以积分形式给予鼓励!
最后的最后,谢谢每一个关注本系列的同学们,你们的支持是Victor最大的动力,让我们开始下一阶段更精彩的内容吧!
添加新评论3 条评论
2012-09-18 15:29
2012-09-12 21:55
至于一开始控件的搭建是怎么样的这个问题,我没有太明白你说的“一开始控件的搭建”指的是什么?
2012-09-12 16:26