在 Classic ASP 中添加可选的键/值方法参数的简单技术





5.00/5 (1投票)
如何在 VBScript 和经典 ASP 中的任何函数中添加可选参数
引言
本文将介绍一种简单快速的数据结构的创建和使用,该结构支持前向键值对,可用作方法参数。我们将介绍该数据结构、促进其使用的方法,以及在 HTML Helper 类中的一个示例实现。
注意:虽然乍一看可能令人困惑,但本文描述的技术功能极其强大,并在我创建的、为 Classic ASP 开发带来理智的全功能 MVC 框架 Sane 中广泛使用。Sane 提供数据库迁移、领域和视图模型、参数化自动映射器、可插拔验证器、可枚举对象等等。可在 github.com/davecan/Sane 获取。KVArray 为 HTML 助手和领域存储库提供支持。您可以在那里搜索 KVArray,或使用本文底部的搜索链接。我并不期望任何人在生产环境中使用 Sane,但如果您必须维护 Classic ASP,可能会有一些可以借鉴的技术来清理您的代码。尤其是在现代系统中,VBScript 和 Classic ASP 可以比老式的意大利面条式代码强大得多,Sane 应该清楚地证明这一点,但现代语言和框架仍然会超越它。
KVArray:数组的简单变体
在开发 Sane 框架时,我再次被 VBScript 的固有局限性所困扰,即它不支持键/值参数。我真的需要它们提供的灵活性,并因它们的缺失而感到沮丧。我没有(再次)接受这个限制,而是着手寻找解决问题的方法。结果就是 KVArray,Key Value Array 的缩写。这个简单的变体具有深远的益处:它释放了 ASP 中巨大的力量,并允许我们显著简化代码,这反过来又帮助我们创建更好的应用程序。
KVArray 不过是一个遵循“一个奇怪规则”的数组
KVArray 是一个标准的 VBScript 变体数组,其中元素的总数始终为偶数,每个键包含在偶数位置,其对应的值包含在紧随其后的奇数位置。
现在定义已经完成,它在代码中是这样的
dim kv_array : kv_array = Array("var_1", "value 1", "var_2", "value 2")
就其本身而言,这并不引人注目。“为什么不使用 Dictionary 呢?”你问。两个原因
- 创建字典需要实例化 COM 组件。这可能代价高昂,而我们需要在任何地方使用这种结构。实例化几十个 COM 组件并非最佳选择。
- 出于我们的目的,我们不需要通过传递键的名称来检索值。因此,这种数据结构针对快速前向迭代进行了优化,而不是像 Dictionary对象那样进行任意键查找。
于是你可能会说,“但这不就是老一套的传数组作为参数的方法,自古以来就一直在用。”嗯,不完全是。那些方法要求方法开发者每次想要允许数组时都重新创建键/值提取过程。这很麻烦,容易出错,也意味着每个方法都可以选择以稍微不同的方式处理数组。通过这种方法,处理是标准化的,方法调用者可以期望数据结构每次都以相同的方式使用。
那么,我们可以用它做些什么呢?这里有一些例子
<%= HTML.LinkTo("This is a simple link", url) %>
<%= HTML.LinkToExt("This link has querystring params", url, 
                    array("var_1", 1, "var_2", "value_2", empty) %>
<%= HTML.LinkToExt("This link is highlighted and opens in a new window", 
                    url, empty, array("class", "highlight", "target", "_blank")) %>
输出如下
<a href='http://www.example.com'>This is a simple link</a>
<a href='http://www.example.com?var_1=1&var_2=value_2'>This link has querystring params</a>
<a href='http://www.example.com' class='highlight' target='_blank'>This link is highlighted and opens in a new window</a>
如您所见,一个主要模式是使用 KVArray 遍历并将键/值对作为相应的 HTML 键/值对附加,用于 URL 查询字符串、HTML 属性等。这些都是简单的示例——我们用 KVArray 可以做的事情远不止生成 HTML。
KVArray 的强大之处在于其专门的辅助方法。主要有两种方法:KeyVal 和 KVUnzip。KeyVal 遍历 KVArray 并在每一步弹出一个键/值对,而 KVUnzip 创建两个新数组,一个包含键,另一个包含值。还有第三个方法,KVAppend,它允许我们向现有的 KVArray 追加键和值,但实际上很少使用。本文将讨论 KeyVal。您可以在 Sane 中的 lib.Collections.asp 中查看方法定义。KVUnzip 的用法在 Order Repository 类中进行了演示,该类使用键动态构建 SQL 以填充 where 子句,然后将查询和值数组传递给 Database 类(通过 DAL.query),该类使用预处理语句处理查询。
KeyVal()
这是用于操作 KVArray 的主要方法。它在 For...Next 循环中使用,用于从 KVArray 中提取当前的键/值对。For...Next 循环每次必须以 Step 2 递增,这导致索引每次迭代递增 2。在循环中,调用 KeyVal 方法并传递 KVArray、当前索引,以及两个 ByRef 参数 key 和 val;它们将包含本次迭代的当前键和值。
Sub KeyVal(kv_array, key_idx, ByRef key, ByRef val)
  if (key_idx + 1 > ubound(kv_array)) then err.raise 1, "KeyVal", 
        "expected key_idx < " & ubound(kv_array) - 1 & ", got: " & key_idx
  key = kv_array(key_idx)
  val = kv_array(key_idx + 1)
End Sub
一个简单的使用示例
dim keys
keys = Array("key_1", "val_1", "key_2", "val_2", "key_3", "val_3")
dim key, val, idx
For idx = 0 to UBound(keys) Step 2
  KeyVal keys, idx, key, val
  response.write key & " = " & val & "<br>"
Next
结果是以下输出
key_1 = val_1
key_2 = val_2
key_3 = val_3
正如您所看到的,当循环遍历数组的键/值对时,KeyVal 方法提取当前的键和值,并通过 ByRef 参数返回它们。现在让我们看一些这个“数据结构 + 循环模式 + 辅助方法”模式所释放强大功能的实际示例。
HTML Helper 类
HTML_Helper_Class 提供接受各种参数并生成 HTML 标签字符串的方法。该类遵循一个基本命名模式,将函数的基例与其“扩展”情况区分开来。例如,名为 LinkTo 的方法内部调用 public 方法 LinkToExt,方法 LinkToIf 内部调用 public 方法 LinkToExtIf,等等。此模式广泛用于表示接受额外参数(例如一个或多个 KVArray)的函数。基例(LinkTo)处理大多数场景,而“扩展”情况(LinkToExt)处理额外的场景。
让我们看看最简单的方法:LinkTo 和 LinkToExt
Public Function LinkTo(link_text, url)
  LinkTo = LinkToExt(link_text, url, empty, empty)
End Function
Public Function LinkToExt(link_text, url, params, attribs)
  LinkToExt = "<a href='" & url & UrlParams(params) & "' " & _
              HtmlAttribs(attribs) & ">" & link_text & "</a>"
End Function
如您所见,LinkTo 方法内部调用 LinkToExt 方法并传递 VBScript 关键字 empty 来表示一个空参数。这使我们能够通过 IsEmpty 轻松检查参数是否存在。
KeyVal 方法用于 HTML 帮助器类的 private 方法,这些方法创建 HTML 属性字符串和 URL 参数字符串。HtmlAttribs 和 UrlParams 函数都接收一个 KVArray,然后成对(step 2)遍历数组,将数组和当前索引传递给一个从属函数(分别是 HtmlAttrib 和 UrlParam),这些从属函数又调用 KeyVal 并返回构建的 HTML 属性 string 或 URL 参数 string。
Private Function HtmlAttribs(attribs)
  dim result : result = ""
  if not IsEmpty(attribs) then
    if IsArray(attribs) then
      dim idx
      for idx = lbound(attribs) to ubound(attribs) step 2
        result = result & " " & HtmlAttrib(attribs, idx)
      next
    else  ' assume string or string-like default value
      result = attribs
    end if
  end if
  HtmlAttribs = result
End Function
Private Function HtmlAttrib(attribs_array, key_idx)
  dim key, val
  KeyVal attribs_array, key_idx, key, val
  HtmlAttrib = Encode(key) & "='" & Encode(val) & "'"
End Function
Private Function UrlParams(the_array)
  dim result : result = ""
  if not isempty(the_array) then
    result = result & "?"
    dim idx
    for idx = lbound(the_array) to ubound(the_array) step 2
      result = result & GetParam(the_array, idx)
      'append & between parameters, but not on the last parameter
      if not (idx = ubound(the_array) - 1) then result = result & "&"
    next
  end if
  UrlParams = result
End Function
Private Function GetParam(params_array, key_idx)
  dim key, val  
  KeyVal params_array, key_idx, key, val
  GetParam = key & "=" & val
End Function
通过创建通用的 KeyVal 方法和上述 HtmlAttribs 方法,我们现在可以编写非常简单的 HTML 生成器函数。例如,以下方法使我们能够从代码中动态创建有序和无序 HTML 列表。方法 ListExt 根据输入数组(普通数组)或 记录集 生成列表。如果传递了 记录集,则 list_text_field 是要提取并在列表中显示的 记录集 字段的名称。list_attribs 参数是一个 KVArray,其中包含将转换为 parent 标签上 HTML 属性的键/值对。
辅助方法 UList、UListExt、OList 和 OListExt 封装了 ListExt 并简化了列表创建。
'If list is a recordset then list_text_field is required, otherwise list is assumed to be an
'array and each array element becomes the text field in the output list.
'list_attribs is a KVArray of HTML attributes for the parent list tag
Public Function ListExt(parent_tag_name, list, list_text_field, list_attribs)
  dim item
  dim s : s = Tag(parent_tag_name, list_attribs) & vbCR
    Select Case typename(list)
      Case "Recordset"
        Do Until list.EOF
          s = s & "<li>" & CStr(list(list_text_field)) & "</li>" & vbCR
          list.MoveNext
        Loop
        
      Case "Variant()"
        dim i
        For i = 0 to ubound(list)
          s = s & "<li>" & CStr(list(i)) & "</li>" & vbCR
        Next
    End Select
  s = s & Tag_(parent_tag_name) & vbCR
  ListExt = s
End Function
'shortcut for ListExt for unordered lists
Public Function UListExt(list, list_text_field, list_attribs)
  UListExt = ListExt("ul", list, list_text_field, list_attribs)
End Function
'shortcut for UListExt() that eliminates the HTML ID for the ul element and only works for arrays
Public Function UList(list)
  UList = UListExt(list, empty, empty)
End Function
'shortcut for ListExt for ordered lists
Public Function OListExt(list, list_text_field, list_attribs)
  OListExt = ListExt("ol", list, list_text_field, list_attribs)
End Function
'shortcut for OListExt() that eliminates the HTML ID for the ol element and only works for arrays
Public Function OList(list)
  OList = OListExt(list, empty, empty)
End Function
如果我们可以生成列表,为什么不动态生成下拉菜单呢?通过我们的新功能,这实际上非常容易。
'If list is a recordset then option_value_field and option_text_field are required.
'If list is an array the method assumes it is a KVArray and those parameters are ignored.
'Side Effect: If list is a recordset, the recordset is at EOF after function returns.
Public Function DropDownList(id, selected_value, list, option_value_field, option_text_field)
  DropDownList = DropDownListExt(id, selected_value, list, option_value_field, option_text_field, empty)
End Function
Public Function DropDownListExt(id, selected_value, list, option_value_field, option_text_field, attribs)
  If IsNull(selected_value) then
    selected_value = ""
  Else
    selected_value = CStr(selected_value)
  End If
  
  dim item, options, opt_val, opt_txt 
  options = "<option value=''>"  ' first value is "non-selected" blank state
  select case typename(list)
    case "Recordset"
      do until list.EOF
        If IsNull(list(option_value_field)) then
          opt_val = ""
        Else
          opt_val = CStr(list(option_value_field))
        End If
          
        opt_txt = list(option_text_field)
        If Not IsNull(opt_val) And Not IsEmpty(opt_val) then
          options = options & "<option value='" & Encode(opt_val) & "' " & _
                    Choice((CStr(opt_val) = CStr(selected_value)), "selected='selected'", "") & _
                    ">" & Encode(opt_txt) & "</option>" & vbCR
        End If
        list.MoveNext
      loop
    case "Variant()"    'assumes KVArray
      dim i
      for i = 0 to ubound(list) step 2
        KeyVal list, i, opt_val, opt_txt
        options = options & "<option value='" & Encode(opt_val) & "' " & _
                  Choice((opt_val = selected_value), "selected='selected'", "") & ">" _
                  & Encode(opt_txt) & "</option>" & vbCR
      next
  end select
  DropDownListExt = "<select id='" & Encode(id) & "' name='" & Encode(id) & "' " & _
                    HtmlAttribs(attribs) & " >" & vbCR & options & "</select>" & vbCR
End Function
<p>
  <%= HTML.Label("States from array, Hawaii selected, onchange event", "state_3") %>
  <%= HTML.DropDownListExt("state_3", "HI", states, empty, empty, _
                            array("onchange", "showChangedValue(state_3)")) %>
</p>
注意:这里的名称 HTML 是在文件 lib.HTML.asp 中使用我在上一篇文章中讨论的单例命名模式定义的函数。这允许您包含一个包含类的文件,并将一个实例注入到全局范围,有效地模拟了其他语言的命名空间功能。例如,此代码位于文件 lib.HTML.asp 的末尾,并且每当包含该文件时都会执行
dim HTML_Class__Singleton
Function HTML()
  If IsEmpty(HTML_Class__Singleton) then set HTML_Class__Singleton = new HTML_Class
  set HTML = HTML_Class__Singleton
End Function
DropDownList 中的两个空参数对应于未使用的 recordset 参数。如果使用 recordset,调用如下所示
<p>
  <%= HTML.Label("Styled dropdown", "database_3") %>
  <%= HTML.DropDownListExt("database_3", 1, rs, "database_id", "name", 
   aray("class", "styled_dropdown", "onchange", "showChangedValue(database_3)")) %>
</p>
在这种情况下,传递的列表是一个名为 rs 的 recordset,为每个选项元素的 ID 提取的字段是 database_id,为每个选项元素的文本提取的字段是 name。
本文未涵盖此类的更多功能,包括将其用于领域存储库和数据库查询。查看上面链接的 Sane 项目以获取 KVArray 提供的表达能力的更多示例。这是 GitHub 项目中对 KVarray 的搜索,以查看更多示例。

