クラシックASPのコーディング考慮点 その2

If文の書き方

On Error Resume Nextの動作について。

If文の評価式で実行時エラーが発生した場合の動作を確認してみた。

On Error Resume Next
If Foo() = True Then
    Response.Write "<p>returned True.</p>"
Else
    Response.Write "<p>returned False.</p>"
End If

Function Foo()
    Err.Raise 5
    'Foo = False
End Function

Foo関数で実行時エラーが発生しているので、Falseの評価をしてほしいのだが、実際は「returned True.」と表示される。

この点を考慮すると、vbscriptでIf文を使う場合は、以下のようにするとよいだろう。

If 「エラーかどうかの判定」 Then
    'エラー時の処理
Else
    '正常時の処理
End If

具体的には、

If Not Foo() Then
    Response.Write "<p>returned False.</p>"
Else
    Response.Write "<p>returned True.</p>"
End If

となる。

同様にDo While文でも評価式で実行時エラーが発生するとループの中の処理が実行されるので注意する。

ちなみに、Select Case文だと、End Select行の次行から処理される。

入力値の表示と復元

PostメソッドでFormデータをsubmitすることが前提。

Request.Formから入力値をそれぞれ取得するのは効率が悪い。 表示時に使用したgDataに復元できる関数を用意する。

' Request.FormからCollectionにデータを復元する。
' arListId : 一覧識別子の配列
Function CreateFormCollection(arListId)
   Dim i
   Dim o
   Dim listCnt
   Dim colNmInfo
   Dim isList,lstId,idx, childNm
   Dim nm

   Set o = CreateCollection()
   
   listCnt = UBound(arListId) + 1
   
   For i = 0 To listCnt - 1
      o.Add arListId(i), CreateCollection()
   Next
   
   isList = False
   lstId = ""
   idx = 0
   childNm = ""
   
   For Each nm In Request.Form
      isList = False
      For i = 0 To listCnt - 1
         '一覧識別子 + "_" + index + "_" + name の分解
         Set colNmInfo = GetIndexAndNameFromNameInList(nm, arListId(i))
         If colNmInfo.Count > 0 Then
            isList = True
            lstId = arListId(i)
            idx = colNmInfo("idx")
            childNm = colNmInfo("nm")
         End If
      Next
      
      If Not isList Then
         o.Add nm, Request.Form(nm)
      Else
         If Not o(lstId).Exists(idx) Then
            o(lstId).Add idx, CreateCollection()
         End If
         
         o.Item(lstId)(idx).Add childNm, Request.Form(nm)
      End If
   Next
   
   Set CreateFormCollection = o
End Function

Const LISTID_SEPARATOR = "_"

'一覧識別子 + "_" + index + "_" + name の分解
Function GetIndexAndNameFromNameInList(nm, lstId)
   Dim o
   Dim idx, childNm
   Set o = CreateCollection()


   If Mid(nm, 1, Len(lstId) + 1) = lstId & LISTID_SEPARATOR Then
      If InStr((Len(lstId) + 1) + 1, nm, LISTID_SEPARATOR) > (Len(lstId) + 1)  + 1 Then
         idx = CLng(Mid(nm, Len(lstId) + 1 + 1 _
                  , InStr(Len(lstId) + 1 + 1, nm, LISTID_SEPARATOR) _
                      - (Len(lstId) + 1) - 1 _
                ))
         childNm = Mid(nm, Len(lstId & LISTID_SEPARATOR & idx & LISTID_SEPARATOR) + 1)
         o.Add "idx", idx
         o.Add "nm", childNm
      End If
   End If
   
   Set GetIndexAndNameFromNameInList = o
End Function

簡単なものであるが、一覧データを復元できるような仕組みにしている。 1階層のみで一覧の中の一覧は対応していない。

使用例

<%
'省略


Sub InitializePage()
    TemplateInitializePage
    
    Dim i
    Set gData = CreateFormCollection(Array("lst1", "lst2"))
    If gData("lst1").Count = 0 Then
       For i = 0 To 5 - 1
          gData("lst1").Add i, CreateCollection()
          gData("lst1")(i).Add "txtCol1", Right("0000" & (i+1), 4)
          gData("lst1")(i).Add "txtCol2", "あいうえお" & i
       Next
    End If
