用ASP技术开发 WEB 调查(投票)系统 (2)

网络整理 - 09-14
    
  二、调查项目的创建和维护
  作 者 : 仙人掌工作室
  
     本节我们说明调查项目创建和维护功能的实现。
  
     创建与维护调查项目的起始页面是Startup.html,该页面负责设定PollMaker.asp和ItemMaker.asp等页面使用的帧结构(从上到下共分三个帧)。起始页面所引用的Blank.html只用于指定背景颜色,StartMsg.html提供启动时显示在最下面帧的提示信息。
  
     PollMaker.asp和ItemMaker.asp都包含运行于服务器的ASP脚本,同时也包含(和创建)浏览器脚本以支持客户端操作。运行于服务器的脚本由VBscript写成,而客户端脚本则是Javascript,这使得它既可以运行于Netscape Navigator,也适合于IE。
  
     PollMaker.asp通过查找Poll数据库的MSysObjects系统表获得已经定义的调查项目名字。因此,ASP应用必须具有读取该表的权限。在Access97中的设定方法是:先选择菜单“工具/选项”设置系统对象可见,然后在“工具/安全/用户与组的权限”下设定。在本文所附代码中Poll.mdb已经设置了这个权限。如果要在不同的RDBMS上实现这个调查系统,这部分代码必须改写,使它适应目标数据库上的系统表结构。
  
     要将这些已定义的调查项目名字显示到下拉列表框,首先需要从S_表的表名中除去“S_”前缀,然后将这些字符串格式化为< SELECT>元素的< OPTION>字符串:
  
   
   < SELECT>
   < OPTION VALUE="StartPoll" SELECTED>NEW POLL
   < %
    ' 获取当前已经定义的调查项目名字
    Set objConn = Server.CreateObject("ADODB.Connection")
    objConn.Open "poll"
    Set objRS = _
    objConn.Execute("SELECT Name FROM MSysObjects " & _
    "WHERE Type=1 AND Name LIKE 'S_%' ORDER BY Name")
    Do While Not objRS.EOF
    PollName = Mid(objRS("Name"), 3)
    Response.Write("< OPTION VALUE=""" & PollName & """>" & PollName)
    objRS.MoveNext
    Loop
   %>
   < /SELECT>
  
     ItemMaker将指定调查项目的所有定义信息下载到浏览器端的Javascript数组中,从而实现问题的快速编辑和创建。所有的问题编辑操作都在浏览器内通过修改两个Javascript数组完成,只有单击“保存” 按钮才可将修改结果写入数据库。
  
     ItemMaker.asp首先将Response.Buffer设置为True,因此在页面生成完成之前HTML输出将一直缓冲在服务器上,这使得产生错误或执行非编辑功能时ItemMaker.asp可以不下载那些Javascript函数而退出。在Sub Main的开始处,程序通过检查表单变量OpType来决定是创建新的调查项目,还是删除调查项目或调查结果。这三个操作均在ItemMaker.asp内完成,即所有受密码保护的功能均在同一脚本内实现,这使得整个系统中密码只在一个地方存取。上述三个操作均需动态地生成与执行SQL语句。不论是脚本执行出现错误,还是指定的操作执行成功,都调用Response.Redirect语句重新加载PollMaker.asp,并把一个说明字符串传递给它:
  
   
   < %
   Dim PollName
   Dim Password
   Dim objConn
   Dim objRS
   
   PollName = Request("PollName")
   Password = UCase(Request("Password"))
   OpType = Request("OpType") ' 要求ItemMaker.asp执行的操作
   
   SName = "[S_" & PollName & "]"
   AName = "[A_" & PollName & "]"
   RName = "[R_" & PollName & "]"
   Call Main
   Sub Main
   If Password < > "WEBPOLL" Then ' 如密码错误返回PollMaker.asp并提示
    Response.Redirect "PollMaker.asp?Msg=密码错误,请再试一次。"
    Exit Sub '从ItemMaker.asp返回
   End If
   If OpType = "Edit" Then ' 编辑或创建调查调查项目
    If PollName = "StartPoll" Then
    If Not newPoll() Then '用户没有给出调查项目的名字
    Response.Redirect "PollMaker.asp?Msg=请输入调查项目名字"
    Exit Sub
    End If
    SName = "[S_" & PollName & "]"
    AName = "[A_" & PollName & "]"
    RName = "[R_" & PollName & "]"
    End If
   ElseIf OpType = "DelResp" Then ' 删除调查项目的已有结果
    If PollName = "StartPoll" Then
    Response.Redirect "PollMaker.asp?Msg=请输入调查项目名字"
    Exit Sub
    End If
    Set objConn = Server.CreateObject("ADODB.Connection")
    objConn.Open "poll"
    Set objRS = Server.CreateObject("ADODB.Recordset")
    objConn.Execute "DELETE FROM " & RName
    Response.Redirect "PollMaker.asp?Msg=已经删除指定调查项目的投票结果"
   ElseIf OpType = "DelPoll" Then ' 删除整个调查项目
    If PollName = "StartPoll" Then
    Response.Redirect "PollMaker.asp?Msg=不能删除新调查项目"
    Exit Sub
    End If
    Set objConn = Server.CreateObject("ADODB.Connection")
    objConn.Open "poll"
    Set objRS = Server.CreateObject("ADODB.Recordset")
    objConn.Execute "DROP TABLE " & SName
    objConn.Execute "DROP TABLE " & AName
    objConn.Execute "DROP TABLE " & RName
    Response.Redirect "PollMaker.asp?Msg=已经删除指定的调查项目。"
   End If
   %>
  
  
     在运行ItemMaker.asp编辑调查项目时,程序提取Poll数据库中定义的调查项目及其问题定义数据初始化数组stemArray[]和ansArray[]。这些用来初始化的字符串在写入时经过“转义”处理(即调用Escape()函数),从而避免了由于嵌入的引号或其它控制字符可能导致的问题,在使用这些数组之前这些字符串被还原(initPoll()函数):
  
  
   
   < script LANGUAGE="Javascript">
   var stemIx = 0;
   var INumber;
   var NoOpinion;
   
   // 初始化数组stemArray[]与ansArray[]
   var stemArray = new Array(
   < %
   Set objConn = Server.CreateObject("ADODB.Connection")
   objConn.Open "poll"
   Set objRS = Server.CreateObject("ADODB.Recordset")
   ' 从数据库读取数组内容
   objRS.Open "Select * FROM " & SName & " ORDER BY ID", objConn
   Do While Not objRS.EOF
    ' 转换问题与回答字符串以允许使用所有字符
    stemStr = Escape(objRS("Stem"))
    Response.Write("""" & objRS("ID") & objRS("IType") & _
    Left(objRS("NoOpinion"),1) & stemStr & """")
    objRS.MoveNext
    If Not objRS.EOF Then
    Response.Write(",")
    End If
   Loop
   objRS.Close
   %>
   );
   var ansArray = new Array(
   < %
   ' 从数据库读取数组内容
   objRS.Open "Select * FROM " & AName & " ORDER BY ID, ALabel", objConn
   Do While Not objRS.EOF
    ansStr = Escape(objRS("Answer"))
    Response.Write("""" & objRS("ID") & objRS("ALabel") & ansStr & """")
    objRS.MoveNext
    If Not objRS.EOF Then
    Response.Write(",")
    End If
   Loop
   objRS.Close
   objConn.Close
   %>
   );
   
   // 初始化
   function initPoll() {
    writeMsgFrame('< HTML>< BODY BGCOLOR="#c0c0c0">' +
    '< CENTER>已经读取或创建调查项目"< %=PollName%>"' +
    '< /CENTER>< /BODY>< /HTML>');
    if (stemArray.length > 0) INumber = stemArray[0].substr(0,3);
    for (i=0; i< stemArray.length; i++) stemArray[i] = unescape(stemArray[i]);
    for (i=0; i< ansArray.length; i++) ansArray[i] = unescape(ansArray[i]);
    doNav("< "); // 显示第1个问题
   } // initPoll()
  
  
     如前所述,图2所显示的页面由三个帧构成。最上面的帧(ControlFrame)包含由ItemMaker.asp输出的文档,其中大部分代码是用来支持浏览器内的编辑操作的Javascript。这些代码通过动态生成HTML表单在位于中间的帧(ItemFrame)显示问题。在编辑问题的时候,编辑事件将触发保存更新数据到stemArray[]和ansArray[]数组的Javascript函数。最下面的一个帧(MsgFrame)用来显示提示信息。
  
     Javascript函数doNew()在调查项目的最后加入新的问题,它先在stemArray[]的最后加入一行,然后在ansArray[]数组中加入对应于问题类型的合适行数,最后调用doNav()函数显示这个问题:
  
  
   // 创建新问题并将它加入到调查项目的最后
   function doNew() {
    stemLen = stemArray.length;
    INumber = (stemLen > 0) ?
    parseInt(stemArray[stemLen-1].substr(0,3)) : 0;
    INumber = " " + (++INumber).toString();
    INumber = INumber.substr(INumber.length-3, 3);
    for (IType = 0; IType < 5; IType++) {
    if(document.ItemType.IType[IType].checked) break;
    }
    IType++;
    NoOpinion =
    (document.ItemType.NoOpinion.checked) ? "Y" : "N";
    stemArray[stemLen] = INumber + IType + NoOpinion;
    if (IType > 3) ACount = parseInt(document.ItemType.nChoices.value);
    else if (IType == 3) ACount = 2; // 语义区别
    else ACount = 0;
    if (ACount > 26) Acount = 26;
    if (ACount > 0) {
    j = ansArray.length;
    for (i=0; i< ACount; i++)
    ansArray[j+i] = INumber + String.fromCharCode(65+i);
    }
    stemIx = stemLen;
    doNav(""); // 显示空白的问题
   } // doNew()
   
  
     doMove()函数修改问题在调查项目中的位置(序号),它先将指定问题与答案在各自的数组中重新编号,然后将这两个数组排序:
  
  
   // 将问题移到指定位置之后
   function doMove() {
    if (stemArray.length < = 0) return; // 没有已定义的问题
    ID = stemArray[stemIx].substr(0,3); // 要移动问题的ID
    QTemp = document.ItemType.IAfter.value; // 移到哪里
    if (QTemp >= 0 && QTemp < stemArray.length)
    NID = stemArray[QTemp].substr(0,3); // 被移动问题新的ID
    else if (QTemp == stemArray.length)
    NID = stemArray[stemArray.length-1].substr(0,3);
    else return;
    minID = (ID< NID) ? ID : NID;
    maxID = (ID< NID) ? NID : ID;
    offset = (ID< NID) ? -1 : 1; // 移动方向
    for (i=0; i< stemArray.length; i++) {
    thisID = stemArray[i].substr(0,3);
    if (thisID>=minID && thisID< =maxID) {
    if (thisID == ID)
    stemArray[i] = NID + stemArray[i].substr(3);
    else {
    tempID = " " +
    (parseInt(thisID) + offset).toString();
    tempID = tempID.substr(tempID.length-3, 3);
    stemArray[i] = tempID + stemArray[i].substr(3);
    }
    }
    }
    stemArray.sort();
    for (i=0; i< ansArray.length; i++) {
    thisID = ansArray[i].substr(0,3);
    if (thisID>=minID && thisID< =maxID) {
    if (thisID == ID)
    ansArray[i] = NID + ansArray[i].substr(3);
    else {
    tempID = " " +
    (parseInt(thisID) + offset).toString();
    tempID = tempID.substr(tempID.length-3, 3);
    ansArray[i] = tempID + ansArray[i].substr(3);
    }
    }
    }
    ansArray.sort();
    document.ItemType.INumber.value = NID;
    doNav("Go->"); // 显示移动后的问题
   } // doMove()
   
  
     doDelete()函数通过重新编号并压缩这两个数组实现问题的删除(将最后几行去掉):
  
  
   // 确认删除,然后在stemArray[]和ansArray[]中执行删除操作
   function doDelete() {
    if (confirm("是否删除当前显示的问题?")) {
    if (stemArray.length > 0) {
    ID = stemArray[stemIx].substr(0,3); // 被删除问题的ID
    for (i=stemIx; i< stemArray.length-1; i++)
    stemArray[i] = stemArray[i].substr(0,3) +
    stemArray[i+1].substr(3); // 调整删除后的数组内容
    stemArray.length--;
    }
    if (ansArray.length > 0) {
    delCount = 0;
    for (i=0; i< ansArray.length; i++) {
    if (ansArray[i].substr(0,3) == ID) {
    ansArray[i] = "ZZZ"; // 删除该项(排到最后面)
    delCount++;
    } else if (ansArray[i].substr(0,3) > ID) {
    tempID = " " +
    (parseInt(ansArray[i].substr(0,3)) - 1).toString();
    tempID = tempID.substr(tempID.length-3, 3);
    ansArray[i] = tempID + ansArray[i].substr(3);
    }
    }
    ansArray.sort();
    ansArray.length-=delCount;
    }
    doNav("< "); // 尝试退回前一问题
    }
   } // doDelete()
   
  
     doNav()函数支持问题的导航,实现方法是改变需要显示问题的索引号,然后调用writeItem完成输出,代码略。
  
     writeItem是这里最为复杂的函数。它按照问题类型的不同,将HTML代码以字符串的形式输出,这个字符串即为显示与编辑该问题的表单。表单为问题的每个可编辑部件提供了onChange()函数定义。当这个表单被写入页面中间的帧(ItemFrame),用户的编辑操作就会引起对saveStem()或saveAnswer()函数的调用,这些函数负责修改当前调查项目的stemArray[]或ansArray[]数组。
  
  
   // 在浏览器中显示指定的问题
   function writeItem(stemIx) {
    var IType = parseInt(stemArray[stemIx].substr(3,1)); // 问题类型
    var NoOpinion = stemArray[stemIx].substr(4,1); // 是否允许不回答
    var ID = stemArray[stemIx].substr(0,3); // 问题的ID
   
    for (ansIx=0; ansIx< ansArray.length; ansIx++) {
    if (ansArray[ansIx].substr(0,3) == ID) break; // 问题的回答
    }
    if (ansIx >= ansArray.length) ansIx = -1; // 当前问题没有回答结果
    outStr = '< HTML>< BODY BGCOLOR="#c0c0c0">' +
    '< FORM>' +
    '【当前问题编号】' + (stemIx + 1) + '【总问题数】 ' + stemArray.length +
    ' 【调查项目名字】 < %=PollName%>< /B>< BR>< /table>< TEXTAREA ROWS=2 COLS=85 '+
    'onBlur="parent.ControlFrame.saveStem()">' +
    stemArray[stemIx].substr(5) + '< /TEXTAREA>< BR>';
   
    if (NoOpinion == "Y" && IType != 5) outStr+=
    ' < INPUT TYPE=radio>暂不回答 ';
    switch (IType) {
    case 1: // 是/否
    outStr+= '< INPUT TYPE=radio>是 ' +
    '< INPUT TYPE=radio>否 ';
    break;
    case 2: // 赞同程度
    ...略...
    break;
    case 3: // 语义区别
    ...略...
    break;
    case 4: // 多个选择项
    ...略...
    break;
    case 5: // 允许复选
    ...略...
    break;
    default:
    outStr+= '错误:' + IType + " "
    }
    outStr+= '< /FORM>';
    writeItemFrame(outStr);
   } // writeItem()
   
  
     最后,当用户单击“保存”按钮时执行的是doSave()函数。该函数先创建一个表单(表单中包含的隐藏变量描述了stemArray[]和ansArray[]数组的内容),然后把这个表单提交给PollMaker.asp,在这里完成数据库的更新:
  
  
   // 将问题保存到数据库
   function doSave() {
    if (stemArray.length < = 0) return;
    outStr = '< HTML>< BODY BGCOLOR="#c0c0c0">< CENTER>正在保存 ' +
    '"< %=PollName%>"< /CENTER>' +
    '< FORM METHOD=post ACTION="PollMaker.asp">' +
    '< INPUT TYPE=hidden VALUE="SavePoll">' +
    '< INPUT TYPE=hidden VALUE="< %=PollName%>">'
    for (i=0; i< stemArray.length; i++)
    outStr+='< INPUT TYPE=hidden VALUE="' +
    escape(stemArray[i]) + '">';
    for (i=0; i< ansArray.length; i++)
    outStr+='< INPUT TYPE=hidden VALUE="' +
    escape(ansArray[i]) + '">';
    outStr+= '< /FORM>';
    with (parent.MsgFrame.document) {
    open("text/html");
    writeln(outStr);
    close();
    }
    parent.MsgFrame.document.Save.submit();
   } // doSave()