The Scripting.Dictionary object can be an extremely useful tool for storing and retrieving information. We’ve had a whole series of QTips covering it’s basic uses, as well as some advanced articles of using it as a reserved global dictionary, a parameter storage for generic functions (here and here), and more.
However, with all its power, the dictionary object has some inherent shortcomings – you can’t retrieve a value by its index (which can really be annoying when using it in loops), it will throw an error when trying to add an item with a duplicate key, and it lacks merging, importing and exporting abilities.
This article will show a step by step guide for building a new dictionary object, which will be more robust and flexible than the native Scripting.Dictionary. You might want to brush up your VBScript class knowledge before reading on.
Specifically, we’re going to write a wrapper – meaning, we’ll be using an inner scripting.dictionary object, and wrap a more sophisticated mechanism around it. Wrappers are very common in programming – they allow us to expand upon the work of others, and “stand on the shoulders of Giants”, sort of speak.
Wrappers almost never “breaks” the original object they’re wrapping. Meaning that every piece of code that worked with the original dictionary, should ideally work with our new dictionary wrapper. This means that we should keep all the object’s methods and properties names, and only expand with new ones, and new abilities to the existing ones.
So, let’s get this show on the road
Basic Interfaces
First, let’s define our basic class structure, with a private (hidden) variable holding a Scripting.Dictionary object. We’ll also insert initialization and termination event procedures for the object:
Sub Class_Initialize 'Will fire automatically when create a new instance
Set oDic = CreateObject("Scripting.Dictionary")
End Sub
Sub Class_Terminate 'Will fire automatically when an instance is destroyed
Set oDic = Nothing
End Sub
Next, we’ll add some simple public interfaces, which will allow the user to use the new dictionary like the regular native one. For example, we’ll need to add a .Count property to our new dictionary, but since there was nothing wrong with the old dictionary’s .Count property, we could immediately return the answer from our hidden inner dictionary.
'All these properties are read-only
'This is why they only contain a Get block
Public Property Get Count 'Returns the number of items in our dictionary
Count = oDic.Count 'The answer is the number of items in the inner dictionary
End Property
Public Property Get Keys 'Returns the dictionary keys (array)
Keys = oDic.Keys
End Property
Public Property Get Items 'Returns the dictionary items (array)
Items = oDic.Items
End Property
Public Property Get Exists(Key) 'Returns a True/False if the Key exists
Exists = oDic.Exists(Key)
End Property
As you can see, since these interfaces don’t require any additional tweaking and manipulations, they’re very direct and simple. The next interfaces won’t be so straightforward.
Modified Interfaces
We’ll begin with the .Remove method. The original dictionary object throws an error when trying to remove an item in a key which doesn’t exist. We’ll perform a simple patch – only remove the item if the key exists:
Public Sub Remove(Key)
If oDic.Exists(Key) Then oDic.Remove(Key)
End Sub
Let’s continue with something a bit trickier – the .Add method, which adds a new item to our dictionary. The original method had an annoying “feature” – when adding an item with a key that already exists, the script threw an error, instead of simply overriding the current item in that key. In our new method, we’ll do just that:
Public Sub Add(Key, Value)
If oDic.Exists(Key) Then
'We cannot simply add the item, we have to replace it
oDic.Item(Key) = Value
Else
'Here we can add the key as usual
oDic.Add Key, Value
End If
End Sub
But wait, things aren’t so simple. Dictionaries can contain objects as items (even other dictionaries!), but in VBScript, assigning an object to a variable requires using the special keyword Set. So if the Value we’re trying to add in an object, we’ll have to execute Set oDic.Item(Key) = Value, not simply oDic.Item(Key) = Value. This will quickly turn into an impossible If structure, so let’s present a simpler alternative – if the value exists, we’ll remove it, and then add it as usual.
Public Sub Add(Key, Value)
Call Me.Remove(Key)
oDic.Add Key, Value
End Sub
Let’s take a second to see what we’ve done here. We want to remove the item only if the key already exists in our dictionary. But we’ve just created a .Remove method which does just that! So instead of recoding the algorithm, we reuse the code we’ve already written, and call our new and improved .Remove method. Once that’s taken care of, we can add the new key and value as usual.
And now the crown jewel: the .Item method. It will be quite complicated, we’ll have to distinguish between an “real” key, and a numeric key, as well as between returning a regular item and returning an object. First, let’s deal with the key issue:
Public Function Item(Key)
Dim arrKeys 'Will hold the inner dictionary keys
Dim sRealKey
arrKeys = oDic.Keys
If IsNumeric(Key) Then
sRealKey = arrKeys(Key) 'We have to translate the number to the corresponding key
Else
sReakKey = Key 'We can use the key as it is
End If
Item = oDic.Item(sRealKey)
End Function
Notice that we’re ignoring a possible exception – what if we have a key named “1”? Our code will interpret it as a numeric index and not an actual key. We could build a workaround, but let’s keep things simple. We’ll let this one slide by and say we don’t allow any such keys.
Now let’s deal with the second problem – we got the key, what’s left is returning the value. Here’ we encounter the same problem we have in the .Add method – a simple item and an object item require different handling (the object item requires using the Set keyword). Unlike the .Add method, we’ll have to tackle this head-on:
Public Function Item(Key) 'Returns an item, either by a key or an index
Dim arrKeys 'Will hold the inner dictionary keys
Dim sRealKey 'The actual key which holds the needed value
arrKeys = oDic.Keys
If IsNumeric(Key) Then
sRealKey = arrKeys(Key) 'We have to translate the number to the corresponding key
Else
sReakKey = Key 'We can use the key as it is
End If
'If the relevant item is an object, we’ll have to use the Set keyword to retun it
If IsObject(oDic.Item(sRealKey)) Then
Set Item = oDic.Item(sRealKey)
Else
Item = oDic.Item(sRealKey)
End If
End Function
New Interfaces
The .Item method was the last of the original dictionary’s interfaces. After we’ve dealt with them, we can now create some new interfaces, adding new functionality and power to our dictionary.
The first new interface we’ll implement is .Key. It will return the key at a certain index. Simple, and very useful:
Public Funciton Key(iIndex)
Dim arrKeys
If iIndex > Me.Count -1 Then Exit Function 'There is no such key
arrKeys = Me.Keys
Key = arrKeys(iIndex)
End Function
Now we can implement another new interface - Clone. It will return a copy of the current dictionary. Perfect for sending it to outer functions with just minor adjustments to some of the items:
Public Function Clone
Dim oResult
Dim i
Set oResult = New NewDictionary
For i = 0 to Me.Count - 1
oResult.Add Me.Key(i), Me.Item(i)
Next
Set Clone = oResult
End Function
And now – what for me is quite a killer feature – merge. We’ll take another new dictionary object, and merge its data into our own. Our dictionary’s data takes precedence, so we’ll only import new values. Notice how these features become very easy to implement once you have the improved .Item and new .Key methods.
Public Sub Merge(oOutsideDictionary)
Dim i
For i = 0 to oOutsideDictionary.Count - 1
If Not Me.Exists(oOutsideDictionary.Key(i)) Then _
Me.Add oOutsideDictionary.Key(i), oOutsideDictionary.Item(i)
Next
End Sub
And, last but not least, we’ll add a .Import and .Export methods, which will transfer the dictionary’s data from and to an external file. Dictionary’s are often used to carry important information, which we might like to save for the next test run. Since the object itself will be destroyed once QTP stops, we need to same the information in an external resource – a text file. The data will be saved in this format: Key>Value|Key>Value|… . Of course that this only applies to simple data-types – objects will not be saved.
Sub Export(sFileName)
Dim i
Dim oFSO
Dim oFile
Dim sData
On Error Resume Next 'Protects from object items
For i = 0 to Me.Count - 1
sData = sData & "|" & Me.Key(i) & ">" & Me.Item(i)
Next
sData = Mid(sData,2) 'Get rid of the first, unneeded '|’
Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFile = oFSO.CreateTextFile(sFileName, True)
Call oFile.Write(sData)
oFile.Close
Set oFile = Nothing
Set oFSO = Nothing
On Error Goto 0
End Sub
Sub Import(sFileName)
Dim i
Dim oFSO
Dim oFile
Dim sData
Dim arrData, arrSingleField
On Error Sesume Next 'Protects from wrong file names
Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFile = oFSO.OpenTextFile(sFileName, 1, True)
sData = oFile.ReadAll
oFile.Close
Set oFile = Nothing
Set oFSO = Nothing
arrData = Split(sData, "|")
For i = 0 to uBound(arrData)
arrSingleField = Split(arrData(i), ">")
Me.Add arrSingleField(0), arrSingleField(1)
Next
On Error Goto 0
End Sub
We can add more methods and properties, but even now we have quite a buffed up dictionary, which can make our lived much easier.
Bringing it all together
Here’s the entire code, with extra comments:
Class NewDictionary
Private oDic 'Will hold the hidden inner dictionary
'****** Class LifeCycle ******
Sub Class_Initialize 'Will fire automatically when create a new instance
Set oDic = CreateObject("Scripting.Dictionary")
End Sub
Sub Class_Terminate 'Will fire automatically when an instance is destroyed
Set oDic = Nothing
End Sub
'****** Basic properties ******
'All these properties are read-only
'This is why they only contain a Get block
Public Property Get Count 'Returns the number of items in our dictionary
Count = oDic.Count 'The answer is the number of items in the inner dictionary
End Property
Public Property Get Keys 'Returns the dictionary keys (array)
Keys = oDic.Keys
End Property
Public Property Get Items 'Returns the dictionary items (array)
Items = oDic.Items
End Property
Public Property Get Exists(Key) 'Returns a True/False if the Key exists
Exists = oDic.Exists(Key)
End Property
'****** Improved Methods ******
Public Sub Remove(Key) 'Removes the key from the dictionary (it it existed)
If oDic.Exists(Key) Then oDic.Remove(Key)
End Sub
Public Sub Add(Key, Value) 'Adds a new value to the dictionary. Overwrites existing values
Call Me.Remove(Key)
oDic.Add Key, Value
End Sub
Public Function Item(Key) 'Returns an item, either by a key or an index
Dim arrKeys 'Will hold the inner dictionary keys
Dim sRealKey 'The actual key which holds the needed value
arrKeys = oDic.Keys
If IsNumeric(Key) Then
sRealKey = arrKeys(Key) 'We have to translate the number to the corresponding key
Else
sReakKey = Key 'We can use the key as it is
End If
'If the relevant item is an object, we’ll have to use the Set keyword to retun it
If IsObject(oDic.Item(sRealKey)) Then
Set Item = oDic.Item(sRealKey)
Else
Item = oDic.Item(sRealKey)
End If
End Function
'****** New Methods ******
Public Function Key(iIndex) 'Returns the key at a given index
Dim arrKeys
If iIndex > Me.Count -1 Then Exit Function 'There is no such key


Yaron Assa
