Login   /   Register

An Improved Dictionary Object

Rate this article
     6 votes, average: 4.67 out of 56 votes, average: 4.67 out of 56 votes, average: 4.67 out of 56 votes, average: 4.67 out of 56 votes, average: 4.67 out of 5
Loading ... Loading ...
June 19th, 2008 by Yaron Assa

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
    
        arrKeys = Me.Keys
        Key = arrKeys(iIndex)    
    End Function
    
    Public Function Clone 'Returns a copy of the dictionary
        Dim i
        Dim oResult
    
        Set oResult = New NewDictionary
    
        For i = 0 to Me.Keys - 1
            oResult.Add Me.Key(i), Me.Item(i)
        Next
    
        Set Clone = oResult
    End Function
    
    Public Sub Merge(oOutsideDictionary) 'Merges the dictionary with another one
        Dim i
    
        For i = 0 to oOutsideDictionary.Count - 1
            'Add the value, but don’t overwrite
            If Not Me.Exists(oOutsideDictionary.Key(i)) Then _
                Me.Add oOutsideDictionary.Key(i), oOutsideDictionary.Item(i)
        Next
    End Sub
 
    Sub Export(sFileName) 'Exports the dictionary to a text file
        Dim i
        Dim oFSO
        Dim oFile
        Dim sData
    
       On Error Resume Next 'Protects from object items
    
        'First, create a string holding the dictionary data
        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 '|’
    
        'Now, write the string to an external file
        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) 'Builds the dictionary from an external file
        Dim i
        Dim oFSO
        Dim oFile
        Dim sData
        Dim arrData, arrSingleField
    
        On Error Resume 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
    
        'Split the data string to its separate key>item pairs
        arrData = Split(sData, "|")
    
        For i = 0 to uBound(arrData)
            'Split each pair
            arrSingleField = Split(arrData(i), ">")
 
            'Add the value to the dictionary
            Me.Add arrSingleField(0), arrSingleField(1)
        Next
    
        On Error Goto 0
    End Sub
 
End Class

You can also download a clean version of the code:

Dictionary.VBS

Posted in Data Structures, Dictionary Objects, Using Classes

12 Responses to “An Improved Dictionary Object”

  1. Stefan Thelenius Says:

    [-]

    Impressive Yaron, I’ll try it out immediatly!

    /Stefan

  2. chidambaram_l Says:

    [-]

    Excellent. Came at a right time when Iam Implementing Dictionary object. Thank you Yaron!!

  3. michael.entwistle Says:

    [+]

    Hi Yaron, Code works well after the typo in the Item Function has the following line corrected: sReakKey = Key 'We can use ... ...

  4. Yaron Assa Says:

    [-]

    Thanks a lot, I’ll correct it soon.

  5. huemach Says:

    [-]

    This class still missing function set value to existing key
    Ex:
    oDic.Item(”key1″) = “sValue”

  6. roxyie Says:

    [-]

    Good one. Explained well about creating wrapper using basic dictionary functions.

  7. QTP: Working with Multiple Browser Applications - A Concept (by Anshoo Arora) | Relevant Codes Says:

    [-]

    […] An Improved Dictionary Object by Yaron Assa (AdvancedQTP, SolmarKN) […]

  8. heqingbluesky Says:

    [+]

    Take a close look at it again. Learn it again on how to operate dictionary object among QTP even though I have no chance to apply ... ...

  9. QTP: Working with Multiple Browser Applications (Revised) | Relevant Codes by Anshoo Arora Says:

    [+]

    [...] 1. An Improved Dictionary Object by Yaron Assa (AdvancedQTP, SolmarKN) 2. Singleton Pattern by Yaron Assa (AdvancedQTP, [...... ...

  10. brownnrl Says:

    [+]

    Hi! So I was going through the "Implementing a Queue" article, and I tried to use a NewDictionary object in place of the CreateOb... ...

  11. brownnrl Says:

    [-]

    And I almost forgot, I love this site! Thanks!

  12. Jeff Nyman Says:

    [+]

    This is a good article -- but it really needs to be modified to incorporate brownnrl's changes. (Comment 10.) That's the only way ... ...

Leave a Reply

You must be logged in to post a comment.

This article was viewed 3007 times