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:
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:
- 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.
- 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:
<%= 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:
<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.
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:
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:
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
:
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
.
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
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)
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.
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
Public Function UListExt(list, list_text_field, list_attribs)
UListExt = ListExt("ul", list, list_text_field, list_attribs)
End Function
Public Function UList(list)
UList = UListExt(list, empty, empty)
End Function
Public Function OListExt(list, list_text_field, list_attribs)
OListExt = ListExt("ol", list, list_text_field, list_attribs)
End Function
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.
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=''>"
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()"
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>
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:
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:
<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.