Chapter 10

Building a MAPI-Enabled Fourm Tool


CONTENTS


In this chapter you'll learn how to use MAPI services to create a threaded discussion client. Threaded discussions clients are used quite frequently in groupware settings and on the Internet. The biggest difference between a discussion tool and an e-mail client is that the e-mail client is designed primarily for conducting one-to-one conversations. The discussion tool, however, is designed to support multiple parties, all participating in the same discussion. Even though MAPI services were originally deployed as only an e-mail solution, Microsoft has added several features to MAPI that now make it an excellent platform for building discussion applications.

Before getting into the details of building the tool, you'll learn a bit more about the differences between e-mail and discussion communications. You'll also learn how to take advantage of little-known MAPI properties and new features of the OLE Messaging library that make it easy to put together a standalone discussion tool that can be deployed from within a Local Area Network, over a Wide Area Network, or even across the Internet. You'll learn

As an added bonus, after you've completed the programming example in this chapter, you'll have a set of library functions that allow you to easily add threaded discussion capabilities to new or existing Visual Basic applications.

Discussion Groups versus E-Mail

By now, most of us have used electronic discussion forums at least once in our lives. And there are many who use them every single day. The advantage of a discussion forum over direct person-to-person e-mail is that you can communicate with a large number of people without generating a large number of messages. In fact, reduced traffic is one of the biggest reasons to consider employing discussion groups as part of your electronic message system.

There are a number of other advantages to using discussion groups instead of e-mail. Often, it is easier to find answers to technical problems if you can go to a place where all the "techies" hang out. Discussion forums can work the same way. Instead of trying to e-mail several people in attempts to solve your problem, you can often find a discussion forum where you can send a single request that will reach hundreds (possibly thousands) of people who may be able to help you. So discussion forums can increase your ability to get your problems solved sooner.

Another good way to highlight the differences between e-mail and discussion groups is by comparing several face-to-face meetings (e-mail messages) with a single staff meeting where everyone shows up at the same place at the same time (the discussion forum). Using forum communications can reduce the amount of time you need to spend communicating. You can say it once and reach lots of people instead of having to repeat your message in lots of single e-mail messages.

Companies use forums as a way to communicate information of general interest as well as a way to take advantage of specialized expertise within an organization. Where corporate e-mail can lead to isolated individuals communicating in a vacuum, discussion groups can foster increased interaction with others and a sense of belonging to a special group-even if one or more of the team members is half-way around the world.

Now that you have a general idea of what discussion groups are and how you can use them in your organization, you're ready to review the three key concepts that make discussion systems different from e-mail systems.

Folders as Destinations

First, and most important, users participating in an online discussion forum do not send messages to individuals. Instead they send their messaging to a single location where all messages are stored. In the MAPI model, the storage location is called a folder. When you create MAPI-based forum tools, you'll select a folder (or folders) to hold all the correspondence from others participating in the forum.

Note
There are times when forum participants will initiate private e-mail correspondence in order to cover a topic in more depth or to delve deeper into an aspect of the discussion that would not be interesting to most of the other members.

Then, after each message has been composed, you just post the message to the discussion folder for others to see. They, if appropriate, respond to your message with their own comments. The key point to remember is that messages are addressed to locations, not people.

Using the ConversationTopic and ConversationIndex Properties

Another key aspect of discussion groups is the ability to track topics in a thread that leads from the beginning of the discussion to the end. As each participant replies to an existing message, a relationship is established between the source message and the reply. Of course, it is perfectly correct (and quite common) to generate a "reply to a reply." This can continue on indefinitely. And in the process an intricate web of ideas and comments all collect in the storage folder that houses the discussion group.

Some discussion tools simply list the messages in the chronological order in which they were received. These are referred to as non-threaded or flat-table discussion tools (see Figure 10.1).

Figure 10.1 : Viewing messages in flat format, chronological order.

Non-threaded tools (often called readers) can be handy, but most people prefer the discussion tools that provide threaded discussion tracking. With threaded tools, each conversation that is initiated is a branch. Each branch can have its own subsequent conversation thread. And those threads can be combined with others, too. Viewing discussion messages as related threads is really handy (see Figure 10.2).

Figure 10.2 : Viewing messages in threaded order.

MAPI uses two special properties of the Message object to keep track of each branch and sub-branch of a threaded discussion. These two properties are the ConversationTopic and the ConversationIndex properties. The ConversationTopic is usually a readable string. This is much like the subject line of a mail message. Conversation topics for a discussion forum for a typical computer firm might be

Note
Some forums have fixed topics and only allow users to select from a predetermined list when posting messages. Other forums allow users to enter free-form text into the topic field.

MAPI uses the ConversationIndex property to keep track of conversation threads within the topic group. This is usually done using some type of numbering scheme. Microsoft recommends that programmers use the same method that is employed by Microsoft Exchange when populating the ConversationIndex field. Microsoft Exchange uses the CoCreateGuid API call to generate a unique number to place in the ConversationIndex field. New messages that respond to an existing message should inherit the ConversationIndex of the previous message and append their own index value to the right. This way, each new branch of the conversation has a longer ConversationIndex. You'll get a better look at this technique when you build the sample Visual Basic project later in this chapter.

Note
The CoCreateGuid API generates a unique value based on the system date (among other things). You can use any method you wish to keep track of conversation threads, but it is highly recommended that you use the method described here. It is quick, effective, and requires very few computing resources.

Update versus Send

The last major concept to deal with when building discussion tools is the use of the MAPI Update method instead of the Send method. In all the examples covered in the book so far, when you complete a new message and want to place it into the MAPI message pool, you use the Send method (or some type of Send API call) to release the message to MAPI for delivery. This works when you have a person to whom you are addressing the message. However, because discussion messages are not addressed to persons but to folders instead, you cannot "send" new messages. Instead you use the Update method to update the contents of a MAPI folder. In fact, attempting to use the Send method to deliver forum messages will result in a MAPI error.

Tip
The key idea here is to think of updating the contents of a MAPI folder as opposed to sending a message to a person.

The actual process of placing a message in a MAPI folder includes adding a blank message to the target folder, setting that message's subject and text body (just like any mail message) and then setting several other message properties, too. Each of the properties shown in Table 10.1 must be set properly before you invoke the Update method to place a message in a MAPI folder.

Note
You'll get to see the details of creating a forum message when you build the forum tool later in this chapter. The main thing to keep in mind now is that you do not use the Send method to place messages in folders.

So there are the three key points to remember about the differences between e-mail and discussion messages:

Now it's time to use Visual Basic to build the example forum tool.

The Discuss Project

For the rest of the chapter, you'll build a complete discussion tool that can be used to track ongoing messages in a target folder. You'll be able to use the program to read messages, generate replies to existing messages, and start new discussion threads. You'll also be able to select the target folder for the discussion. This way, you can use the same tool to monitor more than one discussion.

