This white paper provides an overview of several of the current technologies that Microsoft® Outlook application developers should consider when attempting to integrate Outlook workflow applications with Enterprise data. While Outlook does not currently offer its own native data access functionality, the Item.Application.CreateObject method opens the object door to either Open Database Connectivity Direct (ODBCDirect), which is a subset of Remote Data Objects (RDO) 2.0 or ActiveX Data Objects (ADO). Both of these object models provide fast and efficient access to back-end enterprise data. You can also create ActiveX components to communicate with Enterprise data. An ActiveX component provides a middle-tier in your Workflow application. Using Distributed Component Object Model (DCOM) and Microsoft Transaction Server, these components provide greater security, performance, and error trapping over the complexities of Outlook data access calling either ODBCDirect or ADO from Visual Basic Script (VBScript). I’ll show you a simple ListServer object developed in Microsoft® Visual Basic® 5.0 that provides significant performance enhancements for accessing static data that will be presented to the user in an ActiveX ListBox or ComboBox control on an Outlook form.
Enterprise data encompasses many forms of data storage-from departmental data that resides on a local area network (LAN)-based database such as Microsoft® Access or Paradox®, corporate data in a relational database management system (RDBMS) such as Microsoft® SQL Server™, Informix® OnLine Dynamic Server, and ORACLE® 7.x Sever, or legacy mainframe data stores such as VSAM, IMS, and DB2®. I’ll show you several different approaches to integrating Enterprise data with Outlook applications. No one method is preferable over the others; the final choice depends upon the nature and design of your workflow application. After this presentation, you should be able to make an informed choice about data access methods depending upon your design goals and data stores.
As companies develop workflow applications using the rich messaging and forms development environment of Microsoft® Exchange 5.0, some of these applications will require integration with other corporate data stores. Enterprise data stores reside in a variety of DBMS-from LAN file-based data to mainframe data.
Outlook/Microsoft Exchange applications store their data in messaging application programming interface (MAPI) data stores. While MAPI data stores are adequate for many workflow applications, more sophisticated applications require additional links with existing corporate data that inhabits more traditional database containers. Additionally MAPI data stores do not lend themselves currently to relational database design. One-to-many relationships cannot be modeled in the typical MAPI folder. You will find that you can add relational design principles to your workflow application by accessing external data and linking that data to your MAPI data store in a relational manner. I’ll discuss this design concept when I take you step by step through the Northwind Structured Query Language (SQL) Order Application.
For example, you could design a billing and time tracking application using the Outlook contacts and tasks folders. What happens when the design specification calls for adding general ledger (G/L) codes to specific tasks so that the billing for these tasks can be sent to a corporate invoicing application? If you are the application designer, you know that it’s not practical to code the G/L codes and descriptions into the Column property of an MSForms ComboBox. You’ll have to retrieve the G/L account data from a database in your organization and then load the data into your combo box. Once you have crossed the bridge named enterprise data, you’ll need to consider issues relating to security, performance, data integrity, and scalability. The design alternatives that confront you initially might seem somewhat daunting. The choice that you will finally make depends upon the scope of your project and the depth of your programming resources. I’ll try to point out the pluses and minuses of each approach.
Currently there are two principle methods of accessing data in Outlook forms. The first and primary method involves writing VBScript code and using the Item.Application.CreateObject(“ProgID”) to instantiate an object model that provides data services to your application. RDO 2.0 ships with Visual Basic 5.0 Enterprise Edition. ODBCDirect, which is part of the DAO 3.5 object model, is in fact a subset of RDO 2.0. Its properties and methods closely resemble those of RDO. RDO implements a thin wrapper over ODBC and provides a high level of developer control and substantial performance gains over Jet and DAO 3.0. However, the RDO object model is complex and hierarchical, a feature which makes it difficult to write and debug in the somewhat constrained environment of VBScript.
ActiveX Data Objects 1.0 (ADO) is Microsoft’s data access object model of the future. You should heed the following statement from the recently published Data Access Interfaces: “While ADO represents an evolution of both DAO and RDO into a single, simplified, and extensible interface, Microsoft plans that later versions of ADO will supersede all DB-Library, DAO, and RDO functionality.”
While my testing of the different data access interfaces showed ADO to be slower than RDO when called from Outlook’s VBScript, this situation is bound to change in the future as OLE DB drivers are created by major database vendors. Currently ADO is the preferred data access object model for Internet Information Server 3.0. One of the major benefits of ADO is that it requires fewer calls to achieve the same result as RDO. In Outlook’s VBScript environment all variables must be dimensioned as variants. Consequently, all objects pertaining to a given object model are late bound. Late binding of objects strongly suggests that a simpler object model and fewer calls to properties and methods will result in better performance. ADO offers the benefits of simplicity without sacrificing functionality.
ActiveX components let you encapsulate RDO or ADO functionality within your own objects that can be instanced from VBScript’s CreateObject method. I’ve included both code and components for the ListServerRDO and ListServerADO ActiveX components that implement a CListServer class. The CListServer class was created using Visual Basic 5.0 Enterprise edition. The purpose of this class is to supply a variant array to either Outlook’s MSForms ListBox or ComboBox controls. Both of these controls have List and Column properties. If you assign a variant array to the Column property, the control is populated with data from the array in one statement. The Column property is currently the only means of creating a multicolumn MSForms list box or combo box. In the combo box, the BoundColumn property allows you to determine which column of the drop-down list is bound to the edit portion of the control. Additionally you would bind the edit portion of the combo box control to a user-defined field on an Outlook form. Database filled combo and list boxes are important elements of Outlook forms that use Enterprise data.
The benefits of custom components for Outlook forms are numerous. First, you can tailor the data access functionality in your component to a specific business application. Security can be tightened within the component framework. If you desire, you can remove the ConnectString property and place all login passwords in the component itself. Error trapping within Visual Basic 5.0 is superior to error trapping within VBScript. You should use the Err.Raise method in the component to return errors to your Outlook form. Performance is enhanced because Visual Basic components use early binding of all data interface objects rather than the late binding of VBScript. For application developers who want to package their components for enterprise distribution, you can use Microsoft Transaction Server and DCOM to distribute those components securely over your network. By using technologies such as Microsoft Transaction Server, systems network architecture (SNA) Server, and the forthcoming “Cedar” (a Microsoft Transaction Server component that allows intranet, Internet and client/server applications to interoperate with mainframe programs), Outlook developers will be able to reach data on the mainframe and create components that provide that data to Outlook forms. For additional information regarding Microsoft’s mainframe interoperability strategy, see “Integrate the Enterprise” (MSDN Library, Backgrounders, Enterprise Computing Articles).
ActiveX controls will also become available for Outlook forms development. At this point the native MSForms controls on Outlook forms can only be bound to MAPI user-defined fields. Any data binding to remote databases must be accomplished through code. The ActiveX Outlook data control which was in alpha during this writing will provide a means of accessing ODBC compliant databases without writing VBScript code. Finally, the Visual Basic 5.0 Custom Control Edition offers the avenue of customizing ActiveX controls for Outlook forms. You can create those forms on your own or purchase them from third-party vendors.
Here is a summary outline of the technologies discussed above for providing data access to Outlook forms:
ODBCDirect (RDO 2.0)
General Features:
Outlook-specific features:
ActiveX Data objects (ADO)
General Features:
Outlook-specific features:
ActiveX components
Outlook data control
ActiveX custom controls
The initial point of creating the CListServer class in its various flavors was to provide a testing environment for data access methods through the CreateObject method. Since VBScript does not implement Win32 API calls, CListServer also provides a GetTick method which simply is a wrapper around the GetTickCount API function. GetTickCount returns the number of milliseconds elapsed since Windows started running. GetTick provides an accurate method of determining elapsed time. However, the real benefit of Automation components in Outlook forms is that they can provide a middle tier between the Outlook form and your data store. You can create a persistent database connection in your ActiveX component that serves many users opening forms simultaneously. It is not necessary to open a connection for each user that opens a form. Moreover, you can create static variant arrays for recordsets that are required consistently on the forms in your application. For example, the cmbSuppliers combo box in the Northwind SQL application shows all suppliers so that the end user can select a supplier for the items they wish to order. The GetSuppliers method of CListServer can return the variant array to Outlook with a performance gain in the order of 100-300 times faster than using ODBCDirect natively in VBScript.
The GetSuppliers and GetShippers methods in CListServer provide variant arrays based upon a predetermined select statement for the Nwind database upsized to SQL Server. To provide greater functionality, the ConnectString property lets you set a connect string which establishes a persistent connection to a remote database through either RDO or ADO. Finally, you call the GetList method which takes a SQL string as its argument and returns a variant array containing the recordset created by the SQL statement.
For purposes of simplicity, the Northwind SQL Order sample application does not use the CListServer class to access data in SQL Server. The ListServer folder provided with this article contains several examples with VBScript code for using an ActiveX component for data access. I’ll leave it to you to adopt the ListServerADO and ListServerRDO components in the Northwind SQL application. Remember that you can embrace and extend this technology so that all remote data inserts, updates, and deletes are completed via component technology. You’ve moved the bulk of your coding from the sparse environment of VBScript to the robust features of Visual Basic 5.0. C++ developers can also create Outlook ActiveX components using Microsoft Visual C++ 5.0 Enterprise edition.
If you need to turn the tables and access Outlook data in order to write that data to a remote data store, what options do you have? Although this article focuses on bringing enterprise data into Outlook workflow applications, I’ll briefly discuss the current options you face when you want to bring MAPI data into a database. An Microsoft Exchange indexed sequential access method (ISAM) driver is available which can link or import an Microsoft Exchange folder (Mailbox or Public folder) or an Microsoft Exchange Address Book (including the GAL) with Microsoft Access.
Download the Outlook Microsoft ExchangeWizard at http://www.microsoft.com/AccessDev/AccWhite/ExchWiz.htm. This URL also contains a comprehensive article on “Accessing Exchange and Outlook Data Using Visual Basic.” You should be aware however that the Microsoft Exchange ISAM contains several constraints including the following:
If you need greater functionality, you should consider (surprise, surprise) building an ActiveX component that uses a combination of the Microsoft Outlook Object Library, the ActiveX Messaging Library 1.1 (available with Microsoft Exchange Server 5.0), and either DAO 3.5, RDO 2.0, or ADO 1.0. An ActiveX component will give you full reciprocal access between your MAPI data store and your Enterprise data.
Two components are included with this article. ListServerRDO uses RDO to provide variant arrays to MSForms combo and list box controls on Outlook forms. ListServerADO uses ADO to accomplish the same functionality. Here are the step-by-step installation instructions for ListRDO. To install ListADO, just follow the same steps but substitute ListADO for ListRDO.
You can test the performance of ActiveX components with the Outlook forms contained in the ListServer folder of Enterprise Data.pst. When you press a command button on the sample ListServerRDO and ListServerADO forms, the GetTick method is called to begin timing the process. When the process of retrieving a variant array and populating the ComboBox1 control is complete, the GetTick method is called again and the result in milliseconds is displayed in a message box. An ActiveX component, once instantiated, offers the best performance gains when retrieving data from a remote data source. ADO in its current version 1.0 iteration is slower than RDO. As stated above, this situation is likely to change in the future as ADO begins to come in ‘flavors’ optimized for different environments. Here’s a code fragment that demonstrates how the ListServerRDO object is instantiated and called from VBScript:
'Script Level Declarations Dim ComboBox1 Dim objListRDO Const rdDriverNoPrompt = 1 Const rdOpenStatic = 3 Sub Item_Open() Set ComboBox1 = Item.GetInspector.ModifiedFormPages _ ("ListServer Example").Controls("ComboBox1") Set objListRDO = CreateObject("ListServerRDO.CListServer") End Sub Sub cmdRDOGetList_Click() Dim lngStart Dim lngEnd Dim strSQL ComboBox1.Clear strSQL = "Select CompanyName, Phone, SupplierId " strSQL = strSQL & "from Suppliers ORDER BY CompanyName" lngStart = objListRDO.GetTick() 'Modify this connect string if necessary objListRDO.ConnectString = "DSN=NwindSQL;UID=sa;PWD=" ComboBox1.Column = objListRDO.GetList(strSQL) lngEnd = objListRDO.GetTick() MsgBox "Elapsed Time: " & (lngEnd-lngStart) & "milliseconds", vbInformation End Sub
The chart shown in Figure 1 provides a dramatic illustration of the performance benefits of ActiveX components.
Figure 1. Performance Data for ActiveX Components
While it is not possible to draw conclusions that apply to every Outlook application development project that integrates with Enterprise data, the following overall recommendations should be looked at closely when you devise a method for reading and writing remote data in your Outlook forms:
The Northwind SQL Order application illustrates the use of ODBCDirect with Outlook forms. An ODBCDirect connection is established with a SQL Server database created by upsizing the Northwind sample database that ships with Access 97. If you did not install the sample databases with Access 97, you must use Setup to install the Northwind sample database. Once you have installed northwind.mdb, you should then download the Access 97 upsizing wizard from the Access Developer's web site:
http://www.microsoft.com/accessdev/AccInfo/AUT97dat.htm
Use the Upsizing wizard to upsize the Northwind database to SQL Server or any other ODBC Level 2 compliant back-end database.
The Northwind SQL Order application also demonstrates persistence of data in normalized tables. A MAPI folder allows storage of data in its folder's properties and custom user properties. However, a MAPI folder does not lend itself to normalized storage of data. In the Northwind SQL Order example, a user can create an order with custom properties such as date needed, order id, and order total amount. These fields are stored in the Northwind SQL folder. How can you store data that participates in a one-to-many relationship with the Order ID? In order to create relationships with MAPI folder data, you need to use a database container. If a user creates a Purchase Order and posts that order to the Northwind SQL database, the next time they open the Post item for the order in Outlook, the Northwind SQL Order form 'remembers' the detail items associated with that purchase order's Order ID.
Remember that the Northwind SQL Order application focuses on showing you how to use ODBCDirect in conjunction with Microsoft Forms in an Outlook custom form. Microsoft Forms consist of a special set of lightweight ActiveX controls that are intrinsic to Office 97 applications. This sample application will show you how to populate the Microsoft Forms combo box and list box from the tables in the Northwind SQL database. What you won’t see here is a demonstration of custom actions that might route the purchase order to a supervisor for approval. For additional examples that will expand your knowledge of custom forms design and programming, see the sample applications available on the Outlook web site at http://www.microsoft.com/outlook.
Samples that come with Outlook can only be used with Microsoft Outlook. Users of the Microsoft Exchange Client cannot read Microsoft Outlook forms. For more information on creating forms and public folders, see Building Microsoft Outlook 97 Applications, available from Microsoft Press, and these web sites:
http://www.microsoft.com/outlook/
http://www.microsoft.com/OfficeDev/outlook/
A. Establish an ODBC DSN for Northwind.
Before you run the Northwind SQL sample application, you must establish a user DSN (Data Source Name) that points to the Nwind database in SQL Server.
To establish a DSN for Nwind:
NwindSQL
Northwind Sample Database
B. Upsize the Access 97 Northwind database.
C. Install VBScript version 2.0 and the Microsoft Script Debugger for Internet Explorer.
The VBScript code in this sample application utilizes functions (such as FormatCurrency) available in VBScript version 2.0. If you have not already upgraded your version of VBScript, connect to http://www.microsoft.com/vbscript in order to download VBScript version 2.0. If you are visiting the VBScript forum, you should download the current version of Microsoft Script Debugger for Internet Explorer. At the time of this writing, the Script Debugger was still a beta product. Script Debugger provides a vastly improved development environment for VBScript code in Outlook forms. The script debugger in combination with VBScript 2.0 allows you to set breakpoints in code, step the code using the familiar F8 key, and examine variables in the debugger's immediate window.
D. Connect to the Enterprise Data personal folder.
By default, the information store that contains the Northwind SQL Sample Application will be installed to:
c:\program files\microsoft office\office
Follow these instructions to add the folder to your Outlook folders:
Select File | Open Special Folder | Personal Folder command and open Enterprise Data.pst. See the default installation directory information above. Click OK.
If you only want to see how the forms are designed, or if you want to customize the forms that come with the sample application, install the sample for your own private use.
If you want to make the forms available to your organization, or if you want to test the folders, install them in the organization. If you are testing the folder and forms, it is a good idea to restrict access to the public folders while you test them. You must have owner permissions on the organization forms library to install the forms in an organization. For more information, see your Microsoft Exchangeserver administrator.
Follow this procedure to register the Northwind SQL Order sample form in the Personal Forms Library for your private use.
Follow this procedure to register the Northwind SQL Order sample form in the Organization Forms Library for use by yourself and others. You must have administrator privileges to complete this procedure.
To examine the extensive VBScript code that comes with Northwind Order application, post a New Northwind order into the folder. Select the Tools | Design Outlook Form command. Select the Forms | View Code command to view VBScript code. If you wish to examine the code in a different editor than the Outlook Script editor, do the following:
Item.Application.CreateObject(“DAO.dbEngine.35”)
Set MyPage = Item.GetInspector.ModifiedFormPages(“My Page”)
Set MyControls = MyPage.Controls
Set txtName = MyControls.Controls(“txtName”)
Base Form Type: | Base Message Class: | Field categories available: | User-defined fields created in: |
Appointment | IPM.Appointment | All | Active Folder |
Contact | IPM.Contact | All | Active Folder |
Journal | IPM.Activity | All | Active Folder |
Message | IPM.Note | Field categories for built-in forms such as All Contact items are not available for Messages. | Inbox |
Office Document |
IPM.Document.[ClassID] Example: IPM.Document.Office.Binder.8 |
Defined within the source Office application by using Custom tab on document Properties dialog. | Must be created at the folder level rather than the item level. |
Post | IPM.Post | All | Active Folder |
Task | IPM.Task | All | Active Folder |
Table 1 - Field Binding for Outlook custom forms
Variant = object.Column( column, row )
'****************************************************************************** '* Northwind SQL Sample Application '* Purpose: Demonstrates use of ODBCDirect in Outlook Forms '* using VBScript and ActiveX controls. '* Author: Randy Byrne, MCSD Charter Member, MVP Outlook 97 '* Micro Eye, Microsoft Solution Provider '* E-mail: randy_byrne@msn.com or microeye@ncal.net '* Comments: If you want to step execution in this code, '* remove the comment from the stop statement below. '* This code uses functions only available in VBScript 2.0. '* You must install VBScript 2.0 to run this script. '* You must be using Internet Explorer Script Debugger 1.0 '* and VBScript 2.0 in order to step code (Press F8). '* Both are available on http://www.microsoft.com/vbscript '* Extensive VBScript documentation is also available. '****************************************************************************** 'Force explicit variable declaration Option Explicit 'Remove comment from stop only if you want to step execution of code 'Stop 'Script level declarations 'Remember that in VBScript the AS keyword and typed variables 'are not allowed. All variables are typed as variant by default. Dim dbe Dim wrkODBC Dim conDB Dim Rs Dim gstrAppName Dim IsLoading Dim MyArray Dim MyArrayShip Dim MyArrayPO 'Dim page objects and controls collections Dim objPage Dim objControls Dim objPagePO Dim objControlsPO 'Dim all controls on the form Dim cmbSuppliers Dim cmbShippers Dim lstItems Dim lstPO Dim cmdAdd Dim cmdEditQty Dim cmdPostPO Dim cmdDeleteAll Dim txtOrderID Dim txtTotal Dim txtDateNeeded 'Application Name gstrAppName = "Northwind SQL Sample Application" '****************************************************************************** 'DAO Constants 'Remember that DAO constants must be declared with Const statement. 'The Const statement is only available in VBScript 2.0 or above. 'They cannot be called directly as you would in VB or VBA. '****************************************************************************** 'WorkspaceTypeEnum Const dbUseODBC=1 'DriverPromptEnum Const dbDriverComplete = 0 Const dbDriverNoPrompt = 1 Const dbDriverPrompt = 2 Const dbDriverCompleteRequired = 3 'RecordsetTypeEnum Const dbOpenTable = 1 Const dbOpenDynaset = 2 Const dbOpenSnapshot = 4 Const dbOpenForwardOnly = 8 Const dbOpenDynamic = 16 'RecordsetOptionEnum Const dbAppendOnly = 8 Const dbConsistent = 32 Const dbDenyRead = 2 Const dbDenyWrite = 1 Const dbExecDirect = 2048 Const dbFailOnError = 128 Const dbForwardOnly = 256 Const dbInconsistent = 16 Const dbReadOnly = 4 Const dbRunAsync = 1024 Const dbSeeChanges = 512 Const dbSQLPassThrough = 64 '****************************************************************************** 'Message Box Constants 'Message Box constants do not have to be declared as constants 'They are built into VBScript 2.0 '****************************************************************************** '****************************************************************************** '* Procedure: Item_Open() '* Description: Upon open, create an ODBCDirect connection to Northwind DSN '* If an order number exists in the MAPI Folder, then supply '* order details to lstPO and give focus to Purchase Order tab. '* If order number is zero, we have a new order and give the '* focus to Select Items tab. '****************************************************************************** Sub Item_Open() 'Dim procedure variables. Dim curExtension Dim lngOrderID Dim i Dim strSQL 'Error trapping-remove this statement if you are debugging 'On Error Resume Next 'Set this variable to True to indicate loading state IsLoading = True 'Set references to pages and controls so that their properties can be set 'Note that controls are set in the Open procedure. You do not have 'to set these controls again in subsequent procedures since their 'variable names have been dimensioned globally. 'Must unhide in case it's hidden otherwise can't set controls Item.GetInspector.ShowFormPage ("Select Items") Set objPage = Item.GetInspector.ModifiedFormPages("Select Items") Set objControls = objPage.Controls Set cmbSuppliers = objControls("cmbSuppliers") Set lstItems = objControls("lstItems") Set cmdAdd = objControls("cmdAdd") Set cmdEditQty = objControls("cmdEditQty") Set objPagePO = Item.GetInspector.ModifiedFormPages("Purchase Order") Set objControlsPO = objPagePO.Controls Set cmbShippers = objControlsPO("cmbShippers") Set lstPO = objControlsPO("lstPO") Set txtOrderID = objControlsPO("txtOrderID") Set cmdPostPO = objControlsPO("cmdPostPO") Set cmdDeleteAll = objControlsPO("cmdDeleteAll") Set txtTotal = objControlsPO("txtTotal") Set txtDateNeeded = objControlsPO("txtDateNeeded") 'Use the GetODBCConnection function to establish an ODBC Connection If Not (GetODBCConnection("NWindSQL","ODBC;DSN=NwindSQL;UID=sa;PWD=", _ dbDriverCompleteRequired)) Then Exit Sub End If 'Hide the message page Item.GetInspector.HideFormPage ("Message") 'If the order number exists (<>0), then fill lstPO (persistent data on form) 'Otherwise, we have a new order and give Select Items tab the focus lngOrderID = Item.UserProperties("Order Number") If lngOrderID = 0 Then 'New order Item.GetInspector.ShowFormPage ("Select Items") Item.GetInspector.SetCurrentFormPage ("Select Items") txtTotal.Text = FormatCurrency(0) lstPO.Clear cmdPostPO.Enabled = True cmdDeleteAll.Enabled = True cmdAdd.Enabled = True Else 'Existing Order Item.GetInspector.HideFormPage ("Select Items") Item.GetInspector.SetCurrentFormPage ("Purchase Order") lstPO.Clear 'Retrieve persistent data from Database strSQL = "SELECT Order_Details.ProductID, Products.ProductName," strSQL = strSQL & "Order_Details.Quantity, Products.QuantityPerUnit," strSQL = strSQL & "Order_Details.UnitPrice FROM Products" strSQL = strSQL & " INNER JOIN Order_Details ON" strSQL = strSQL & " Products.ProductID = Order_Details.ProductID" strSQL = strSQL & " Where Order_Details.OrderID = " & lngOrderID Set RS = conDB.OpenRecordset(strSQL, dbOpenSnapshot) If RS.RecordCount Then i = 0 Redim MyArrayPO(RS.RecordCount - 1, 5) Do Until RS.EOF MyArrayPO(i, 0) = RS(0) MyArrayPO(i, 1) = RS(1) MyArrayPO(i, 2) = RS(2) MyArrayPO(i, 3) = RS(3) MyArrayPO(i, 4) = FormatCurrency(RS(4), 2) curExtension = RS(2)* RS(4) MyArrayPO(i, 5) = FormatCurrency(curExtension, 2) i = i + 1 RS.MoveNext Loop lstPO.List = MyArrayPO End IF cmdPostPO.Enabled = False cmdDeleteAll.Enabled = False cmdAdd.Enabled = False End If 'Create a recordset to fill an array 'Caution: VBScript does not support the use of bang (!) operator 'to reference fields within recordset such as RS!CompanyName strSQL = "Select CompanyName, Phone, SupplierId " strSQL = strSQL & "from Suppliers ORDER BY CompanyName" Set RS = conDB.OpenRecordset(strSQL, dbOpenSnapshot) If RS.RecordCount Then i = 0 ReDim MyArray(RS.RecordCount-1, RS.Fields.Count) Do Until RS.EOF MyArray(i, 0) = RS(0) MyArray(i, 1) = RS(1) MyArray(i, 2) = RS(2) i = i + 1 RS.MoveNext Loop RS.Close 'Use the List method to populate the cmbSuppliers drop-down cmbSuppliers.List = MyArray cmbSuppliers.ListIndex = 0 End If strSQL = "Select companyname, phone, shipperID " strSQL = strSQL + "from shippers order by companyname" Set RS = conDB.OpenRecordset(strSQL, dbOpenSnapshot) If RS.RecordCount Then ReDim MyArrayShip(RS.RecordCount, RS.Fields.Count) MyArrayShip = RS.GetRows(RS.RecordCount) RS.Close 'Use the Column method to populate the cmbShippers drop-down cmbShippers.Column = MyArrayShip End If 'Create dynamic SQL for items recordset strSQL = "SELECT ProductName, QuantityperUnit, UnitPrice, ProductID" strSQL = strSQL & " FROM products" strSQL = strSQL & " WHERE Discontinued = 0 and SupplierID = " strSQL = strSQL & cmbSuppliers.Column(2) strSQL = strSQL & " ORDER BY ProductName" Set RS = conDB.OpenRecordset(strSQL, dbOpenSnapshot) If RS.RecordCount Then ReDim MyArray(RS.RecordCount - 1, 4) i = 0 lstItems.Clear Do Until RS.EOF 'Populate lstItems MyArray(i, 0) = RS(0) MyArray(i, 1) = 1 MyArray(i, 2) = RS(1) MyArray(i, 3) = FormatCurrency(RS(2), 2) MyArray(i, 4) = RS(3) lstItems.List = MyArray i = i + 1 RS.MoveNext Loop RS.Close End If 'Now the cmbSuppliers control can respond to a change event Isloading = False End Sub '****************************************************************************** '* Procedure: Item_Write() '* Description: Note: This procedure only works if it is defined '* as a function rather than a sub procedure. '* Illustrates the use of Outlook events to enforce '* business rules. '****************************************************************************** Function Item_Write() On Error Resume Next Dim lngOrderID 'Access Order Number through the UserProperties collection lngOrderID = Item.UserProperties("Order Number") If lngOrderID = 0 and lstPO.ListCount > 0 Then If MsgBox("You are posting an open order to the Northwind folder." _ & Chr(13) & "Use the Post Purchase Order command" & Chr(13) _ & "to post to the database and folder." & Chr(13) _ & "Continue to post open order?", _ vbQuestion + vbYesNo , gstrAppName) = vbYes Then Item_Write = True Else Item_Write = False End If End If End Function '****************************************************************************** '* Procedure: Item_Close() '* Description: Housekeeping function to close ODBC connection. '****************************************************************************** Function Item_Close() On Error Resume Next conDB.Close wrkODBC.Close End Function '****************************************************************************** '* Procedure: Item_CustomPropertyChange(ByVal MyPropName) '* Description: You use a custom property change event to respond to a change '* in supplier in drop-down. You can't use the control's '* intrinsic events. These events currently do not fire '* in the VBScript of an Outlook form. '****************************************************************************** Sub Item_CustomPropertyChange(ByVal MyPropName) Dim strSQL Dim i Select Case MyPropName Case "Supplier" If Not(IsLoading) Then lstItems.Clear strSQL = "SELECT ProductName, QuantityperUnit, UnitPrice, ProductID" strSQL = strSQL & " FROM products" strSQL = strSQL & " WHERE Discontinued = 0 and SupplierID = " strSQL = strSQL & cmbSuppliers.Column(2) strSQL = strSQL & " ORDER BY ProductName;" Set RS = conDB.OpenRecordset(strSQL, dbOpenSnapshot) If RS.RecordCount Then ReDim MyArray(RS.RecordCount - 1, 4) i = 0 lstItems.Clear Do Until RS.EOF 'Populate lstItems MyArray(i, 0) = RS(0) 'Produce Name Column 0 MyArray(i, 1) = 1 'Initial Qty Column 1 MyArray(i, 2) = RS(1) 'Unit per Qty Column 2 MyArray(i, 3) = FormatCurrency(RS(2), 2) 'Unit Price Column 3 MyArray(i, 4) = RS(3) 'Item ID Column 4 lstItems.List = MyArray i = i + 1 RS.MoveNext Loop RS.Close End If End If End Select End Sub '****************************************************************************** '* Procedure: cmdAdd_Click() '* Description: This procedure adds selected items from lstItems '* to the lstPO. Copies the existing elements of MyArrayPO '* into TempArrayPO and adds selected items in lstItems '* to TempArrayPO. Finally uses the List method to '* repopulate lstPO with old and new selected items. '****************************************************************************** Sub cmdAdd_Click() Dim i Dim j Dim intUbound Dim curTotal Dim curExtension Dim intSelected Dim TempArrayPO curTotal = CCur(txtTotal.Text) intSelected = 0 'Determine the count of select items in lstItems For i = 0 To lstItems.ListCount - 1 If lstItems.Selected(i) Then intSelected = intSelected + 1 End if Next 'Bail out if none are selected If intSelected = 0 Then MsgBox "You must select items in the list.", vbInformation, gstrAppName Exit Sub End If 'If lstPO contains items then copy MyArray into TempArrayPO If lstPO.ListCount Then intUbound = lstPO.ListCount + (intSelected - 1) Redim TempArrayPO(intUbound, 5) For i = 0 to Ubound(MyArrayPO, 1) For j = 0 to Ubound(MyArrayPO, 2) TempArrayPO(i, j) = MyArrayPO(i, j) Next Next j = lstPO.ListCount For i = 0 to lstItems.ListCount -1 If lstItems.Selected(i) Then TempArrayPO(j, 0) = lstItems.Column(4, i) TempArrayPO(j, 1) = lstItems.Column(0, i) TempArrayPO(j, 2) = lstItems.Column(1, i) TempArrayPO(j, 3) = lstItems.Column(2, i) TempArrayPO(j, 4) = lstItems.Column(3, i) curExtension = lstItems.Column(1, i) * lstItems.Column(3, i) TempArrayPO(j, 5) = FormatCurrency(curExtension, 2) curTotal = curTotal + curExtension j = j + 1 End If Next Redim MyArrayPO(intUbound, 5) MyArrayPO = TempArrayPO lstPO.List = MyArrayPO Else Redim MyArrayPO(intSelected - 1, 5) j = 0 For i = 0 to lstItems.ListCount -1 If lstItems.Selected(i) Then MyArrayPO(j, 0) = lstItems.Column(4, i) MyArrayPO(j, 1) = lstItems.Column(0, i) MyArrayPO(j, 2) = lstItems.Column(1, i) MyArrayPO(j, 3) = lstItems.Column(2, i) MyArrayPO(j, 4) = lstItems.Column(3, i) curExtension = lstItems.Column(1, i) * lstItems.Column(3, i) MyArrayPO(j, 5) = FormatCurrency(curExtension, 2) curTotal = curTotal + curExtension j = j + 1 End If Next lstPO.List = MyArrayPO End If 'Set total in txtTotal txtTotal.Text = FormatCurrency(curTotal) End Sub '****************************************************************************** '* Procedure: cmdDeleteAll_Click() '* Description: Enables deletion of all items in lstPO using clear method. '****************************************************************************** Sub cmdDeleteAll_Click() 'Use the clear method to remove all items from list box lstPO.Clear 'Reset total to zero txtTotal.Text = FormatCurrency(0, 2) End Sub '****************************************************************************** '* Procedure: cmdAbout_Click() '* Description: Show About Message Box. '****************************************************************************** Sub cmdAbout_Click() MsgBox "Northwind SQL Sample Application code by:" & chr(13) _ & "Randy Byrne, MCSD, Micro Eye" & chr(13) & "E-mail: randy_byrne@msn.com" _ & chr(13) & chr(13) & "Uses Northwind SQL Sample Database" _ & chr(13) & "to demonstrate ODBCDirect in Outlook Forms." _ & chr(13) & "You must upsize Northwind database and" _ & chr(13) & "establish NwindSQL DSN before you open this form.", _ vbInformation, "About Northwind SQL Sample Application" End Sub '****************************************************************************** '* Procedure: cmdPostPO_Click() '* Description: Uses insert into to insert PO items into Northwind. '****************************************************************************** Sub cmdPostPO_Click() Dim i Dim strSQL Dim lngOrderID Dim intFail 'Use in-line error trapping On Error Resume Next IntFail = 0 If lstPO.ListCount = 0 Then MsgBox "Add items to Purchase Order before posting!", _ vbExclamation, gstrAppName Exit Sub End If 'Begin transaction wrapper 'Note-This transaction would ideally be wrapped in an ActiveX Automation server wrkODBC.BeginTrans 'Insert into orders table strSQL = "Insert Into Orders (CustomerID,EmployeeID,ShipVia,OrderDate,RequiredDate)" _ & " Values ('WHITC',1," & cmbShippers.Column(2) & ",'" & Now() _ & "','" & txtDateNeeded.Text & "')" conDB.Execute strSQL If Err <> 0 Then MsgBox Err.Description , vbCritical, gstrAppName intFail = True Err.Clear End If 'Returns OrderId of order just added strSQL = "Select Max(OrderID) from Orders" Set RS = conDB.OpenRecordset(strSQL) lngOrderID = RS(0) 'Insert line items in lstPO into order detail table For i = 0 To lstPO.ListCount - 1 strSQL = "Insert Into Order_Details (OrderID, ProductID, UnitPrice," strSQL = strSQL & " Quantity, Discount) Values (" & lngOrderID strSQL = strSQL & "," & lstPO.Column(0, i) strSQL = strSQL & "," & cDbl(lstPO.Column(4, i)) strSQL = strSQL & "," & cDbl(lstPO.Column(2, i)) strSQL = strSQL & ",0)" conDB.Execute strSQL If Err <> 0 Then MsgBox Err.Description , vbCritical, gstrAppName intFail = True Err.Clear End If Next 'Commit the transaction if no error occurred If Not (intFail) Then wrkODBC.CommitTrans txtOrderID.Text = lngOrderID cmdPostPO.Enabled = False cmdDeleteAll.Enabled = False cmdAdd.Enabled = False Item.GetInspector.HideFormPage ("Select Items") Item.Subject = "Northwind Order " & lngOrderID 'Save the form item. Item.Save Else MsgBox "Could not post the order!", vbCritical, gstrAppName wrkODBC.Rollback End If End Sub '****************************************************************************** '* Procedure: cmdEditQty_Click() '* Description: Since the grid cannot be edited directly, this allows '* the user to change the quantity for a selected items. '****************************************************************************** Sub cmdEditQty_Click() Dim i Dim intQty intQty=InputBox ("Enter Quantity", gstrAppName) If Cint(intQty)> 0 Then For i = 0 to lstItems.ListCount - 1 If lstItems.Selected(i) Then lstItems.Column(1, i) = intQty End If Next End If End Sub '****************************************************************************** '* Procedure: GetODBCConnection (ByVal MyDSN, ByVal MyConn, ByVal MyPrompt) '* Description: This procedure establishes an ODBCDirect connection object '* which can be used to open recordsets and execute SQL code. '* Arguments: MyDSN = A valid User DSN. '* MyConn = A valid ODBC connect property string. '* Example: "ODBC;DSN=Nwind" or "ODBC;DSN=Pubs" '* MyPrompt = A valid option value specifying ODBC driver '* prompt options. '* Note: The options argument determines if and when to prompt '* the user to establish the connection. '* Returns: TRUE if successful; FALSE if connection fails. '****************************************************************************** Function GetODBCConnection (ByVal MyDSN, ByVal MyConn, ByVal MyPrompt) Dim strUser Dim strPass 'Turn on error trappping On Error Resume Next 'Set to defaults-change these values if required strUser = "admin" strPass = "" 'Create a DAO object. You must use DAO.dbEngine.35 or you will cause 'a page fault on machines with both DAO 3 and DAO 3.5 installed. Set dbe = Item.Application.CreateObject("DAO.dbEngine.35") If Err.Number <> 0 Then Msgbox "Error#: " & err.number & chr(13) & err.description & chr(13) _ & "Warning -- Could not create DAO 3.5 Object!" & Chr(13) _ & "Please make sure that DAO 3.5 is installed on this machine!", _ vbCritical, gstrAppName GetODBCConnection = False Exit Function End If 'Create an ODBCDirect Workspace Set wrkODBC = dbe.CreateWorkspace("ODBCWorkspace", strUser , strPass , dbUseODBC) If Err.Number <> 0 Then Msgbox "Error#: " & err.number & chr(13) & err.description & chr(13) _ & "Warning -- Could not create ODBC workspace!" & chr(13) _ & "Please make sure that user name and password are correct.", _ vbCritical, gstrAppName GetODBCConnection = False Exit Function End If dbe.Workspaces.Append wrkODBC 'Establish the connection to DSN Set conDB = wrkODBC.OpenConnection("Connection1", MyPrompt, , MyConn) If Err.Number <> 0 Then Msgbox "Error#: " & err.number & chr(13) & err.description & chr(13) _ & "Warning -- Could not create connection to " & MyDSN & "!" & chr(13) _ & "Please make sure that " & MyDSN & " is a valid DSN!", _ vbCritical, gstrAppName GetODBCConnection = False Exit Function End If GetODBCConnection = True End Function
The following code is for ListServerRDO.vbp is contained in one module and one class. You must use Tools | References to set a reference to Microsoft Remote Data Objects 2.0. This listing contains the complete code for ListServerRDO:
'****************************************************************************** 'Name: basListServer 'Purpose: Module level code for ListServerRDO 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: None '******************************************************************************* Option Explicit Public lngInstanceCount As Integer Public intColumnCount As Integer Public strConnect As String Public blnConnected As Boolean Public MyEnv As rdoEnvironment Public MyConn As rdoConnection Public avarSuppliers As Variant Public avarShippers As Variant Public Const ERR_CONNECT = 1000 Public Const ERR_RECORDSET = 1001 Declare Function GetTickCount Lib "Kernel32" () As Long 'Entry point of component Sub Main() End Sub '****************************************************************************** 'Name: RDOOpen 'Purpose: Open rdoConnection 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: True if successful, False if error '******************************************************************************* Function RDOOpen() As Boolean Dim lngRowCount As Long Dim MyRSSuppliers As rdoResultset Dim MyRSShippers As rdoResultset Dim strSQL As String On Error Resume Next Set MyEnv = rdoEngine.rdoEnvironments(0) Set MyConn = MyEnv.OpenConnection("", _ rdDriverNoPrompt, , strConnect) If Err <> 0 Then RDOOpen = False blnConnected = False On Error GoTo 0 Err.Raise vbObjectError + ERR_CONNECT, , _ "Connection to database failed!" _ & vbCr & "Connect String:" & vbCr & strConnect Exit Function Else RDOOpen = True blnConnected = True End If On Error Resume Next 'Create a static variant array that is passed when GetSuppliers 'method is called from base client strSQL = "Select CompanyName, Phone, SupplierId " strSQL = strSQL & "from Suppliers ORDER BY CompanyName" Set MyRSSuppliers = MyConn.OpenResultset(strSQL, rdOpenStatic) lngRowCount = MyRSSuppliers.RowCount If lngRowCount > 0 Then ReDim avarSuppliers(lngRowCount, MyRSSuppliers.rdoColumns.Count) avarSuppliers = MyRSSuppliers.GetRows(lngRowCount) End If strSQL = "Select companyname, phone, shipperID " strSQL = strSQL + "from shippers order by companyname" Set MyRSShippers = MyConn.OpenResultset(strSQL, rdOpenStatic) lngRowCount = MyRSShippers.RowCount If lngRowCount > 0 Then ReDim avarShippers(lngRowCount, MyRSShippers.rdoColumns.Count) avarShippers = MyRSShippers.GetRows(lngRowCount) End If MyRSShippers.Close MyRSSuppliers.Close End Function '****************************************************************************** 'Name: RDOClose 'Purpose: Close rdoConnection 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: None '******************************************************************************* Sub RDOClose() On Error Resume Next MyEnv.Close MyConn.Close blnConnected = False strConnect = "" End Sub Option Explicit '****************************************************************************** 'Name: ClistServer Class 'Purpose: Provide Variant Arrays to controls on Outlook Forms 'Uses: Remote Data Objects 2.0 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/21/97 '******************************************************************************* Private Sub Class_Initialize() lngInstanceCount = lngInstanceCount + 1 End Sub Private Sub Class_Terminate() lngInstanceCount = lngInstanceCount - 1 If lngInstanceCount = 0 Then 'Close the rdoConnection RDOClose End If End Sub '****************************************************************************** 'Name: GetList 'Purpose: Returns variant array to base client 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: strSQL 'Returns: Variant Array populated with resultset created by strSQL '******************************************************************************* Public Function GetList(ByVal strSQL As String) As Variant Dim RS As rdoResultset Dim avarList As Variant Dim intRows As Integer Dim intColumns As Integer On Error Resume Next If MyConn Is Nothing Then ReDim avarList(0, 0) GetList = avarList On Error GoTo 0 Err.Raise vbObjectError + ERR_CONNECT, , _ "Connection to database failed!" _ & vbCr & "Connect String:" & vbCr & strConnect Else Set RS = MyConn.OpenResultset(strSQL, rdOpenStatic) If Err <> 0 Then On Error GoTo 0 Err.Raise vbObjectError + ERR_RECORDSET, , _ "Could not open Recordset for SQL statement" _ & vbCr & strSQL Else intRows = RS.RowCount intColumns = RS.rdoColumns.Count intColumnCount = intColumns End If If intRows Then ReDim avarList(intRows, intColumns) avarList = RS.GetRows(intRows) GetList = avarList Else intColumnCount = 1 ReDim avarList(0, 0) GetList = avarList End If RS.Close End If End Function '****************************************************************************** 'Name: GetTick 'Purpose: Return number of seconds elapsed since midnight to base client 'This function is a wrapper for Win32 GetTickCount 'API calls cannot be made from within VBScript 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/12/97 'Arguments: None 'Returns: None '******************************************************************************* Public Function GetTick() As Long GetTick = GetTickCount End Function '****************************************************************************** 'Name: GetSuppliers 'Purpose: Returns variant array to base client 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: None '******************************************************************************* Public Function GetSuppliers() As Variant If MyConn Is Nothing Then ReDim avarSuppliers(0, 0) GetSuppliers = avarSuppliers Err.Raise vbObjectError + ERR_CONNECT, , _ "Connection to database failed!" _ & vbCr & "Connect String:" & vbCr & strConnect Else intColumnCount = UBound(avarSuppliers) + 1 GetSuppliers = avarSuppliers End If End Function '****************************************************************************** 'Name: GetShippers 'Purpose: Returns variant array to base client 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: None '******************************************************************************* Public Function GetShippers() As Variant If MyConn Is Nothing Then ReDim avarShippers(0, 0) GetShippers = avarShippers Err.Raise vbObjectError + ERR_CONNECT, , _ "Connection to database failed!" _ & vbCr & "Connect String:" & vbCr & strConnect Else intColumnCount = UBound(avarSuppliers) + 1 GetShippers = avarShippers End If End Function '****************************************************************************** 'Name: ConnectString Property Let 'Purpose: Establish rdoConnection based on strConn 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: strConn 'Returns: None '******************************************************************************* Public Property Let ConnectString(ByVal strConn As String) If strConnect = "" Then strConnect = strConn RDOOpen Else If Not (blnConnected) And strConn <> strConnect Then 'Close existing connection RDOClose 'Open new connection RDOOpen If blnConnected Then strConnect = strConn End If End If If Not (blnConnected) Then RDOOpen End If End If End Property '****************************************************************************** 'Name: ConnectString Property Get 'Purpose: Return strConnect to base client 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: strConnect '******************************************************************************* Public Property Get ConnectString() As String ConnectString = strConnect End Property '****************************************************************************** 'Name: ColumnCount Property Get 'Purpose: Returns intColumnCount to base client 'Created By: Randy Byrne, randy_byrne@msn.com 'Date: 3/20/97 'Arguments: None 'Returns: intColumnCount '******************************************************************************* Public Property Get ColumnCount() As Integer ColumnCount = intColumnCount End Property
Available on http://www.microsoft.com:
Building Custom Applications with Microsoft Outlook
Building Outlook Information-Sharing Solutions
Available on http://www.microsoft.com/outlookdev
The Microsoft Outlook 97 Automation Server Programming Model
http://www.microsoft.com/OutlookDev/TechInfo/outprog.htm
Using ODBCDirect in Outlook 97: The Northwind Order Application
http://www.microsoft.com/OutlookDev/TechInfo/odbcoutl.htm
From Office 97/Visual Basic Programmer’s Guide
http://www.microsoft.com/OfficeDev/Docs/OPG/
Understanding Object Models
http://www.microsoft.com/OfficeDev/Docs/Opg/002/002.htm
Microsoft Outlook Objects
http://www.microsoft.com/OfficeDev/Docs/Opg/005/005.htm
Data Access Objects
http://www.microsoft.com/OfficeDev/Docs/Opg/011/011.htm
Accessing Exchange and Outlook Data Using Visual Basic
http://www.microsoft.com/AccessDev/AccWhite/ExcOutlk.htm
Building Successful Client/Server Applications
http://www.microsoft.com/visualtools/scenarios/ClientServer.htm
Web Collaboration and Workflow Applications
http://www.microsoft.com/visualtools/scenarios/WebWorkflow.htm
Integrate the Enterprise
http://www.microsoft.com/visualtools/scenarios/LegacyInteroperability.htm
Data Access Interfaces
http://www.microsoft.com/visualtools/strategy/DataAccess.htm
Other Important Sites for Microsoft Outlook:
Outlook Resource Site
http://outlook.useast.net/outlook/
Slipstick Systems Microsoft ExchangeCenter
http://www.slipstick.com/exchange/
© 1997 Microsoft Corporation. All rights reserved.
The information contained in this document represents the current view of Microsoft Corporation on the issues discussed as of the date of publication. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication.
This document is for informational purposes only. MICROSOFT MAKES NO WARRANTIES, EXPRESS OR IMPLIED, IN THIS SUMMARY.
Microsoft and Visual Basic are registered trademarks and SQL Server is a trademark of Microsoft Corporation.
DB2 is a registered trademark of International Business Machines Corporation. Informix is a registered trademark of Informix Softeware, Inc. ORACLE is a registered trademark of Oracle Corporation. Paradox is a registered trademark of Borland Ingernational, Inc.
Other product or company names mentioned herein may be the trademarks of their respective owners.