Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VBScript

A Simple Technique To Add Optional Key/Value Method Parameters In Classic ASP

5.00/5 (1 vote)
12 Apr 2018CPOL7 min read 11.1K  
How to add optional parameters to any function in VBScript & Classic ASP

Introduction

This article will cover the creation and use of a simple and fast data structure that supports forward-only key-value pairs for use as method parameters. We will cover the data structure, the methods that facilitate its use, and an example implementation in an HTML Helper class.

Note: While it may seem confusing at first, the technique described in this article is extremely powerful and is used extensively to power Sane, a full-featured MVC framework I created that brings sanity to Classic ASP development. Sane provides database migrations, domain & view models, parameterized automappers, pluggable validators, enumerables, and more. Available at github.com/davecan/Sane. The KVArray powers the HTML helpers and domain repositories. You can search for KVArray there or use the link to the search at the bottom of this article. I don't expect anyone to actually use Sane in a production environment, but if you have to maintain Classic ASP, there may be some techniques you can borrow to clean up your code. Especially with modern systems, VBScript and Classic ASP can be far more powerful than the spaghetti code of the old days, and Sane should demonstrate that clearly, but modern languages and frameworks will still outperform it.

The KVArray: A Simple Twist on Arrays

While developing the Sane framework, I was struck once again by VBScript's inherent limitation in that it cannot support key/value parameters. I really needed the flexibility these provide, and was frustrated by their absence. Instead of settling (again) for this limitation, I set out to find a way to solve the problem. The result is the KVArray, short for Key Value Array. This simple twist has a profound benefit: it unlocks a tremendous amount of power in ASP and allows us to significantly streamline our code, which in turn helps us create much better applications.

The KVArray is nothing more than an array that follows "one weird rule":

A KVArray is a standard VBScript variant array where the total number of elements is always even, with each key contained in an even-numbered position and its corresponding value contained in the immediately following odd-numbered position.

Now that the definition is out of the way, this is what it looks like in code:

VBScript
dim kv_array : kv_array = Array("var_1", "value 1", "var_2", "value 2")

By itself, this is unremarkable. "Why not use a Dictionary" you ask? Two reasons:

  1. Creating a dictionary requires instantiating a COM component. This can be expensive, and we need the ability to use this structure everywhere. Instantiating potentially dozens of COM components is not optimal.
  2. For our purposes, we don't need the ability to retrieve a value by passing the name of the key. Therefore, this data structure is optimized for fast forward iteration, not arbitrary key lookup as with a Dictionary object.

So then you say, "But this is just the same old pass-an-array-as-a-parameter method used since forever." Well, not exactly. Those approaches required the method developer to re-create the key/value extraction process each time they wanted to allow arrays. That is cumbersome and prone to error, and also means each method has the option of treating the arrays slightly differently. With this approach, the processing is standardized, and the method caller can expect the data structure to be used the same way each time.

So, what types of things can we do with it? Here are some examples:

VBScript
<%= 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")) %>

With the following output:

HTML
<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>

As you can see, a primary pattern is to use the KVArray to iterate over and attach the key/value pairs as corresponding HTML key/value pairs for use in URL querystrings, HTML attributes, etc. These are trivial examples -- there is much more that we can do with a KVArray, beyond simply generating HTML.

The KVArray derives its power from the helper methods dedicated to it. There are two primary methods: KeyVal and KVUnzip. KeyVal walks the KVArray and pops out a key/value pair at each step, while KVUnzip creates two new arrays, one containing the keys and the other the values. There is also a third method, KVAppend, that allows us to append a key and value to an existing KVArray, but in practice it is rarely used. KeyVal will be discussed in this article. You can see the method definitions in lib.Collections.asp in Sane. KVUnzip usage is demonstrated in the Order Repository class which constructs the SQL on the fly using the keys to populate the where clause and then passes the query and values array to the Database class (through DAL.query) which processes the query using prepared statements.

KeyVal()

This is the primary method used to manipulate the KVArray. It is used within a For...Next loop to extract the current key/value pair from the KVArray. The For...Next loop must iterate by two each time using Step 2, which causes the index to increment by 2 with each iteration. Within the loop, the KeyVal method is called and passed the KVArray, the current index, and two ByRef parameters key and val; these will contain the current key and value for this iteration.

VBScript
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

A trivial usage example:

VBScript
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

The result of which is the following output:

VBScript
key_1 = val_1
key_2 = val_2
key_3 = val_3

As you can see, as the loop iterates over the array's key/value pairs, the KeyVal method extracts the current key and value and returns them via the ByRef parameters. Now let's look at some real-world examples of the power this data structure + loop pattern + helper method pattern unleashes.

HTML Helper Class

The HTML_<code>Helper_Class provides methods that accept a variety of parameters and generate HTML tag strings. The class follows a basic naming pattern that separates the base case of a function from its "extended" case. For example, the method named LinkTo internally calls public method LinkToExt, the method LinkToIf internally calls public method LinkToExtIf, etc. This pattern is used extensively to denote a function that accepts additional parameters such as one or more KVArrays. The base case (LinkTo) handles most scenarios, and the "extended" case (LinkToExt) handles additional scenarios.

Let's look at the simplest methods: LinkTo and LinkToExt:

VBScript
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

As you can see, the LinkTo method internally calls the LinkToExt method and passes the VBScript keyword empty to denote an empty parameter. This allows us to easily check for a parameter's existence via IsEmpty.

The KeyVal method is used in the HTML Helper class' private methods that create the HTML attribute string and URL parameter string. The HtmlAttribs and UrlParams functions each receive a KVArray and then iterate over the array in pairs (step 2), passing the array and current index to a subordinate function (HtmlAttrib and UrlParam, respectively) which in turn call KeyVal and return the constructed HTML attribute string or URL parameter string.

VBScript
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

By creating a generic KeyVal method and the above HtmlAttribs method, we can now write very simple HTML generator functions. For example, the following methods enable us to dynamically create ordered and unordered HTML lists from code. The method ListExt generates a list based on an input array (normal array) or recordset. If a recordset is passed, then list_text_field is the name of the recordset field to extract and display in the list. The list_attribs parameter is a KVArray that contains key/value pairs that will be turned into HTML attributes on the parent tag.

The helper methods UList, UListExt, OList, and OListExt wrap ListExt and simplify list creation.

VBScript
'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 we can generate lists, why not generate dropdowns dynamically? This is actually quite easy with our new capabilities.

VBScript
'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
VBScript
<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>

Note: The name HTML here is function defined in the file lib.HTML.asp using the singleton naming pattern discussed in my previous article. This allows you to include a file containing a class and have an instance injected into global scope, effectively simulating the namespace functionality of other languages. For example, this code is at the end of the file lib.HTML.asp and is executed any time the file is included:

VBScript
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

The two empty parameters in DropDownList correspond to the non-used recordset parameters. If a recordset is used, the call looks like this:

VBScript
<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>

In this case, the passed list is a recordset named rs, the field extracted for each option element's ID is database_id, and the field extracted for each option element's text is name.

There are many more features in this class not covered in this article, including using it for domain repositories and database queries. Review the Sane project linked above for more examples of the expressive power the KVArray provides. Here's a search for KVarray in the project on GitHub to see more examples.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)