Note
The forum tool described in this chapter is a very basic project. Not much time will be spent on the user interface. This is done to keep focus on the general issues of creating a discussion tool for MAPI systems. If you plan to put a version of this project into production, you'll want to add some bells and whistles to make this a more friendly product.

You'll use the OLE Messaging library (OML) to access MAPI services for this project. The OML gives you all the features you need to be able to manipulate message and folder properties within the MAPI file system. Although you can use the MAPI.OCX to read most of the discussion-related properties, you cannot use the OCX to place messages in a target MAPI folder. Also, you cannot use the simple MAPI API declarations to gain access to target folders or to manipulate message properties.

Tip
If you are planning to build any software that manipulates folders, you'll need to use the OLE Messaging library. No other Microsoft programming interface (besides the C MAPI interface) allows you to gain access to the folders collection.

There is one main code module and several forms in the Discuss project you'll build here.

Note
Before you start coding the Discuss project, make sure you have added the OLE Messaging type library. Do this by selecting Tools | References from the main menu of Visual Basic. You will need to install the OLE Messaging library from the MSDN CD-ROM before you can locate it using the "Tools | References" menu option.

The MAPIPost Code Library

The MAPIPost Code Library contains several very important routines. These routines will be used throughout the project to gain access to folder collections and message collections within the folders. This also holds routines for posting new messages and generating replies to existing messages. There are also several routines for performing folder and message searches. You'll be able to use the routines in this module in other MAPI-related projects.

First, start a new Visual Basic project and add a BAS module (Insert | Module). Set its Name property to MAPIPost and save the file as MAPIPOST.BAS. Now add the code shown in Listing 10.1 to the declaration section of the form.


Listing 10.1. Adding the declarations for the module.
Option Explicit

'
' OLE message objects
Public objSession As Object
Public objMsgColl As Object
Public objMsg As Object
Public objRecipColl As Object
Public objRecip As Object
Public objAttachColl As Object
Public objAttach As Object
Public objAddrEntry As Object
Public objUserEntry As Object
Public objFolderColl As Object
Public objFolder As Object
Public objInfoStoreColl As Object
Public objInfoStore As Object
Public gnIndentlevel as Integer
Public cStoreID As String

'
' UDT for store/folder pairs
Type FolderType
    Name As String
    FolderID As String
    StoreID As String
End Type
'
Public FolderRec() As FolderType    ' members
Public iFldrCnt As Integer          ' pointer

'
' UDT for message/conversation pairs
Type MsgType
    Subject As String
    Topic As String
    ConvIndex As String
    MsgID As String
    Date As Date
End Type
'
Public MsgRec() As MsgType          ' members
Public iMsgCnt As Integer           ' pointer

'
' type for creating Exchange-compliant timestamp
Type GUID
    guid1 As Long
    guid2 As Long
Listing 10.1. continued
    guid3 As Long
    guid4 As Long
End Type
'
Declare Function CoCreateGuid Lib "OLE32.DLL" (pGuid As GUID) As Long
Public Const S_OK = 0

You'll notice the usual OLE Messaging library object declarations along with two user-defined types that will be used to keep track of message and folder collections. This will come in quite handy as you'll see later on. There is also the CoCreateGuid API call. You'll use this to generate unique ConversationIndex values for your message threads.

Next you need to add code that starts and ends MAPI services. Listing 10.2 shows the OLEMAPIStart and OLEMAPIEnd routines. Add them to your project.


Listing 10.2. Adding the OLEMAPIStart and OLEMAPIEnd routines.
Public Sub OLEMAPIStart(cUserProfile As String)
    '
    ' start an OLE MAPI session
    '
    On Error Resume Next
    Set objSession = CreateObject("MAPI.Session")
    objSession.Logon profilename:=cUserProfile
    '
    If Err <> 0 Then
        MsgBox "Unable to Start MAPI Services!", vbCritical, "OLEMAPIStart"
        End
    End If
    '
End Sub

Public Sub OLEMAPIEnd()
    '
    On Error Resume Next
    objSession.Logoff ' end mapi session
    '
End Sub

The next routines are used to build a collection of all the folders in all the message stores available to the workstation. You'll remember that MAPI 1.0 allows more than one message store for each workstation. Typically, users will have a personal message store, a server-based collection of messages, and possibly a message store related to an outside messaging service (such as Sprint, CompuServe, and so on). Each one of these stores has its own set of folders, too. The routine in Listing 10.3 shows you how you can enumerate all the folders in all the message stores and build a local user-defined type that you can use to locate and manipulate MAPI folders. Add the code shown in Listing 10.3 to your project.


Listing 10.3. Adding the CollectFolders routine.
Public Sub CollectFolders(Optional ctrl As Variant)
    '
    ' build folder tree
    '
    Dim objRootFolder As Object
    Dim lobjFolders As Object
    Dim nIter As Integer
    Dim nStoreCount as Integer
Dim bLoadCtrl As Boolean
    '
    ' loading optional display?
    If IsMissing(ctrl) Or IsNull(ctrl) Then
        ctrl = Null
        bLoadCtrl = False
    Else
        bLoadCtrl = True
    End If
    '
    gnIndentlevel = 0
    cStoreID = ""
    '
    ' get collection of information stores
    Set objInfoStoreColl = objSession.InfoStores
    nStoreCount = objInfoStoreColl.Count
    '
    ' walk through all stores to get folders
    For nIter = 1 To nStoreCount
        iFldrCnt = iFldrCnt + 1
        ReDim Preserve FolderRec(iFldrCnt)
        FolderRec(iFldrCnt).StoreID = objInfoStoreColl.Item(nIter).ID
        FolderRec(iFldrCnt).FolderID = "" ' no folder for a store!
        FolderRec(iFldrCnt).Name = objInfoStoreColl.Item(nIter).Name
        cStoreID = FolderRec(iFldrCnt).StoreID ' hold this for the other folders
        '
        ' add to display control
        If bLoadCtrl = True Then
            ctrl.AddItem FolderRec(iFldrCnt).Name ' add to display
        End If
        '
        ' point to top of store and start loading
        Set objRootFolder = objInfoStoreColl.Item(nIter).RootFolder
        Set objFolderColl = objRootFolder.Folders
        Set lobjFolders = objFolderColl
        LoadFolder lobjFolders, ctrl
        gnIndentlevel = 0
    Next nIter
    '
End Sub

You'll see that the routine in Listing 10.3 walks through all the attached message stores and calls the LoadFolders routine to actually collect all the folders in a message store. This routine also allows you to pass an option list control (list box, combo box, or outline control). The routine will use this control to build an onscreen pick list of the available folders.

Now add the LoadFolders routine from Listing 10.4 to your project.