End Sub

Sub RenderHtml()
    If Not gNoErrorHandlingForDebug Then
        On Error Resume Next
    End If
    
    Dim i, s, row, nm
%>

<html>
<head>
<title>sample04</title>
</head>
<body>
<form name="f" method="post">
   <input type="text" name="txtVal1" value="<%= Server.HTMLEncode(gData("txtVal1")) %>"><br>
   <input type="text" name="txtVal2" value="<%= Server.HTMLEncode(gData("txtVal2")) %>"><br>
   <table>
      <tr><th>Index</th>
          <th>列1</th>
          <th>列2</th>
      </tr>
<%
   For i = 0 To gData("lst1").Count - 1
      Set row = gData("lst1")(i)
%>
      <tr><td><%=i%></td>
         <td><input type="text" name="<%="lst1_" & i & "_txtCol1"%>" value="<%=Server.HTMLEncode(row("txtCol1"))%>"></td>
         <td><input type="text" name="<%="lst1_" & i & "_txtCol2"%>" value="<%=Server.HTMLEncode(row("txtCol2"))%>"></td>
      </tr>
<%
   Next
%>
   </table>

   <div><input type="button" name="btnPost" value="post" onclick="document.f.submit();">
   </div>
%>
</form>
</body>
</html>
<%
End Sub 'RenderHtml
%>

ポイントは、gDataのKey名とinputタグのname属性の値を一致させる。こうすることで、CreateFormCollection()で処理してくれる。

一覧形式のデータについては、name属性を「(一覧の識別子)+"_"+index+"_"+(名前)」の形式とすることで、一覧をコレクション配列として復元している。

javascript出力時のエスケープ処理

htmlに文字を埋め込む場合、< > & をエスケープする必要がある。

aspでは、Server.HTMLEncode()が用意されているのでこれを利用する。

javascriptの場合についても、入力値などの文字列をjavascriptに埋め込む場合にはエスケープが必要である。

とりあえず、下記のような関数を用意した。

Function JsEscape(v)
    Dim s
    s = Replace(v, "\"     , "\\")
    s = Replace(s, vbCrLf  , "\n")
    s = Replace(s, vbCr    , "\n")
    s = Replace(s, vbLf    , "\n")
    s = Replace(s, """"    , "\""")
    s = Replace(s, "'"     , "\'")
    s = Replace(s, Chr(&H8), "\b")
    s = Replace(s, Chr(&HC), "\f")
    s = Replace(s, Chr(&H9), "\t")
    s = Replace(s, Chr(&HB), "\v")
    
    s = Replace(s, "&"     , "\x26")
    s = Replace(s, "<"     , "\x3c")
    s = Replace(s, ">"     , "\x3e")
    JsEscape = s
End Function

htmlに埋め込むことが前提なので、< > & を追加している。 エスケープ対象文字はほかにもあって、Unicode文字のLine SeparatorやParagraph Separatorがあるとのこと。

JSONの場合には、javascriptと同じエスケープでは駄目なので、以下を用意した。

Function JsonEscape(v)
    Dim s
    s = Replace(v, "\"     , "\\")
    s = Replace(s, vbCrLf  , "\n")
    s = Replace(s, vbCr    , "\n")
    s = Replace(s, vbLf    , "\n")
    s = Replace(s, """"    , "\""")
    s = Replace(s, "/"     , "\/")
    s = Replace(s, Chr(&H8), "\b")
    s = Replace(s, Chr(&HC), "\f")
    s = Replace(s, Chr(&H9), "\t")
    s = Replace(s, Chr(&HB), "\u000b")
    
    s = Replace(s, "'"     , "\u0027")
    
    s = Replace(s, "&"     , "\u0026")
    s = Replace(s, "<"     , "\u003c")
    s = Replace(s, ">"     , "\u003e")
    JsonEscape = s
End Function

単純にエスケープ対象の文字をReplace関数で置き換えているが、もっといい方法があるだろう。 少なくとも1文字ずつ処理したほうが速い気がする。