Jim Groves
Microsoft Corporation
September 1999
Applies To: Microsoft® Outlook® 2000
Summary: This article describes common pitfalls that developers can encounter when working with Microsoft® Outlook® items in a collection and suggests ways to avoid those problems. It shows how to properly access items in a collection, how to delete them, and how to deal with unexpected results. (14 printed pages)
Collections are a very important part of Microsoft® Outlook®. If you are familiar with the Outlook object model, you are already aware of the Items collection, which represents the items contained in a folder, for example. Outlook 2000 adds a number of new collections, including such collection objects as Explorers, Inspectors, OutlookBarGroups, OutlookBarShortcuts, and Selection.
Generally speaking, working with collections in Outlook is not much different from working with collections in other object models. There are basic programming techniques that apply to working with collections of any type. However, special techniques are required when working with the Outlook Items collection because, unlike most collections, it can contain heterogeneous object types. If your code accesses each member of an Items collection and assumes that each member supports the same properties, your program can fail with a run-time error when it encounters a collection member that does not support a property you are trying to access. In addition, changes made to an item in an Items collection can be lost if the item is not properly referenced.
Another common problem programmers encounter when working with Outlook items in a collection is that changes to items are not saved if the item was accessed in the context of a loop. In addition, using a loop to delete Outlook items in a collection can produce unexpected results, as you will see below.
This article examines common problems that occur when items in a collection are accessed in a loop and shows how to properly use loops with an Outlook Items collection. It also shows how to deal with unexpected item types that can appear in an Items collection.
There are two common mistakes programmers make when using a loop to reference Outlook items in a collection:
(By “dynamic reference,” I mean using properties of a parent or container object to retrieve a reference to a collection or item, as with myFolder.Folders
or myItems.Item(x)
.)
This section explains the problems produced by these techniques and shows how to avoid them.
The GetFirst and GetNext methods, along with their mirror methods GetLast and GetPrevious, provide a convenient way to access individual members of an AddressEntries, Folders, or Items collection. However, these methods will not work, and can even produce an infinite loop, if you use them with dynamic references. These problems occur because each dynamic reference creates a new collection. As a result, the GetFirst and GetNext methods are reset by the dynamic reference.
The following example illustrates this problem.
Note The examples in this article are intended to be run as Outlook 2000 Visual Basic® for Applications (VBA) programs. Minor modifications must be made to use them in other contexts.
Before running this code, make sure the Immediate window in the Visual Basic Editor is visible, and that you have at least four task items in your Tasks folder.
Public Sub GetFirstBadExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItem As TaskItem
Dim y As Integer
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = myNS.GetDefaultFolder(olFolderTasks)
Set myItem = myFolder.Items.GetFirst
Do While TypeName(myItem) <> “Nothing”
y = y + 1
If y = 100 Then Exit Do
Debug.Print “Item #” & y & “: “ & myItem.Subject
Set myItem = myFolder.Items.GetNext
Loop
Set myNS = Nothing
Set myFolder = Nothing
Set myItem = Nothing
End Sub
In this example, the GetFirst and GetNext methods are called on a collection retrieved with a dynamic reference. As a result, each time the GetNext method is called, it is always called on a different collection, and so always refers to the second item in the collection. Because the GetNext method always refers to an existing item, the condition of the Do While loop is always true, and so the loop never exits. (The Exit Do statement in the example provides an arbitrary way to terminate the loop to make it easy to stop the code’s execution.)
To correct this problem, you must set an object variable to refer to the collection and call the GetFirst and GetNext methods on the collection referenced by that variable, as shown in this example.
Public Sub GetFirstGoodExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As TaskItem
Dim y As Integer
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = myNS.GetDefaultFolder(olFolderTasks)
Set myItems = myFolder.Items
Set myItem = myItems.GetFirst
Do While TypeName(myItem) <> “Nothing”
y = y + 1
‘ The following line is not required because GetNext
‘ will eventually reach the end of the collection.
If y = 100 Then Exit Do
Debug.Print “Item #” & y & “: “ & myItem.Subject
Set myItem = myItems.GetNext
Loop
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
When you run this code, you will see that each GetNext call fetches a different item from the collection. The Exit Do statement is not needed, since the GetNext method will eventually reach the end of the collection.
Not only can problems occur when you use dynamic references to a collection, but other unexpected results can occur when you use dynamic references to an item in a collection.
As shown in the previous section, dynamic references to a collection refer to different instances of the collection. In much the same way, dynamic references to items in a collection also refer to different instances of an item. Consequently, if you use dynamic references to refer to an item, changes made to one instance of an item are not reflected in other instances.
A common programming error is to use a dynamic reference to set a property for an item and then use another dynamic reference to attempt to save the changed item, as shown in this example. (Before running this sample, create a folder named Test in your Mailbox and copy some messages to the Test folder.)
Public Sub ChangingItemsBadExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim x As Integer
Set myNS = Application.GetNamespace(“MAPI”)
‘ Get reference to user’s Mailbox.
Set myFolder = _
myNS.GetDefaultFolder(olFolderInbox).Parent
‘ Get reference to Test folder in user’s Mailbox.
Set myFolder = myFolder.Folders(“Test”)
Set myItems = myFolder.Items
For x = 1 To myItems.Count
myItems(x).Subject = UCase(myItems(x).Subject)
Debug.Print myItems(x).Subject
myItems(x).Close olSave
Next x
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
End Sub
In this example, the Subject property and the Close method are called on items referenced by myItems(x)
, which is an implicit call to myItems.Item(x)
. Because each time the Item method is used it returns a new instance of an item, every myItems(x)
in the example refers to a different instance of the item. Consequently, myItems(x).Close
saves a copy of the original item, not the instance modified by myItems(x).Subject
=
UCase(myItems(x).Subject)
. You can see how this works by observing the output of the Debug.Print
statement in the Immediate window.
To change an item and save those changes, use the Item method to set an object variable to refer to that item, and then use that object variable exclusively. The following example shows this technique.
Public Sub ChangingItemsGoodExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As MailItem
Dim x As Integer
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = _
myNS.GetDefaultFolder(olFolderInbox).Parent
Set myFolder = myFolder.Folders(“Test”)
Set myItems = myFolder.Items
For x = 1 To myItems.Count
Set myItem = myItems(x)
myItem.Subject = UCase(myItem.Subject)
Debug.Print myItem.Subject
myItem.Close olSave
Next x
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
The myItem
variable always refers to the same instance of the item being changed, and so after the Subject property is set, the changed Subject text is reflected in the output of the Debug.Print
statement and saved by the myItem.Close
statement.
A For Each . . . Next loop can also be used to achieve the same results, as shown in the following example.
Public Sub ChangingItemsGoodExample2()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As MailItem
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = _
myNS.GetDefaultFolder(olFolderInbox).Parent
Set myFolder = myFolder.Folders(“Test”)
Set myItems = myFolder.Items
For Each myItem in myItems
myItem.Subject = UCase(myItem.Subject)
Debug.Print myItem.Subject
myItem.Close olSave
Next
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
This section explained why changes to an item might not be saved; the next section shows why an item in a collection unexpectedly might not be deleted.
A common task in a VBA program in Outlook is to move or delete items from a collection. What may not be apparent, however, is the fact that removing an item from a collection changes that collection, leading to problems when other items in the collection are referenced. For example, a routine that steps through a collection while calling the Delete method for each item in the collection does not delete every item in the collection, as one might expect. Instead, it deletes only half the items in the collection.
The problem lies in the fact that the items in a collection are usually accessed in order. However, deleting an item moves subsequent items up in the order, causing whatever is keeping track of the current item and the number of items in the collection to be out of sync with the updated collection. Consider the following example of a typical routine designed to delete all the items from a collection. (Before running this sample, create a folder named Test in your Mailbox and copy some messages to the Test folder.)
Public Sub DeletingBadExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As MailItem
Set myNS = Application.GetNamespace(“MAPI”)
‘ Get reference to user’s Mailbox.
Set myFolder = _
myNS.GetDefaultFolder(olFolderInbox).Parent
‘ Get reference to Test folder in user’s Mailbox.
Set myFolder = myFolder.Folders(“Test”)
Set myItems = myFolder.Items
For Each myItem In myItems
myItem.Delete
Next
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
When this code is run, only about half of the items in the Test folder are deleted. The For Each . . . Next statement can’t deal with the updated collection that is referenced by myItems
, loses track of the actual number of items in the collection, and so stops short before the end of the collection is reached. Essentially the same results are produced if you try to use a For . . . Next loop. Using the GetFirst and GetNext methods to step through the collection doesn’t work, either, because the collection changes between the GetFirst call and the GetNext call, causing the GetNext method to return Nothing.
The most reliable way to delete multiple items from a collection is to begin at the end of the collection, stepping through it in reverse order, as in this example.
Public Sub DeletingGoodExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim x As Integer
Set myNS = Application.GetNamespace(“MAPI”)
‘ Get reference to user’s Mailbox.
Set myFolder = _
myNS.GetDefaultFolder(olFolderInbox).Parent
‘ Get reference to Test folder in user’s Mailbox.
Set myFolder = myFolder.Folders(“Test”)
Set myItems = myFolder.Items
For x = myItems.Count To 1 Step -1
myItems(x).Delete
Next x
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
End Sub
This technique works because when an item is deleted, it does not affect the position of other items in the collection. You can also use this method if you need to selectively delete items from the collection by putting an If. . . Then statement inside the For . . . Next loop.
When you delete an item, you should not attempt to access an item in the collection whose index is higher than the index of the deleted item because deleting an item has unexpected effects on the position of those items within the collection.
Besides the problem with deleting items, the DeletingBadExample routine in this section suffers from another programming error: It was written with the assumption that all items in the myItems
collection are MailItem objects. As the following section explains, this can often be a dangerous assumption.
Some Outlook folders are very likely to contain a single item type. For example, the default Calendar folder almost certainly will contain only AppointmentItem objects. Many other folders can contain several types of items, however. If your code assumes that a folder contains one type of item and it encounters another type, a run-time error will result.
Examples of items that you might unexpectedly find in a folder are:
This last item will be particularly troublesome for programs written for earlier versions of Outlook that access the default Contacts folder, because such programs often rely on the fact that the Contacts folder rarely contains anything other than contact items.
The following example shows a routine that would probably run without errors in Outlook 98, but is likely to produce a run-time error when it is run in Outlook 2000.
Public Sub ItemTypesBadExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As ContactItem
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = _
myNS.GetDefaultFolder(olFolderContacts)
Set myItems = myFolder.Items
For Each myItem In myItems
Debug.Print myItem.FullName
Next
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
Because myItem
is declared as a ContactItem object, when the For . . . Each loop encounters a DistListItem object, a run-time error will result and the program will stop.
One way to avoid this, of course, is simply to avoid declaring the variable type so that it defaults to Variant. However, you still run the risk of attempting to access a property or method that isn’t supported by the unexpected item type. For example, even if the myItem
variable were a Variant object in the preceding example, the routine would fail if the Contacts folder contained a distribution list item, because the DistListItem object does not have a FullName property.
There are essentially three ways to deal with unexpected items types in a collection:
The following sections show examples of each of these methods.
The easiest way to avoid encountering an unexpected item type in a collection is to exclude all but one item type from the collection. You use the Restrict method of the collection to do this, as shown in this example.
Public Sub RestrictingGoodExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As ContactItem
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = _
myNS.GetDefaultFolder(olFolderContacts)
Set myItems = myFolder.Items
Set myItems = _
MyItems.Restrict(“[MessageClass] = ‘IPM.Contact’”)
For Each myItem In myItems
Debug.Print myItem.FullName
Next
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
The Restrict method takes a string containing a filter and returns a new collection containing only items that match the filter. In the preceding example, this approach ensures that only the expected item type will appear in the collection.
Using the Restrict method becomes cumbersome if you need to work with multiple item types. This might be the case if you are working with several custom message classes, for example. In such cases, you might want to test the type or message class of an item before attempting to access its properties or methods. The following section shows how to use this technique to avoid unexpected item types.
Dealing with unexpected item types can get particularly difficult if your solution uses Microsoft Exchange public folders (which can contain nearly any kind of Outlook item) or a number of custom message classes.
For example, if your code is stepping through the items in a public folder to locate items that were sent or created before a given date, you can test the item type to determine which is the appropriate property to check, as shown in the following sample fragment.
For Each myItem in myItems
Select Case Typename(MyItem)
Case “MailItem”,”MeetingItem”,”PostItem”
myCreateTime = myItem.SentOn
Case Else
myCreateTime = myItem.CreationTime
End Select
Next
If a solution employs custom message classes with custom fields, the code should test whether an item belongs to the appropriate message class before attempting to access the corresponding UserProperty object, as shown in this sample fragment:
If myItem.MessageClass = “IPM.Contact.Customer” Or _
myItem.MessageClass = “IPM.Contact.Prospect” Then
myReferral = Item.UserProperties(“ReferredBy”)
End If
Using the Restrict method or testing the item type or message class is an effective way of ensuring that your code is working only with the item types you’re interested in. Sometimes it is more efficient just to trap the error that results from an unexpected item type and bypass the code that generates an error for the item. The next section explains how to use error handlers in this manner.
In many cases, the methods for handling unexpected item types shown in earlier sections are simply overkill. If it’s improbable that a given collection will contain more than one item type, it is inefficient to test each item in the collection for its type before acting on the item, and even the Restrict method imposes some performance penalties when it is used on a large collection.
Another reason to use error trapping to handle unexpected item types is to deal with items that truly are unexpected. For example, your code could be written to assume that a default folder will contain only one item type. If a new version of Outlook is developed that allows more than one item type in a default folder (such as happened when Outlook 2000 added the distribution list item to the default Contacts folder), well-designed error-handler code can allow your program to continue to work.
As noted previously, the following example would likely run successfully in Outlook 98, but will fail when run in Outlook 2000 if there is even one distribution list item in the user’s Contacts folder.
Public Sub ItemTypesBadExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As ContactItem
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = _
myNS.GetDefaultFolder(olFolderContacts)
Set myItems = myFolder.Items
For Each myItem In myItems
Debug.Print myItem.FullName
Next
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
End Sub
Adding a few lines of error-handling code would reduce the chances that changes to the items supported by the Contacts folder would “break” this code. The following example shows how an error handler would work to reduce this risk.
Public Sub ItemTypesGoodExample()
Dim myNS As NameSpace
Dim myFolder As MAPIFolder
Dim myItems As Items
Dim myItem As ContactItem
Set myNS = Application.GetNamespace(“MAPI”)
Set myFolder = _
myNS.GetDefaultFolder(olFolderContacts)
Set myItems = myFolder.Items
‘Entering item type-dependent area.
On Error GoTo Error_handler
For Each myItem In myItems
Debug.Print myItem.FullName
Skip_section:
Next
Set myNS = Nothing
Set myFolder = Nothing
Set myItems = Nothing
Set myItem = Nothing
Exit Sub
Error_handler:
‘ Don’t need to do anything but resume execution
‘ after item type-dependent code.
Resume Skip_section
End Sub
There are two places in this example that will generate an error if an unexpected data type is encountered in the myItems
collection: the For Each myItem In myItems
statement, and the myItem.Fullname
property call. Since both are simple statements, the On Error Resume Next statement could have been used to continue program flow, ignoring the errors. However, if the For Each . . . Next loop contained code that would not generate an error but depended on successful execution of code that could generate an error, it would be safest to completely bypass all the code that is dependent on a single item type.
Although putting error handlers into your code adds to its complexity, it makes your procedures much more robust because they are less likely to fail in case of unexpected problems. The benefits of using an error handler rather than explicitly limiting or testing the item types is that the error-handler code executes only when a problem is encountered, not every time the procedure is run. In cases where unexpected item types will be rare, responding to the error instead of trying to prevent it is actually more efficient.
The Items collection in Outlook is not difficult to work with, as long as you keep certain principles in mind.
The most important thing to remember when working with an Items collection is that each time the Items collection or Item property is accessed, a new instance is created of the collection or item. Generally, your code will behave more predictably if you assign an object variable to refer to the collection or item before attempting to use it.
Another point to remember is that the collection is not static, but dynamic, especially when your code changes the contents of the collection. Your code must be prepared to deal with changes in the collection, especially changes that affect the number of items in the collection. The safest approach is to retrieve a new instance of the collection after changing it in any significant way.
Finally, you should never take for granted that a given Items collection will contain only one type of item. Whether you use error handlers or some sort of filtering technique, your code should be prepared to deal with heterogeneous collections.
If you keep these principles in mind, you should find the Items collection to be no more difficult to use than other collections.