Listing 10.4. Adding the LoadFolders routine.
Sub LoadFolder(aFolders As Object, Optional ctrl As Variant)
    '
    ' look for folders in the collection
    '
    Dim mobjFolder As Object
    Dim mFolderCol As Object
    Dim bLoadCtrl As Boolean
    '
    gnIndentlevel = gnIndentlevel + 1
    '
    If IsMissing(ctrl) Or IsNull(ctrl) Then
        bLoadCtrl = False
    Else
        bLoadCtrl = True
    End If
    '
    Set mobjFolder = aFolders.GetFirst
    While Not mobjFolder Is Nothing
        iFldrCnt = iFldrCnt + 1
        ReDim Preserve FolderRec(iFldrCnt)
        '
        FolderRec(iFldrCnt).StoreID = cStoreID
        FolderRec(iFldrCnt).FolderID = mobjFolder.ID
        FolderRec(iFldrCnt).Name = mobjFolder.Name
        '
        ' optionally load screen display
        If bLoadCtrl = True Then
            ctrl.AddItem Space(gnIndentlevel * 5) & FolderRec(iFldrCnt).Name
        End If
        '
        ' look for nested folders
        Set mFolderCol = mobjFolder.Folders
        LoadFolder mFolderCol, ctrl
        '
        ' done with nested folders
        gnIndentlevel = gnIndentlevel - 1
        Set mobjFolder = aFolders.GetNext
    Wend
    '
End Sub

Notice that this routine is called recursively in order to collect all the folders that might be found within a folder. Since MAPI places no restrictions on how many levels of folders may be defined, you need to use a recursive routine to locate all the available folders.

Once you have built the folder collection, you'll need a method for pulling information out of the collection. Listing 10.5 shows a function that will take the friendly name of a folder and return the unique folder ID and store ID. Add this to your project.


Listing 10.5. Adding the GetFolderRec function.
Public Function GetFolderRec(cFolderName As String) As FolderType
    '
    ' take name, return structure
    '
    Dim x As Integer
    Dim y As Integer
    '
    ' start w/ a blank return
    GetFolderRec.StoreID = ""
    GetFolderRec.FolderID = ""
    GetFolderRec.Name = ""
    '
    y = UBound(FolderRec) ' get total recs in array
    '
    ' walk through array
    For x = 0 To y - 1
        If Trim(UCase(FolderRec(x).Name)) = Trim(UCase(cFolderName)) Then
            GetFolderRec = FolderRec(x)
            Exit Function
        End If
    Next x
    '
End Function

Warning
This routine will return the folder record of the first folder with the name you request. Because MAPI allows users to define two folders with the same name, this routine may not always return the results expected. This works fine for most projects, but you should keep it in mind when developing MAPI search tools.

You also need some tools for collecting and accessing all the discussion messages in a folder. Listing 10.6 shows the routine that you can use to collect all messages into a local user-defined type. Add the routine to your project.


Listing 10.6. Adding the OLEMAPIGetMsgs routine.
Public Sub OLEMAPIGetMsgs(cFolderName As String, Optional ctrl As Variant)
    '
    ' get all the discussion messages
    '
    Dim uFolder As FolderType
    ReDim MsgRec(0)
    Dim bLoadCtrl As Boolean
    '
    Set objFolder = Nothing
    Set objMsgColl = Nothing
    Set objMsg = Nothing
    '
    iMsgCnt = 0
    uFolder = GetFolderRec(cFolderName)
    '
    ' check for optional control
    If IsMissing(ctrl) Or IsNull(ctrl) Then
        ctrl = Null
        bLoadCtrl = False
    Else
        bLoadCtrl = True
        ctrl.Clear
    End If
    '
    ' now walk through folder to find discussion msgs
    If uFolder.Name = "" Then
        MsgBox "Unable to locate folder!", vbExclamation, uFolder.Name
        Exit Sub
    End If
    '
    ' open store, folder
    Set objFolder = objSession.GetFolder(uFolder.FolderID, uFolder.StoreID)
    If objFolder Is Nothing Then
        MsgBox "Unable to open folder!", vbExclamation, uFolder.Name
        Exit Sub
    End If
    '
    ' get message collection
    Set objMsgColl = objFolder.Messages
    If objMsgColl Is Nothing Then
        MsgBox "No messages in folder", vbExclamation, uFolder.Name
        Exit Sub
    End If
    '
    ' ok, get first message
    On Error Resume Next
    Set objMsg = objMsgColl.GetFirst
    '
    ' now walk through folder to get all discussion msgs
    Do Until objMsg Is Nothing
        If objMsg.Type = "IPM.Discuss" Then
             iMsgCnt = iMsgCnt + 1
             ReDim Preserve MsgRec(iMsgCnt)
             MsgRec(iMsgCnt).Subject = objMsg.Subject
             MsgRec(iMsgCnt).Topic = objMsg.ConversationTopic
             MsgRec(iMsgCnt).ConvIndex = objMsg.ConversationIndex
             MsgRec(iMsgCnt).MsgID = objMsg.ID
             MsgRec(iMsgCnt).Date = objMsg.TimeReceived
             '
             ' optionally load control
             If bLoadCtrl = True Then
                ctrl.AddItem objMsg.ConversationIndex
             End If
        End If
        Set objMsg = objMsgColl.GetNext
    Loop
    '
    Beep ' tell 'em you're done!
    '
End Sub

You'll notice that this routine allows you to place the messages in a list control for sorting and display. You may also have noticed that the field placed in the controls is the ConversationIndex property. Sorting the messages on the ConversationIndex property will automatically give you the threaded list you need. You can then take the sorted list from the list control and use that to generate an onscreen display of the subject or other properties of the message-all in threaded order!

You'll need three different routines to access messages from the user-defined array. First, you need a routine that allows you to pass in a pointer to the sorted list and that returns the ConversationIndex of a message. Listing 10.7 shows how this is done.


Listing 10.7. Adding the MsgIndex function.
Public Function MsgIndex(ctrl As Control, iPtr As Integer) As String
    '
    ' accept pointer to sorted list control
    ' and the sorted list control
    ' return msg convIndex property
    '
    MsgIndex = ctrl.List(iPtr)
    '
End Function

Next you need a routine that takes the conversation index and returns the complete internal message structure. Listing 10.8 shows you how to do this step.


Listing 10.8. Adding the MsgPtr function.
Public Function MsgPtr(cIndex As String) As MsgType
    '
    ' accepts conversation index, returns msg type
    '
    Dim x As Integer
    Dim y As Integer
    '
    y = UBound(MsgRec)
    '
    For x = 1 To y
        If MsgRec(x).ConvIndex = cIndex Then
            MsgPtr = MsgRec(x)
            Exit Function
        End If
    Next x
    '
End Function

Finally, you can also use a function that returns the Message user-defined type based on the direct pointer. Add the code from listing 10.9 to your project.


Listing 10.9. Adding the GetMsgRec function.
Public Function GetMsgRec(iPointer As Integer) As MsgType
    '
    ' accept pointer, return strucutre
    '

    '
    ' start w blank records
    GetMsgRec.MsgID = ""
    GetMsgRec.ConvIndex = ""
    GetMsgRec.Subject = ""
    GetMsgRec.Topic = ""

    '
    ' now try to find it
    If iPointer < 0 Or iPointer > UBound(MsgRec) Then
        MsgBox "Invalid Message pointer!", vbExclamation, "GetMsgRec"
        Exit Function
    Else
        GetMsgRec = MsgRec(iPointer + 1)
    End If
    '
End Function

While you're coding the message routines, add the FillOutline subroutine shown in Listing 10.10. This routine loads an outline control from the sorted list. The outline can then be displayed to the user.


Listing 10.10. Adding the FillOutline routine.
Public Sub FillOutline(ctrlIn As Control, ctrlOut As Control)
    '
    ' accept a sorted list box as input
    ' copy the recs to an outline w/ indents
    '
    Dim x As Integer
    Dim uMsg As MsgType
    '
    ctrlOut.Clear ' throw all the old stuff out
    '
    ' load the outline in sorted order
    For x = 0 To ctrlIn.ListCount - 1
        uMsg = MsgPtr(ctrlIn.List(x))
        ctrlOut.AddItem uMsg.Subject & " (" & Format(uMsg.Date, "general date") &                      Â")"
        If bThreaded = True Then
            ctrlOut.Indent(x) = Len(ctrlIn.List(x)) / 16
        End If
    Next x
    '
    ' expand all nodes for viewing
    For x = 0 To ctrlOut.ListCount - 1
        If ctrlOut.HasSubItems(x) = True Then
            ctrlOut.Expand(x) = True
        End If
    Next x
    '
End Sub

The FillOutline routine also makes sure threaded messages are indented properly and expands the entire message tree for users to see the various branches.

One more handy routine is the MakeTimeStamp function. This will be used to generate the ConversationIndex values. Add the code from Listing 10.11 to your project.


Listing 10.11. Adding the MakeTimeStamp routine.
Public Function MakeTimeStamp() As String
    '
    ' create Exchange-type time stamp
    '
    Dim lResult As Long
    Dim lGuid As GUID
    '
    On Error GoTo LocalErr
    '
    lResult = CoCreateGuid(lGuid)
    If lResult = S_OK Then
        MakeTimeStamp = Hex$(lGuid.guid1) & Hex$(lGuid.guid2)
    Else
        MakeTimeStamp = "00000000"   ' zeroes
    End If
    Exit Function

LocalErr:
    MsgBox "Error " & Str(Err) & ": " & Error$(Err)
    MakeTimeStamp = "00000000"
    Exit Function
    '
End Function

Only two routines remain: OLEMAPIPostMsg and OLEMAPIReplyMsg. The OLEMAPIPostMsg routine builds and posts a new message thread to the target folder. Add the code from Listing 10.12 to your project.


Listing 10.12. Adding the OLEMAPIPostMsg routine.
Public Sub OLEMAPIPostMsg(cFolderName As String, cTopic As String, cSubject As String, cBody As String)
    '
    ' post a message to the folder (starts a new thread)
    '
    ' --------------
    ' Inputs:
    '   cFolderName     - name of target folder
    '   cTopic          - general discussion topic
    '   cSubject        - user's subject line
    '   cBody           - user's body text
    ' --------------
    '
    Dim uFolder As FolderType
    '
    ' get folder structure
    uFolder = GetFolderRec(cFolderName) ' get the structure
    If uFolder.FolderID = "" Then
        MsgBox "Unable to Locate Folder!", vbExclamation, cFolderName
        Exit Sub
    End If
    '
    ' open folder, store
    Set objFolder = objSession.GetFolder(uFolder.FolderID, uFolder.StoreID)
    If objFolder Is Nothing Then
        MsgBox "Unable to open folder!", vbExclamation, uFolder.Name
        Exit Sub
    End If
    '
    ' create new message
    Set objMsg = objFolder.Messages.Add
    If objMsg Is Nothing Then
        MsgBox "Unable to create new message in folder!", vbExclamation, ÂuFolder.Name
        Exit Sub
    End If
    '
    ' fix up subject/topic
    If cTopic = "" And cSubject = "" Then
        cTopic = "Thread #" & Format(Now(), "YYMMDDHHmm")
    End If
    '
    If cTopic = "" And cSubject <> "" Then
        cTopic = cSubject
    End If
    '
    If cSubject = "" And cTopic <> "" Then
        cSubject = cTopic
    End If
    '
    ' now compose the message
    With objMsg
        '
        ' set user-supplied info
        .Subject = cSubject
        .Text = cBody
        '
        ' set stock properties
        .Type = "IPM.Discuss"
        .ConversationTopic = cTopic
        .ConversationIndex = MakeTimeStamp
        .TimeSent = Now()
        .TimeReceived = .TimeSent
        .Submitted = True
        .Unread = True
        .Sent = True
        '
        .Update ' force msg into the folder
    End With
    '
    objFolder.Update ' update the folder object
    '
End Sub

There are quite a few things going on in this routine. First, it locates the folder and message store where the message will be posted. Then a new message object is created, populated with the appropriate values, and posted (using the Update method) to the target folder.

The OLEMAPIReplyMsg is quite similar, but this method carries information forward from the source message to make sure that the conversation thread is maintained. Add the code from Listing 10.13 to your project.


Listing 10.13. Adding the OLEMAPIReplyMsg routine.
Public Sub OLEMAPIReply(FolderName As String, iCount As Integer, cSubject As ÂString, cBody As String)
    '
    ' post a message reply (continues existing thread)
    '
    ' -----------------
    ' Inputs:
    '   FolderName  - string name of target folder
    '   iCount      - index into msg array (points to source msg)
    '   cSubject    - user's subject line (if null uses "RE:" & source subject)
    '   cBody       - user's msg body
    ' -----------------
    '
    Dim cIndex As String
    Dim uMsg As MsgType
    Dim uFolder As FolderType
    Dim objSourceMsg As Object
    '
    ' check msg pointer
    If iCount < 0 Then
        MsgBox "No Message Selected", vbExclamation, "OLEMAPIReply"
        Exit Sub
    End If
    '
    ' get folder for posting
    uFolder = GetFolderRec(FolderName)
    If uFolder.FolderID = "" Then
        MsgBox "Unable to locate folder!", vbExclamation, FolderName
        Exit Sub
    End If
    '
    ' get source message
    cIndex = MsgIndex(frmMsgs.list1, iCount)
    uMsg = MsgPtr(cIndex)
    Set objSourceMsg = objSession.GetMessage(uMsg.MsgID, uFolder.StoreID)
    If objSourceMsg Is Nothing Then
        MsgBox "Unable to Load selected Message!", vbExclamation, uMsg.Subject
        Exit Sub
    End If
    '
    ' open target store, folder
    Set objFolder = objSession.GetFolder(uFolder.FolderID, uFolder.StoreID)
    If objFolder Is Nothing Then
        MsgBox "Unable to open target folder", vbExclamation, uFolder.Name
        Exit Sub
    End If
    '
    ' create a new blank message object
    Set objMsg = objFolder.Messages.Add
    If objMsg Is Nothing Then
        MsgBox "Unable to add message to folder", vbExclamation, objFolder.Name
        Exit Sub
    End If
    '
    ' fix up target subject, if needed
    If cSubject = "" Then
        cSubject = "RE: " & objSourceMsg.Subject
    End If
    '
    ' now compose reply msg
    With objMsg
        '
        ' user properties
        .Subject = cSubject
        .Text = cBody
        '
        ' stock properties
        .Type = "IPM.Discuss"
        .ConversationTopic = objSourceMsg.ConversationTopic
        .ConversationIndex = objSourceMsg.ConversationIndex & MakeTimeStamp
        .TimeSent = Now()
        .TimeReceived = .TimeSent
        .Submitted = True
        .Unread = True
        .Sent = True
        '
        .Update ' force msg into folder
    End With
    '
    objFolder.Update ' update folder object
    '
End Sub

That's all there is to this module. Save the module (MAPIPOST.BAS) and the project (DISCUSS.VBP) before moving on to the next section.

The Discuss and Msgs Forms

The two main forms for the Discuss project are the MDI Discuss form and the Msgs form. The MDI form presents a button array and hosts all the other forms. The Msgs form is used to display the threaded discussion list.

You'll also need to add a few values to a short BAS module. These are project-level values that are used throughout the project. Add a BAS module, set its Name property to ModDiscuss and save it as MODDISCUSS.BAS. Now add the code shown in Listing 10.14 to the general declaration section of the form.


Listing 10.14. Adding project-level declarations.
Option Explicit

'
' constants
Public Const dscRead = 0
Public Const dscNewPost = 1
Public Const dscReply = 2

'
' variables
Public cGroup As String
Public cProfile As String
Public bThreaded As Boolean

That's all you need to add to this form. Save it (MODDISCUSS.BAS) and close it now.

Laying Out the Discuss Form

The Discuss form is the MDI form that controls the entire project. Refer to Figure 10.3 and Table 10.2 when laying out the Discuss form.

Figure 10.3 : Laying out the Discuss form.

Table 10.2. Controls for the Discuss form.
ControlProperty Setting
VB.MDIForm Name mdiDiscuss
 BackColor &H8000000C&
 Caption "Discuss"
 ClientHeight 5685
 ClientLeft 735
 ClientTop 1710
 ClientWidth 9210
 Height 6090
 Left 675
 LinkTopic "MDIForm1"
 Top 1365
 Width 9330
 WindowState 2 'Maximized
VB.PictureBox Name Picture2
 Align 2 'Align Bottom
 Height 345
 Left 0
 ScaleHeight 285
 ScaleWidth 9150
 TabIndex 8
 Top 5340
 Width 9210
VB.Label Name lblStatus
 Caption "Label1"
 Height 315
 Left 60
 TabIndex 9
 Top 0
 Width 9135
VB.PictureBox Name Picture1
 Align 1 'Align Top
 Height 495
 Left 0
 ScaleHeight 435
 ScaleWidth 9150
 TabIndex 0
 Top 0
 Width 9210
VB.CommandButton Name cmdMain
 Caption "&Close"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 6
 Left 7680
 TabIndex 7
 Top 60
 Width 1200
VB.CommandButton Name cmdMain
 Caption "&About"
 Font  
  name= "MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 5
 Left 6420
 TabIndex 6
 Top 60
 Width 1200
VB.CommandButton Name cmdMain
 Caption "&Options"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 4
 Left 5160
 TabIndex 5
 Top 60
 Width 1200
VB.CommandButton Name cmdMain
 Caption "&Load Msgs"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 3
 Left 3840
 TabIndex 4
 Top 60
 Width 1200
VB.CommandButton Name cmdMain
 Caption "&Reply"
 Font
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 2
 Left 2580
 TabIndex 3
 Top 60
 Width 1200
VB.CommandButton Name cmdMain
 Caption "&New Post"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 1
 Left 1320
 TabIndex 2
 Top 60
 Width 1200
VB.CommandButton Name cmdMain
 Caption "R&ead"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 0
 Left 60
 TabIndex 1
 Top 60
 Width 1200

After you complete the form layout, save the form as MDIDISCUSS.FRM before you begin coding.

Coding the Discuss Form

Most of the code in the MDI Discuss form is needed to react to user buttons or basic form events. There are also two custom routines used for reading existing messages and creating new ones.

Listing 10.15 shows the MDIForm_Load event. Add this to your project.


Listing 10.15. Coding the MDIForm_Load event.
Private Sub MDIForm_Load()
    '
    ' startup
    '
    cGroup = GetSetting(App.EXEName, "Options", "Group", "CDG")
    cProfile = GetSetting(App.EXEName, "Options", "Profile", "")
    bThreaded = GetSetting(App.EXEName, "Options", "Threaded", True)

    '
    lblStatus = "Logging into Discussion Group [" & cGroup & "]..."
    '
    OLEMAPIStart cProfile
    CollectFolders
    OLEMAPIGetMsgs cGroup, frmMsgs.list1
    FillOutline frmMsgs.list1, frmMsgs.Outline1
    frmMsgs.Show
    lblStatus = ""
    '
End Sub

Listing 10.16 shows the code for the Activate and Resize events of the form. Add these two routines to your project.


Listing 10.16. Adding the MDIForm_Activate and MDIForm_Resize events.
Private Sub MDIForm_Activate()
    '
    Me.Caption = "Discuss [" & cGroup & "]"
    '
End Sub

Private Sub MDIForm_Resize()
    '
    ' adjust buttons on form
    '
    Dim iFactor As Integer
    Dim iPosition As Integer
    Dim x As Integer
    '
    iFactor = Me.Width / 7 ' # of buttons
    If iFactor < 1200 Then iFactor = 1200
    '
    For x = 0 To 6
        cmdMain(x).Width = iFactor * 0.9
        cmdMain(x).Left = iFactor * x + 60
    Next x
    '
End Sub

Finally, add the code from Listing 10.17 to the MDIForm_Unload event.


Listing 10.17. Adding the MDIForm_Unload event code.
Private Sub MDIForm_Unload(Cancel As Integer)
    '
    ' cleanup before leaving
    '

    '
    ' write to registry
    SaveSetting App.EXEName, "Options", "Profile", cProfile
    SaveSetting App.EXEName, "Options", "Group", cGroup
    SaveSetting App.EXEName, "Options", "Threaded", bThreaded


    '
    'drop all loaded forms
    '
    Dim x As Integer
    Dim y As Integer
    '
    x = Forms.Count - 1
    For y = x To 0 Step -1
       Unload Forms(y)
    Next
    End
    '
End Sub

Next, you need to add code to the cmdMain_Click event to handle user selections from the button array. Listing 10.18 shows the code to handle this.


Listing 10.18. Coding the cmdMain_Click event.
Private Sub cmdMain_Click(Index As Integer)
    '
    ' handle main user selections
    '
    Dim uFolder As FolderType
    Dim uMsgRec As MsgType
    Dim objMsg As Object
    '
    Select Case Index
        Case 0 ' read
            LoadMsgRec dscRead
        Case 1 ' new post
            NewMsgRec
        Case 2 ' reply
            LoadMsgRec dscReply
        Case 3 ' load msgs
            OLEMAPIGetMsgs cGroup, frmMsgs.list1
            FillOutline frmMsgs.list1, frmMsgs.Outline1
        Case 4 ' options
            frmOptions.Show
        Case 5 ' about
            frmAbout.Show
        Case 6 ' close
            Unload Me
    End Select
    '
End Sub

You can see that the cmdMain routine calls two other routines: LoadMsgRec and NewMsgRec. You need to add these routines to your project. First, add the NewMsgRec subroutine to the form. Add the code from Listing 10.19.


Listing 10.19. Adding the NerwMsgRec routine.
Public Sub NewMsgRec()
    '
    ' start a new posting
    '
    Load frmNote
    frmNote.txtSubject = "Msg #" & Format(Now(), "YYMMDDHHmm")
    frmNote.txtTopic = frmNote.txtSubject
    frmNote.txtBody = ""
    frmNote.lblMode = dscNewPost
    frmNote.Show
    '
End Sub

The code in Listing 10.19 initializes values on the note form and then calls the form for the user.

The code for the LoadMsgRec routine is a bit more complicated. This routine must first locate the selected message, load it into the form, and then call the note form. Add the code in Listing 10.20 to your form.


Listing 10.20. Adding the LoadMsgRec routine.
Public Sub LoadMsgRec(iMode As Integer)
    '
    ' read a message record
    '
    Dim uFolder As FolderType
    Dim uMsgRec As MsgType
    Dim cIndex As String
    '
    ' first get folder type
    uFolder = GetFolderRec(cGroup)
    If uFolder.FolderID = "" Then
        MsgBox "Unable to locate Folder", vbExclamation, "ReadMsgRec"
        Exit Sub
    End If
    '
    ' get message type
    cIndex = MsgIndex(frmMsgs.list1, frmMsgs.Outline1.ListIndex)
    uMsgRec = MsgPtr(cIndex)
    If uMsgRec.MsgID = "" Then
        MsgBox "Unable to locate Message", vbExclamation, "ReadMsgRec"
        Exit Sub
    End If
    '
    ' now load real msg from MAPI
    Set objMsg = objSession.GetMessage(uMsgRec.MsgID, uFolder.StoreID)
    If objMsg Is Nothing Then
        MsgBox "Unable to Load Message.", vbExclamation, "ReadMsgRec"
        Exit Sub
    End If
    '
    ' must be good, stuff the form
    Load frmNote
    '
    frmNote.txtTopic = objMsg.ConversationTopic
    If iMode = dscReply Then
        frmNote.txtSubject = "RE: " & objMsg.Subject
    Else
        frmNote.txtSubject = objMsg.Subject
    End If
    frmNote.txtBody = objMsg.Text
    '
    ' mark it for read/reply and show it
    frmNote.lblMode = iMode
    frmNote.lblMsgNbr = frmMsgs.Outline1.ListIndex
    frmNote.Show
    '
End Sub

That's all for the MDI Discuss form. Save it as MDIDISCUSS.FRM, and save the project as DISCUSS.VBP before you continue.

Laying Out and Coding the Msgs Form

The Msgs form is used to show the threaded discussion list. Refer to Figure 10.4 and Table 10.3 for details on laying out the form.

Figure 10.4 : Laying out the Msgs form.

Table 10.3. Controls for the Msgs form.
ControlPropertySetting
VB.Form Name frmMsgs
 Caption "Form1"
 ClientHeight 2955
 ClientLeft 1140
 ClientTop 1515
 ClientWidth 5175
 Height 3360
 Left 1080
 LinkTopic "Form1"
 MDIChild -1 'True
 ScaleHeight 2955
 ScaleWidth 5175
 Top 1170
 Width 5295
VB.ListBox Name List1
 Height 840
 Left 3060
 Sorted -1 'True
 TabIndex 1
 Top 1200
 Visible 0 'False
 Width 1095
MSOutl.Outline Name Outline1
 Height 2715
 Left 120
 TabIndex 0
 Top 60
 Width 4935
 Font  
  Name="MS Sans Serif"
  Size=8.25
  Charset=0
  Weight=700
  Underline=0 'False
  Italic=0 'False
  Strikethrough=0 'False

Note
You'll notice that the form contains both an outline control and a list box control. The list box control is not visible at run time. It is used to automatically sort the messages by conversation index.

There is very little code to the form. Listing 10.21 shows all the code you need to add to the Msgs form.


Listing 10.21. Coding the Msgs form.
Private Sub Form_Load()
    '
    Me.Left = 0
    Me.Top = 0
    '
    Me.Caption = "Discussion Message List"
    '
End Sub

Private Sub Form_Resize()
    '
    ' expand outline control to fill form
    '
    If Me.WindowState <> vbMinimized Then
        Outline1.Left = Me.ScaleLeft
        Outline1.Top = Me.ScaleTop
        Outline1.Width = Me.ScaleWidth
        Outline1.Height = Me.ScaleHeight
    End If
End Sub

Private Sub Outline1_DblClick()
    '
    mdiDiscuss.LoadMsgRec dscRead
    '
End Sub

Save this form as DSCMSGS.FRM and update the project before going to the next step.

Building the Other Forms

There are three other forms you need to add to the project. The Note form will be used to read and reply to messages; the Options form allows the user to set and store some program options; and the About dialog box contains typical program information.

The Note Form

Add a new form to the project and set its Name property to frmNote. Refer to Table 10.4 and Figure 10.5 in laying out the note form.

Figure 10.5 : Laying out the Note form.

Table 10.4. Controls for the Note form.
ControlProperty Setting
VB.Form NamefrmNote
 Caption "Form2"
 ClientHeight 4380
 ClientLeft 1080
 ClientTop 1995
 ClientWidth 6255
 Height 4785
 Left 1020
 LinkTopic "Form2"
 MDIChild -1 'True
 ScaleHeight 4380
 ScaleWidth 6255
 Top 1650
 Width 6375
VB.CommandButton Name cmdBtn
 Caption "Cancel"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 1
 Left 3600
 TabIndex 8
 Top 3840
 Width 1200
VB.CommandButton Name cmdBtn
 Caption "OK"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 0
 Left 4920
 TabIndex 5
 Top 3840
 Width 1200
VB.TextBox Name txtTopic
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 1320
 TabIndex 2
 Text "Text3"
 Top 120
 Width 4800
VB.TextBox Name txtSubject
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 1320
 TabIndex 1
 Text "Text2"
 Top 480
 Width 4800
VB.TextBox Name txtBody
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 2835
 Left 120
 MultiLine -1 'True
 ScrollBars 2 'Vertical
 TabIndex 0
 Text "dscNote.frx":0000
 Top 840
 Width 6000
VB.Label Name lblMsgNbr
 Caption "Label6"
 Height 255
 Left 420
 TabIndex 7
 Top 4020
 Visible 0 'False
 Width 1275
VB.Label Name lblMode
 Caption "Label5"
 Height 255
 Left 1200
 TabIndex 6
 Top 3840
 Visible 0 'False
 Width 2055
VB.Label Name Label4
 Caption "Subject:"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 120
 TabIndex 4
 Top 480
 Width 1200
VB.Label Name Label3
 Caption "Topic:"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
VB.Form NamefrmNote
  strikethrough=0 'False
 Height 300
 Left 120
 TabIndex 3
 Top 120
 Width 1200

After laying out the form, save it (DSCNOTE.FRM) before you add the code.

Along with the typical form-related events, you need to add code to handle user button selections and a custom routine to establish the mode of the form (read, reply, or new message).

First, Listing 10.22 shows all the code for the Form_Load, Form_Activate, and Form_Resize events. Add this to your project.


Listing 10.22. Adding the Form_Load, Form_Activate, and Form_Resize code.
Private Sub Form_Load()
    '
    ' init on first entry
    '
    Label1 = "" ' from
    Text1 = "" ' topic
    Text2 = "" ' subject
    Text3 = "" ' body
    '
End Sub

Private Sub Form_Activate()
    '
    StatusUpdate
    '
End Sub


Private Sub Form_Resize()
    '
    ' adjust controls for form
    '
    If Me.WindowState <> vbMinimized Then
        txtBody.Left = Me.ScaleLeft
        txtBody.Top = 900
        txtBody.Width = Me.ScaleWidth
        txtBody.Height = Me.ScaleHeight - (1320)
        '
        txtTopic.Width = Me.ScaleWidth - 1320
        txtSubject.Width = Me.ScaleWidth - 1320
        '
        cmdBtn(0).Left = Me.ScaleWidth - 1440
        cmdBtn(1).Left = Me.ScaleWidth - 2880
        cmdBtn(1).Top = Me.ScaleHeight - 420
        cmdBtn(0).Top = Me.ScaleHeight - 420
    End If
End Sub

Worth mentioning here is the Form_Resize code. This code will adjust controls on the form to fill out as much (or as little) screen area as is allowed.

Another routine that deals with form controls is the StatusUpdate routine (Listing 10.23). This routine toggles the various controls based on the mode of the form (read, reply, or new message). Add the code from Listing 10.23 to your form.


Listing 10.23. Adding the StatusUpdate routine.
Public Sub StatusUpdate()
    '
    ' check form status
    '
    Select Case lblMode
        Case Is = dscRead
            txtTopic.Enabled = False
            txtSubject.Enabled = False
            txtBody.Enabled = False
            cmdBtn(0).Caption = "Reply"
            cmdBtn(0).SetFocus
            Me.Caption = "Discussion Read Form [" & txtSubject & "]"
        Case Is = dscReply
            txtTopic.Enabled = False
            txtSubject.Enabled = True
            txtBody.Enabled = True
            cmdBtn(0).Caption = "Post"
            txtSubject.SetFocus
            Me.Caption = "Discussion Reply Form [" & txtSubject & "]"
        Case Is = dscNewPost
            txtTopic.Enabled = True
            txtSubject.Enabled = True
            txtBody.Enabled = True
            cmdBtn(0).Caption = "Post"
            txtTopic.SetFocus
            Me.Caption = "Discussion Post New Topic Form"
    End Select
    '
End Sub

The last routine you need to add to the form is the code for the cmdBtn_Click event. This also uses the mode of the form to determine just what the program will do when the user presses the OK button. Add the code from Listing 10.24 to your form.


Listing 10.24. Adding the cmdBtn_Click event code.
Private Sub cmdBtn_Click(Index As Integer)
    '
    ' handle use selections
    '
    Select Case Index
        Case 0 ' new post
            Select Case lblMode
                Case Is = dscNewPost
                    OLEMAPIPostMsg cGroup, txtTopic, txtSubject, txtBody
                    MsgBox "Message has been posted!", vbInformation, "Discuss OLE"
                    OLEMAPIGetMsgs cGroup, frmMsgs.list1
                    FillOutline frmMsgs.list1, frmMsgs.Outline1
                    Unload Me
                Case Is = dscReply
                    OLEMAPIReply cGroup, Val(lblMsgNbr), txtSubject, txtBody
                    MsgBox "Reply has been sent!", vbInformation
                    OLEMAPIGetMsgs cGroup, frmMsgs.list1
                    FillOutline frmMsgs.list1, frmMsgs.Outline1
                    Unload Me
                Case Is = dscRead
                    lblMode = dscReply
                    txtSubject = "RE: " & txtSubject
                    txtBody = String(45, 45) & Chr(13) & Chr(10) & txtBody & Chr(13) & Chr(10) & String(45, 45) & Chr(13) & Chr(10)
                    Form_Activate
            End Select
        Case 1 ' close
            Unload Me
    End Select
    '
End Sub

After you complete the code, save the form (DSCNOTE.FRM) and project (DISCUSS.VBP) before you go to the next section.

The Options Form

The Options form allows the user to select the folder for the discussion group, enter a MAPI profile, and turn the message display from flat to threaded. Refer to Figure 10.6 and Table 10.5 for laying out the Options form.

Figure 10.6 : Laying out the Options form.

Table 10.5. Controls for the Options form.
ControlProperty Setting
VB.Form Name frmOptions
 BorderStyle 3 'Fixed Dialog
 Caption "Form1"
 ClientHeight 3270
 ClientLeft 2370
 ClientTop 2670
 ClientWidth 5250
 Height 3675
 Left 2310
 LinkTopic "Form1"
 MaxButton 0 'False
 MDIChild -1 'True
 MinButton 0 'False
 ScaleHeight 3270
 ScaleWidth 5250
 ShowInTaskbar 0 'False
 Top 2325
 Width 5370
VB.CheckBox Name chkThreaded
 Caption "View Message Threads"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 435
 Left 2700
 TabIndex 7
 Top 900
 Width 2355
VB.CommandButton Name cmdBtn
 Caption "OK"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 1
 Left 3840
 TabIndex 5
 Top 2340
 Width 1200
VB.CommandButton Name cmdBtn
 Caption "Cancel"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Index 0
 Left 3840
 TabIndex 4
 Top 2700
 Width 1200
VB.TextBox Name txtProfile
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 2700
 TabIndex 3
 Top 480
 Width 2400
VB.ListBox Name lstGroup
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 2205
 Left 120
 TabIndex 1
 Top 840
 Width 2400
VB.Label Name lblSelected
 BorderStyle 1 'Fixed Single
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 120
 TabIndex 6
 Top 480
 Width 2400
VB.Label Name lblProfile
 BorderStyle 1 'Fixed Single
 Caption "Enter Login Profile:"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 2700
 TabIndex 2
 Top 120
 Width 2400
VB.Label Name lblGroup
BorderStyle 1 'Fixed Single
Caption "Select Discussion Group:"
Font  
 name="MS Sans Serif"
 charset=0
 weight=700
 size=8.25
 underline=0 'False
 italic=0 'False
 strikethrough=0 'False
Height 300
Left 120
TabIndex 0
Top 120
Width 2400

It's a good idea to save this form (DSCOPTIONS.FRM) before you start coding.

There is not much code for the Options form. Listing 10.25 shows the code for the Form_Load event.


Listing 10.25. Coding the Form_Load event.
Private Sub Form_Load()
    '
    ' set up form
    '
    Me.Caption = "Discuss Options"
    txtProfile = cProfile
    lblSelected = cGroup
    chkThreaded.Value = IIf(bThreaded = 1, 1, 0)

    CollectFolders lstGroup
    '
    Me.Left = (Screen.Width - Me.Width) / 2
    Me.Top = (Screen.Height - Me.Height) / 2
    '
End Sub

The next code to add is for the cmdBtn_Click event. This routine will save the user's choices in the system registry for later recall. Add the code from Listing 10.26 to your form.


Listing 10.26. Adding the cmdBtn_Click code.
Private Sub cmdBtn_Click(Index As Integer)
    '
    ' handle user selection
    '
    Select Case Index
        Case 0 ' cancel
            ' no action
        Case 1 ' OK
            '
            MousePointer = vbHourglass
            '
            ' save to vars
            cGroup = Trim(lblSelected)
            cProfile = Trim(txtProfile)
            bThreaded = IIf(chkThreaded = 1, True, False)

            '
            ' save to registry
            SaveSetting App.EXEName, "Options", "Profile", Trim(cProfile)
            SaveSetting App.EXEName, "Options", "Group", Trim(cGroup)
            SaveSetting App.EXEName, "Options", "Threaded", IIf(chkThreaded.Value =     Â1, 1, 0)
            '
            ' update list & main form
            OLEMAPIGetMsgs cGroup, frmMsgs.list1
            FillOutline frmMsgs.list1, frmMsgs.Outline1
            mdiDiscuss.Caption = "Discuss [" & cGroup & "]"
            '
            MousePointer = vbNormal
            '
    End Select
    '
    Unload Me
    '
End Sub

Finally, you need to add one line to the lstGroup_DblClick event:

Private Sub lstGroup_DblClick()
    '
    lblSelected = Trim(lstGroup.List(lstGroup.ListIndex))
    '
End Sub

Now save the form (DSCOPTIONS.FRM) and update the project. Only one more form to go!

The About Dialog Box

The About dialog box contains basic information about the project. Refer to Figure 10.7 and Table 10.6 for laying out the form.

Figure 10.7 : Laying out the About dialog box.

Table 10.6. Controls for the About dialog box.
ControlProperty Setting
VB.Form Name frmAbout
 BorderStyle 1 'Fixed Single
 Caption "Form1"
 ClientHeight 1785
 ClientLeft 2820
 ClientTop 3165
 ClientWidth 4335
 Height 2190
 Left 2760
 LinkTopic "Form1"
 MaxButton 0 'False
 MDIChild -1 'True
 MinButton 0 'False
 ScaleHeight 1785
 ScaleWidth 4335
 Top 2820
 Width 4455
VB.CommandButton Name cmdOK
 Caption "OK"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 300
 Left 3000
 TabIndex 0
 Top 1380
 Width 1200
VB.Image Name Image1
Height 1215
Left 120
Picture "dscAbout.frx":0000
Stretch -1 'True
Top 60
Width 1275
VB.Label Name Label1
 Caption "Label1"
 Font  
  name="MS Sans Serif"
  charset=0
  weight=700
  size=8.25
  underline=0 'False
  italic=0 'False
  strikethrough=0 'False
 Height 1095
 Left 1560
 TabIndex 1
 Top 120
 Width 2655

You only need to add code to the Form_Load event and the cmdOK_click event. Listing 10.27 shows all the code for the About form.


Listing 10.27. Coding the Form_Load and cmdOK_Click events.
Private Sub cmdOK_Click()
    Unload Me
End Sub

Private Sub Form_Load()
    '
    ' set up about box
    '
    Me.Caption = "About " & App.ProductName
    '
    Image1.Stretch = True
    '
    Label1.Caption = App.FileDescription & Chr(13) & Chr(13)
    Label1 = Label1 & App.LegalCopyright & Chr(13) & Chr(13)
    '
    Me.Left = (Screen.Width - Me.Width) / 2
    Me.Top = (Screen.Height - Me.Height) / 2
    '
End Sub

That's the last of the code for the Discuss project. Save this form (DSCABOUT.FRM) and update the project before you begin testing.

Testing the Discuss Forum Tool

Once you've coded and compiled the project, you'll be able to load it onto any workstation that has MAPI services installed and available.

Note
The discussion tool will not work in a Remote Mail setup. You need to have direct access to at least one message store.

When you first start Discuss, you'll be asked to log into MAPI. One of the first things you should do is open the Options page and select a target folder and set up your default user profile (see Figure 10.8).

Figure 10.8 : Setting Discuss options.

You can select any folder as the discussion target. However, since the idea is to carry on discussions with other people, you'll typically select a public folder as the discussion target.

Note
If you are working on a standalone machine, you can select any personal folder for this demonstration. Just remember that no one but you can see the results!

The program will list only messages that have their Type property set to IPM.Discuss. This means the first time you select a target folder, you won't see any messages. So the next thing you need to do is add a message!

Press New Post to add a new thread to the folder (see Figure 10.9).

Figure 10.9 : Adding a new thread to the folder.

You can also highlight any message in the discussion and double-click it to read it. When you press the Reply button at the bottom of a read form, it automatically turns into a reply form. Even the subject and message body are updated to show that this is a reply (see Figure 10.10).

Figure 10.10 : Replying to a message.

Summary

In this chapter you learned how to use the OLE Messaging library to create online, threaded discussion tools using MAPI services. You learned that there are three ways in which discussion messages differ from mail messages:

You also learned how to build routines that collect all the message stores available to a workstation, all the folders in each of those stores, and all the messages within a folder. You now know how to compose a message for Update (rather than Send) and how to use the CoCreateGuid API to generate unique conversation index values used in threaded discussion tools.

Best of all, most of the heart of this example program can be used to add discussion capabilities to other Visual Basic projects.