Microsoft Access 2002 Using ADO in Microsoft Access 2002 Microsoft® Product Support Services White Paper Written by Keith Fink Published on June 21, 2001 Abstract This paper discusses how to use ActiveX Data Objects (ADO) with forms and reports in Microsoft Access 2002. It also discusses the new Client Data Manager (CDM) in Microsoft Access 2002 and how to use ADO with it. The CDM is a new OLE DB service provider created specifically for use in Microsoft Access 2002. It handles data binding between the database engine (Jet or SQL) and Access objects such as forms, reports, and data access pages. This gives developers more flexibility and better update semantics when they are writing ADO code to bind ADO Recordset objects to Access forms. 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 White Paper is for informational purposes only. MICROSOFT MAKES NO WARRANTIES, EXPRESS OR IMPLIED, AS TO THE INFORMATION IN THIS DOCUMENT. Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation. Microsoft may have patents, patent applications, trademarks, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property. The example companies, organizations, products, people and events depicted herein are fictitious. No association with any real company, organization, product, person or event is intended or should be inferred. 2001 Microsoft Corporation. All rights reserved. Microsoft, Microsoft SQL Server, and Windows are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. The names of actual companies and products mentioned herein may be the trademarks of their respective owners. CONTENTS INTRODUCTION ............................................................................................................. 1 MICROSOFT CLIENT DATA MANAGER (CDM) .............................................................. 2 Client Data Manager Features 2 Improved Update Semantics with Multi-Table Views 2 Improved Autolookup 2 Provides Stable Cursors 3 Visible Defaults 4 Using ADO Connections with CDM 4 Opening ADO Connections with CDM 4 Sharing an Access ADO Connection Using CDM 6 BINDING MICROSOFT ACCESS FORMS TO ADO RECORDSETS .................................. 8 Microsoft SQL Server 8 Microsoft Jet 9 ODBC 10 Oracle 11 Batch Updates Unsupported 13 USING RECORDSETS WITH COMBO BOX AND LIST BOX CONTROLS ...................... 14 Setting the Control Recordset Property 14 Retrieving a Recordset from the Control 16 USING ADO RECORDSETS WITH MICROSOFT ACCESS REPORTS ........................... 18 Setting the Report Recordset Property 18 Assigning the Recordset Property of an Ungrouped Report 20 Assigning the Recordset Property of Grouped Reports 20 Retrieving the Report Recordset Property 22 FOR MORE INFORMATION .......................................................................................... 24 INTRODUCTION With the release of Microsoft Access 2000, developers had the choice of building Microsoft Access solutions that used the Jet database engine or that used Microsoft SQL Server™ or the Microsoft Data Engine (MSDE). Historically, the Jet database engine was the default database engine for use in Microsoft Access, and it offered rich data update semantics to application developers. To make developing client/server applications easier, Microsoft Access 2000 introduced project (.adp) files. In Access 2000, a Microsoft Access project (.adp) file provided access to a Microsoft SQL Server database through the OLE DB component architecture. This allowed developers to create SQL Server databases and an application interface within Microsoft Access without having to load the Jet database engine. Even though project files allowed developers to create client/server applications in Microsoft Access 2000, there were some definite limitations. First, .adp files did not offer the rich update semantics with SQL Server that developers were used to with the Jet engine. Second, developers could write DAO code to further enhance their Access/Jet solutions, such as binding a form to a DAO recordset. However, client/server applications in Microsoft Access 2000 did not offer the same feature set with ADO. Microsoft Access 2000 used the Microsoft Data Shaping OLE DB Provider (MSDataShape) and the SQL Server OLE DB Provider to provide data access to SQL Server databases from within Microsoft Access project (.adp) files. While these providers enabled developers to work with SQL Server objects directly from within Microsoft Access, there were still a number of differences in developing applications for the Jet database engine and Microsoft SQL Server/MSDE. To help bridge the gap between developing Access applications for Jet databases and SQL Server databases, the Microsoft Access team created the Client Data Manager (CDM). Product Support Services White Paper 1 MICROSOFT CLIENT DATA MANAGER (CDM) The CDM is an OLE DB service provider whose goal is to make data access between Microsoft Access and SQL Server very similar to data access between Microsoft Access and the Jet database engine. The CDM is a cursor layer that sits between the data consumer and the Windows® Cursor Engine to enable improved update semantics with the existing MDAC components. There are a number of features the CDM offers in Microsoft Access 2002 to make data access with SQL Server very similar to data access with Microsoft Jet. Client Data Manager Features Improved Update Semantics with Multi-Table Views Improved update semantics is one of the best improvements the CDM offers in Microsoft Access 2002. In Microsoft Access 2000, forms that were based on multi-table views were read-only unless the developer supplied a table name for the form's UniqueTable property. This would allow the form to be partially updateable, so that only fields from the table specified in the UniqueTable property could be edited. When using Microsoft Access 2002 with Microsoft SQL Server 2000, the CDM automatically retrieves unique table information by calling SQL Server stored procedures. This allows fields from both sides of a multi-table view to be updateable without having to supply the form's UniqueTable property. This allows developers to create client/server solutions that are more similar to Access solutions based on Jet. Improved Autolookup Another significant improvement the CDM offers client/server applications is improved Autolookup. Autolookup is a feature that refreshes fields from the Product Support Services White Paper 2 primary table of a one-to-many relationship in a multi-table query when the user edits the foreign key field. For example, if you modified the value in the foreign key field between a Customers table and an Orders table (typically, a customer identifier such as CustomerID), the fields from the Customers table should be refreshed automatically to display the values that correspond with the customer that was selected in the foreign key field. In Microsoft Access 2000, developers had to write custom resynchronization commands using the form's ResyncCommand property. Even though this enabled the Autolookup feature to work, the fields from the primary table were updated only when the record was saved, not when the foreign key field was modified. For more information, please see the following article in the Microsoft Knowledge Base. ACC2000: How to Simulate AutoLookup with a Stored Procedure in Access Client/Server http://support.microsoft.com/support/misc/kblookup.asp?id=Q239886 When using Microsoft Access 2002 with Microsoft SQL Server 2000, developers do not need to write custom resynchronization commands. Microsoft Access retrieves these automatically by calling SQL Server stored procedures. Microsoft Access 2002 also shows the impact of foreign key updates immediately, provided that the recordset contains the primary and foreign keys of the two tables. Provides Stable Cursors One of the most frustrating limitations in Microsoft Access 2000 project (.adp) files was unstable cursors. An unstable cursor is a set of records whose sort order is lost whenever users edited a record. For example, assume a user sorted a form by using the Sort Ascending command on the Access toolbar, and then decided to add a new record. After committing the record, Microsoft Access would lose the sort order that the user had just applied. For more information, please see the following article in the Microsoft Knowledge Base. ACC2000: Sort Order Lost When You Edit Records in a Microsoft Access Project http://support.microsoft.com/support/misc/kblookup.asp?id=Q257532 The CDM solves this problem in Microsoft Access 2002 by providing stable cursors when users modify records after sorting or filtering the recordset. Product Support Services White Paper 3 Visible Defaults In Microsoft Access 2000, users were unable to see the default values of a field when editing data in a form. In order to get this functionality, the developer had to set the form control's DefaultValue property to the desired value. However, if the default value for the field changed on the server, the developer had to change the form as well. The CDM solves this limitation in Access 2002 by automatically showing default values on new records in forms. Using ADO Connections with CDM Because the CDM is an OLE DB service provider, you can use it use it with ADO in a Microsoft Access 2002 application. Note The CDM (Microsoft Access 10.0 OLE DB provider) was designed to be used in Microsoft Access. It is unsupported for use in other applications. There are two ways that you can use ADO connections created with the CDM. You can open and manage your own ADO connection using this provider, or you can share the same ADO connection that Microsoft Access is using to the database. Opening ADO Connections with CDM Opening an ADO connection using the CDM is very similar to opening and managing ADO connections with other OLE DB providers. Because the CDM is an OLE DB service provider, it is not designed to access a specific database format such as Jet or SQL Server. Rather, it is designed to provide services to data retrieved by a separate OLE DB provider, such as the Jet or SQL Server OLE DB providers. Therefore, your ADO code must supply an OLE DB data provider when using the CDM. To specify the CDM as the provider for your ADO connection, set the connection's Provider property to the string "Microsoft.Access.OLEDB.10.0". To specify the data provider for your ADO connection, set the connection's Data Provider property to the OLE DB provider specific string. Sample Code for Opening CDM Connections to Microsoft SQL Server This sample code opens and closes an ADO connection using the CDM and the Microsoft SQL Server OLEDB providers: Sub OpenSQLConnectionWithCDM() Dim cn As ADODB.Connection Set cn = New ADODB.Connection 'Use the Microsoft Access 10.0 (CDM) and 'SQL Server OLEDB providers to open the connection With cn .Provider = "Microsoft.Access.OLEDB.10.0" .Properties("Data Provider").Value = "SQLOLEDB" 'You will need to replace the following properties Product Support Services White Paper 4 'with the name of your SQL Server, user ID, password, 'and database to open the connection. .Properties("Data Source").Value = "MySQLServer" .Properties("User ID").Value = "sa" .Properties("Password").Value = "mypassword" .Properties("Initial Catalog").Value = "NorthwindCS" .Open .Close End With Set cn = Nothing End Sub If you want to specify all of this information in one connect string, you can use code similar to the following: Sub OpenSQLConnectionWithCDM() Dim cn As ADODB.Connection Dim strADOCon As String strADOCon = "Provider=Microsoft.Access.OLEDB.10.0;" & _ "Data Provider=SQLOLEDB;Data Source=MySQLServer;" & _ "User ID=sa;Password=mypassword;" & _ "Initial Catalog=NorthwindCS" Set cn = New ADODB.Connection cn.Open strADOCon cn.Close Set cn = Nothing End Sub Sample Code for Opening CDM Connections with Microsoft Jet This sample code opens and closes an ADO connection using the CDM and the Microsoft Jet 4.0 OLEDB providers: Sub OpenJetConnectionWithCDM() Dim cn As ADODB.Connection Set cn = New ADODB.Connection With cn .Provider = "Microsoft.Access.OLEDB.10.0" .Properties("Data Provider").Value = "Microsoft.Jet.OLEDB.4.0" .Properties("Data Source").Value = "C:\Program Files\" & _ "Microsoft Office\Office10\Samples\Northwind.mdb" .Open .Close End With Set cn = Nothing End Sub If you want to specify all of this information in one connect string, you can use code similar to the following: Sub OpenJetConnectionWithCDM() Dim cn As ADODB.Connection Dim strADOCon As String strADOCon = "Provider=Microsoft.Access.OLEDB.10.0;" & _ "Data Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=C:\Program Files\Microsoft Office\" & _ "Office10\Samples\Northwind.mdb" Set cn = New ADODB.Connection cn.Open strADOCon cn.Close Product Support Services White Paper 5 Set cn = Nothing End Sub Sharing an Access ADO Connection Using CDM Because Microsoft Access opens and manages an ADO connection to the currently open database (Jet or SQL Server), it is important that the connection be accessible for developers writing ADO code within their application. This keeps developers from having to open and manage their own ADO connections to the database when Microsoft Access already has an open connection. To do this, you can use the Connection or AccessConnection properties exposed by the Microsoft Access object model. Connection The Connection property was introduced in Microsoft Access 2000 and is accessible through the CurrentProject object within the Microsoft Access object model. Similar to the way the CurrentDb property exposes a DAO Database object for the currently open database in Microsoft Access, the Connection property exposes an ADO Connection object for the currently open database (Jet or SQL Server). AccessConnection The AccessConnection property is a new property in Microsoft Access 2002 and is also accessible through the CurrentProject object within the Microsoft Access object model. Like the Connection property, it allows developers to share the ADO connection that Microsoft Access is using for the currently open database. The difference between these properties depends on whether you are using a Microsoft Access project (.adp) file connected to a Microsoft SQL Server database or a Jet database (.mdb) file in Microsoft Access. When you are using an Access project in Microsoft Access 2002, the Connection and AccessConnection properties are functionally identical. Both properties return an ADO connection using the Microsoft Access 10.0 OLE DB provider (Microsoft.Access.OLEDB.10.0) and the SQL Server OLE DB data provider (SQLOLEDB). Here is a typical connection string that is returned by either property for a project file connected to a SQL Server database: Provider=Microsoft.Access.OLEDB.10.0;Persist Security Info=True;Data Source=MySQLServer;User ID=sa;Password="mypassword";Initial Catalog=NorthwindCS;Data Provider=SQLOLEDB.1 Therefore, if you are writing ADO code in a Microsoft Access 2002 project file, you can use either property interchangeably because each returns the same connection. For example, in a Microsoft Access 2002 project file, the following two code snippets are functionally equivalent: Dim cn As ADODB.Connection Set cn = CurrentProject.Connection Product Support Services White Paper 6 Dim cn As ADODB.Connection Set cn = CurrentProject.AccessConnection When using a Jet database in Microsoft Access 2002, the Connection and AccessConnection properties return different ADO connections. You can use either property when you are writing ADO code in a Jet database. However, there are certain situations where each property is useful. The following is a breakdown of what each property returns in Microsoft Access 2002, and what you should consider before using these properties. The Connection property behaves exactly as it did in Microsoft Access 2000. It returns an ADO connection that uses the Microsoft Jet OLE DB provider (Microsoft.Jet.OLEDB.4.0 provider). For example, here is a typical connection string that is returned for a Jet database that is using the Connection property: Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Program Files\Microsoft Office\Office10\Samples\Northwind.mdb There are several scenarios where you should use the Connection property when using Jet databases. If your application requires any of the following features, you should continue to use the Connection property in your code: Jet and Replication Objects (JRO) code. Microsoft ADO Extensibility for DDL and Security (ADOX) code. ADO code that requires Index support for ADO recordsets, such as the Seek method. The CDM does not fully support the required OLE DB interfaces necessary for these features to work. If your application requires these features, or if you want to ensure complete backwards compatibility with your Access 2000 ADO code, you should use a connection created with the Microsoft Jet OLE DB provider, which is returned by the Connection property. The AccessConnection property returns an ADO connection that uses the Microsoft Jet OLE DB data provider (Microsoft.Jet.OLEDB.4.0) and the Microsoft Access 10 OLE DB service provider (Microsoft.Access.OLEDB.10.0). For example, here is a typical connection string that is returned for a Jet database that is using the AccessConnection property: Provider=Microsoft.Access.OLEDB.10.0;Data Source=C:\Program Files\Microsoft Office\Office10\Samples\Northwind.mdb;User ID=Admin;Data Provider=Microsoft.Jet.OLEDB.4.0 If you intend to bind Microsoft Access forms to ADO recordsets based on SQL Server or Jet data, you should use the AccessConnection property, or you should open and manage an ADO connection that uses the CDM. Product Support Services White Paper 7 BINDING MICROSOFT ACCESS FORMS TO ADO RECORDSETS This section describes what is required to create an updateable form that is bound to an ActiveX Data Objects (ADO) Recordset object. To bind a Microsoft Access form to a recordset, you must set the form's Recordset property to an open ADO Recordset object. A form must meet two general requirements for the form to be updateable when it is bound to an ADO recordset. The general requirements are: The underlying ADO recordset must be updateable via ADO. The recordset must contain one or more fields that are uniquely indexed, such as a table's primary key. The requirements for updateability in Microsoft Access forms vary for different providers. This section describes the additional requirements for updateability when binding the form to data sources using the Microsoft SQL Server, Jet, ODBC, and Oracle OLE DB providers. Microsoft SQL Server There are two main requirements for supporting updateability when you bind a form to an ADO recordset that is using Microsoft SQL Server data: The ADO recordset's connection must use the Microsoft Access 10.0 OLE DB service provider. The ADO recordset's connection must use the Microsoft SQL Server OLE DB data provider. The following example demonstrates how to bind a form to an ADO recordset that is based on SQL Server data that shares an ADO connection with Microsoft Access: 1. Open the sample project NorthwindCS.adp. 2. Open the Customers form in Design view. 3. Clear the RecordSource property so that the form is unbound. 4. Add the following code to the form's Open event procedure: Private Sub Form_Open(Cancel As Integer) Dim cn As ADODB.Connection Dim rs As ADODB.Recordset 'Use the ADO connection that Access uses Set cn = CurrentProject.AccessConnection 'Create an instance of the ADO Recordset class, 'and set its properties Set rs = New ADODB.Recordset With rs Set .ActiveConnection = cn .Source = "SELECT * FROM Customers" Product Support Services White Paper 8 .LockType = adLockOptimistic .CursorType = adOpenKeyset .Open End With 'Set the form's Recordset property to the ADO recordset Set Me.Recordset = rs Set rs = Nothing Set cn = Nothing End Sub 5. Save the form, and then close it. 6. Open the Customers form in Form view. 7. Add, edit, or delete a record in the form. Note that the form is bound to an updateable recordset that is based on SQL Server data. If you want to open and manage your own ADO connection with SQL Server, see the "Sample Code for Opening CDM Connections to Microsoft SQL Server" section earlier in this white paper. Microsoft Jet Even though it is possible to bind a form to an ADO recordset that is using data from a Jet database, Microsoft recommends you use DAO instead. DAO is highly optimized for Jet and typically performs faster than ADO when used with a Jet database. When you bind a form to an ADO recordset using Microsoft Jet data, you have two alternatives: The recordset's ActiveConnection property must use the Microsoft Access 10.0 OLE DB service provider, as well as the Microsoft Jet 4.0 OLE DB Data provider. The recordset must also be a server-side cursor. -or- The recordset's ActiveConnection property must use only the Microsoft Jet 4.0 OLE DB Data provider and the recordset must be a client-side cursor. The following example demonstrates how to bind a form to an ADO recordset in a Jet database by sharing the ADO connection that Microsoft Access is currently using: 1. Open the sample database Northwind.mdb. 2. Open the Customers form in Design view. 3. Clear the RecordSource property so that the form is unbound. 4. Add the following code to the form's Open event procedure: Private Sub Form_Open(Cancel As Integer) Dim cn As ADODB.Connection Dim rs As ADODB.Recordset Set cn = CurrentProject.AccessConnection Product Support Services White Paper 9 'Create an instance of the ADO Recordset class, and 'set its properties Set rs = New ADODB.Recordset With rs Set .ActiveConnection = cn .Source = "SELECT * FROM Customers" .LockType = adLockOptimistic .CursorType = adOpenKeyset .CursorLocation = adUseServer .Open End With 'Set the form's Recordset property to the ADO recordset Set Me.Recordset = rs Set rs = Nothing Set cn = Nothing End Sub 5. Save the form, and then close it. 6. Open the Customers form in Form view. 7. Add, edit, or delete a record in the form. Note that the form is bound to an updateable recordset that is using Jet data. ODBC When you bind a form to an ADO recordset that is using data from an ODBC database, there are two main requirements: The ADO connection that is used by the recordset must use the Microsoft OLE DB provider for ODBC. The ADO recordset must be a client-side cursor. The following example demonstrates how to open an ADO connection to an ODBC database and to bind a form to it. NOTE These steps assume that the ODBC database contains a table named CUSTOMERS that is identical in structure to the Customers table in the sample database Northwind.mdb. It also assumes you have created an ODBC data source named MyDSN that uses the ODBC driver you need to connect to the back-end database. 1. Open the sample database Northwind.mdb. 2. Open the Customers form in Design view. 3. Clear the RecordSource property so that the form is unbound. 4. Add the following code to the form's Open event procedure: Private Sub Form_Open(Cancel As Integer) Dim cn As ADODB.Connection Dim rs As ADODB.Recordset Dim strConnection As String strConnection = "ODBC;DSN=MyDSN;UID=sa;PWD=;DATABASE=Northwind" 'Create a new ADO Connection object Set cn = New ADODB.Connection Product Support Services White Paper 10 With cn .Provider = "MSDASQL" .Properties("Data Source").Value = strConnection .Open End With 'Create an instance of the ADO Recordset class, and 'set its properties Set rs = New ADODB.Recordset With rs Set .ActiveConnection = cn .Source = "SELECT * FROM Customers" .LockType = adLockOptimistic .CursorType = adOpenKeyset .CursorLocation = adUseClient .Open End With 'Set the form's Recordset property to the ADO recordset Set Me.Recordset = rs Set rs = Nothing Set cn = Nothing End Sub 5. Because you are not using CurrentProject.AccessConnection, add the following code to the UnLoad event of the form: Private Sub Form_Unload(Cancel As Integer) 'Close the ADO connection we opened Dim cn As ADODB.Connection Set cn = Me.Recordset.ActiveConnection cn.Close Set cn = Nothing End Sub 6. Save the form, and then close it. 7. Open the Customers form in Form view. 8. Add, edit, or delete a record in the form. Note that the form is bound to an updateable recordset that is based on ODBC data. Oracle When you bind a form to an ADO recordset that is using data from an Oracle database, there are two main requirements: The ADO connection that is used by the recordset must use the Microsoft OLE DB provider for Oracle. The ADO recordset must be a client-side cursor. The following example demonstrates how to open an ADO connection to an Oracle database and to bind a form to it. NOTE These steps assume that the Oracle database contains a table named CUSTOMERS that is identical in structure to the Customers table in the sample database Northwind.mdb. Product Support Services White Paper 11 1. Open the sample database Northwind.mdb. 2. Open the Customers form in Design view. 3. Clear the RecordSource property so that the form is unbound. 4. Add the following code to the form's Open event procedure: Private Sub Form_Open(Cancel As Integer) Dim cn As ADODB.Connection Dim rs As ADODB.Recordset 'Create a new ADO Connection object Set cn = New ADODB.Connection With cn .Provider = "MSDAORA" .Properties("Data Source").Value = "MyOracleServer" .Properties("User ID").Value = "username" .Properties("Password").Value = "password" .Open End With 'Create an instance of the ADO Recordset class, and 'set its properties Set rs = New ADODB.Recordset With rs Set .ActiveConnection = cn .Source = "SELECT * FROM Customers" .LockType = adLockOptimistic .CursorType = adOpenKeyset .CursorLocation = adUseClient .Open End With 'Set the form's Recordset property to the ADO recordset Set Me.Recordset = rs Set rs = Nothing Set cn = Nothing End Sub 5. Because you are not using CurrentProject.AccessConnection, add the following code to the UnLoad event of the form 6. : Private Sub Form_Unload(Cancel As Integer) 'Close the ADO connection we opened Dim cn As ADODB.Connection Set cn = Me.Recordset.ActiveConnection cn.Close Set cn = Nothing End Sub 7. Save the form, and then close it. 8. Open the Customers form in Form view. 9. Add, edit, or delete a record in the form. Note that the form is bound to an updateable recordset that is based on Oracle data. Product Support Services White Paper 12 Batch Updates Unsupported A powerful feature of ADO is that it allows developers to create client/server solutions that can implement batch updates. It does this by allowing developers to create local, disconnected recordsets for their applications. After the user has edited the data in the local cursor and is ready to commit the changes to the server, the application can use ADO to reconnect the recordset to the server, and then call the UpdateBatch method to commit the changes. Unfortunately, a major limitation of the form Recordset property is that it does not support the UpdateBatch method. Although it is possible to bind a form to a disconnected recordset and edit the data in a local cursor, the UpdateBatch method will not batch updates to the back-end server. When editing data in a form, any changes the user makes are stored successfully in the local cursor. However, Microsoft Access does not notify the Windows Cursor Engine that the affected records require an update. Therefore when you call the UpdateBatch method, ADO does not realize that rows have been modified and does not batch update the changes to the server. Microsoft Access does not have a particularly elegant solution to this problem. One approach you can use to obtain this functionality is to unbind the form completely, and then use ADO code to populate unbound controls on the form with data from the recordset. This gives you the most control over your application. However, it requires the most work because you have to manage the navigation and updating of your recordset completely. Product Support Services White Paper 13 USING RECORDSETS WITH COMBO BOX AND LIST BOX CONTROLS In Microsoft Access 2002, it is possible to use ADO and DAO recordset objects with combo box and list box controls. These controls expose a read/write Recordset property which developers can use to either set or retrieve a recordset. If you set the Recordset property, your code will override the control's RowSource property and Microsoft Access will derive the control's list portion from records within the recordset. The combo box and list box control properties function the same way as for natively filled boxes. Bound Column The column number which contains the bound value Column Count The number of columns to show in the list Column Widths The width of each column in the list Column Heads If set to Yes shows the column name in the list header Important When setting or retrieving the Recordset property of a combo box or list box control, the control's RowSourceType property must be set to Table/View/Stored Proc (or Table/Query in an MDB). If you attempt to set or retrieve the Recordset property of a combo box or list box control whose RowSourceType property is not set to Table/View/Stored Proc (or Table/Query in an MDB), you will receive the following error message: Setting the Control Recordset Property When setting the Recordset property of a combo box or list box control, it is possible to fill the list portion of the control from the recordset. The easiest approach to fill the list portion of a combo box or list box is to set the RowSource property to an SQL statement or to the name of a database object that returns records, such as a table, a query, a view, a stored procedure, or a function. It is also possible to define the list by opening an ADO or DAO recordset object, and then setting the Recordset property of the control to that recordset. This approach would be useful if you already had an open recordset that provided the appropriate records for the list, and you did not want Microsoft Access to open an Product Support Services White Paper 14 additional recordset to fill the list by natively binding it with the RowSource property. The following example demonstrates how to fill the list portion of a combo box control by opening an ADO recordset and setting the Recordset property of the control. NOTE You should always use a client-side cursor or make sure the ADO connection used by the recordset uses the Microsoft Access 10.0 OLE DB service provider. If you use a server-side cursor, the list portion of the control may not be filled correctly or the control's current value may not be displayed. 1. Open the sample database Northwind.mdb. 2. Open the Products form in Design view. 3. Clear the RowSource property of the CategoryID combo box control. 4. On the View menu, click Code to view the form's module. 5. Add the following code to the form's Load event procedure: Private Sub Form_Load() 'Requires reference to Microsoft ActiveX Data Objects '2.1 Library or higher Dim rsCategories As ADODB.Recordset Set rsCategories = New ADODB.Recordset With rsCategories .ActiveConnection = CurrentProject.AccessConnection .Source = "SELECT CategoryID, CategoryName FROM Categories" .LockType = adLockReadOnly .CursorType = adOpenKeyset .CursorLocation = adUseClient .Open End With 'Set the Recordset property of the control to the 'ADO recordset we opened, and then close the recordset Set Me.CategoryID.Recordset = rsCategories rsCategories.Close Set rsCategories = Nothing End Sub The recordset cursor position has no effect on the list portions of combo box and list box controls. If you use ADO to move or change position within the recordset, neither the control's value nor the list portion will change. If you make data changes to the recordset (for example, adding records, editing records, or deleting records), the changes may not show up immediately depending on the provider you are using. If you make data changes to the recordset, you should call the recordset's Requery method, and then reset the Recordset property of the control to ensure that the list is showing the correct values. Product Support Services White Paper 15 Note There is a confirmed problem in Microsoft Access 2002 that causes the Recordset property to reset when switching between form views. To avoid this problem, don't allow users to switch views, or fill the control by using the AddItem method or RowSource property. For more information, please see the following article in the Microsoft Knowledge Base: ACC2002: Combo Box or List Box Recordset Is Lost When You Switch Form Views http://support.microsoft.com/support/misc/kblookup.asp?id=Q282358 Retrieving a Recordset from the Control You can only retrieve a recordset object from the control if you have already set its Recordset property. Combo box and list box controls that are filled from the RowSource property do not return any type of recordset object. If you try to refer to the Recordset property of a control whose Recordset property has not been previously set, you will receive the following error message: To retrieve a recordset from the control whose Recordset property has been previously set, declare an object variable as the appropriate type (ADODB.Recordset or DAO.Recordset). Then set the object variable to the Recordset property of the control. The following example demonstrates how to retrieve an ADO recordset from the Recordset property of a combo box in the NorthwindCS sample project file. 1. Open the sample project NorthwindCS.adp connected to the NorthwindCS database on Microsoft SQL Server 2000. 2. Open the Products form in Design view. 3. Clear the RowSource property of the CategoryID control. 4. Add a command button to the form, and then set its Name property to cmdRecordset. 5. Add the following code to the form's Open event procedure: Private Sub Form_Open(Cancel As Integer) 'Requires reference to Microsoft ActiveX Data 'Objects 2.1 Library or higher Dim rs As ADODB.Recordset Set rs = New ADODB.Recordset With rs Product Support Services White Paper 16 .ActiveConnection = CurrentProject.AccessConnection .Source = "SELECT CategoryID, CategoryName FROM Categories" .CursorType = adOpenKeyset .LockType = adLockOptimistic .Open End With Set Me.CategoryID.Recordset = rs End Sub 6. Add the following code to the command button's Click event: Private Sub cmdRecordset_Click() 'Requires reference to Microsoft ActiveX Data 'Objects 2.1 Library or higher Dim rs As ADODB.Recordset Set rs = Me.CategoryID.Recordset Do Until rs.EOF Debug.Print rs.Fields("CategoryName").Value rs.MoveNext Loop End Sub 7. Open the form in Form view. 8. Click the command button. 9. Press CTRL+G to view the Immediate windows in the Visual Basic Editor. Note that the Category Names have been printed in the Immediate windows. Product Support Services White Paper 17 USING ADO RECORDSETS WITH MICROSOFT ACCESS REPORTS In Microsoft Access 2002, it is now possible to use ADO recordsets with reports in Microsoft Access project files. Unfortunately, the use of the report Recordset property is limited to project files. If you try to set or retrieve a report's Recordset property in a Jet database (.mdb) file, you receive the following error message: Setting the Report Recordset Property Setting the Recordset property of a report is similar to setting the Recordset property of a form; you simply assign the Recordset property of the report to a valid ADO recordset that you have already opened. There are two requirements when setting the report's Recordset property. The first requirement is that you may set the Recordset property only during the report's Open event. The second requirement is that the recordset's shape must match the same shape as the report's design. Must Set the Recordset Property During the Open Event The first requirement of setting a report's Recordset property is that you may set the Recordset property only during the report's Open event. If you try to set the report's Recordset property from anywhere other than the Open event, you receive the following error message: Product Support Services White Paper 18 This applies to subreports as well. You may only set the Recordset property of subreports from the Open event of the subreport. NOTE Even though it is possible to set the Recordset property of a subreport, Microsoft does not recommend that you do so. Microsoft Access ignores the LinkChildFields and LinkMasterFields properties when setting the Recordset property. For natively bound subreports, Microsoft Access uses these properties along with others (RecordSource, Filter, and so on) to build the recordset for the subreport. Because you are supplying the subreport's recordset by setting its Recordset property, Microsoft Access ignores any properties that it would normally use to build the subreport's recordset. Because you can only set the Recordset property during the subreport's Open event, it is not possible to create linked subreports using the Recordset property. Recordset Shape Must Match Report Design Shape The second requirement of setting a report's Recordset property is that the recordset shape must match the report's shape in Design view. In other words, if the report contains one or more grouping levels or aggregate functions (or both), the ADO recordset must be a hierarchical recordset that contains the same grouping levels or aggregate functions (or both). If you try to set the report's Recordset property to an ADO recordset that does not match the grouped shape of the report, the report may be blank or Microsoft Access may return the following error message: Cursor Movement and Data Changes There are some additional things to consider when setting a report's Recordset property. First, cursor movement and position within the ADO recordset does not affect the report. If you use ADO to move or change position within the recordset, the report's current page/printing location will not change. Second, any data changes made to the recordset (for example, adding or editing records) will be shown on the report only if the record being modified has not been printed yet. In other words, if you use ADO to modify a record after the report has formatted and printed it, the changes will not be reflected on the report, even though the underlying table will be updated. Product Support Services White Paper 19 Assigning the Recordset Property of an Ungrouped Report When you use ungrouped reports, setting the Recordset property is very similar to setting it in forms. During the report's Open event, set the Recordset property to a recordset that has been opened. The following example demonstrates how to assign a recordset object to a simple, ungrouped report: 1. Open the sample project NorthwindCS.adp. 2. Open the Customer Labels report in Design view. 3. Clear the RecordSource property so that the report is unbound. 4. Add the following code to the report's Open event procedure: Private Sub Report_Open(Cancel As Integer) Dim cn As ADODB.Connection Dim rs As ADODB.Recordset 'Use the ADO connection that Access uses Set cn = CurrentProject.AccessConnection 'Create an instance of the ADO Recordset class and open it Set rs = New ADODB.Recordset rs.Open "SELECT * FROM Customers WHERE CustomerID LIKE 'a%'", cn 'Set the report's Recordset property to the ADO recordset Set Me.Recordset = rs Set rs = Nothing Set cn = Nothing End Sub 5. Press ALT+F11 to return to Microsoft Access. 6. Save the report, and then close it. 7. Print preview the Customer Labels report. Note that the report is bound to a recordset that only displays records where the CustomerID field begins with 'A'. Assigning the Recordset Property of Grouped Reports When you bind a grouped report to an ADO recordset, the recordset's shape must match the report's grouping level shape. In order to do this, you must create a hierarchical recordset using the SHAPE, APPEND, RELATE, and COMPUTE clauses in your SQL string. It is beyond the scope of this white paper to go into detail on data shaping syntax; however, this white paper will demonstrate an approach to determining the correct syntax for a grouped report. To build the correct SQL grammar for a grouped report, you must determine what fields are necessary for each group level and the detail section of the report, plus any aggregate functions the report will need. The easiest approach to understanding the data shaping syntax required by grouped reports is to inspect the Shape property of a grouped report. The Shape property returns a string that represents the ADO shape command that Microsoft Access is using for the report. Product Support Services White Paper 20 Using the Report's Shape Property to Understand Shaping Syntax In Microsoft Access 2002, the Shape property returns the ADO shape command corresponding to the sorting and grouping of the report. It is possible to use this property for existing reports to determine how to build the appropriate ADO shape command for your reports. The following example demonstrates how to analyze the Shape property for an existing report and determine how it is applied to the report: 1. Open the sample project NorthwindCS.adp. 2. Open the Products by Category report in Design view. Note that this report contains one group level, composed of a group header and footer and a detail section. 3. Note that the group level is grouping records based on the CategoryName field. 4. Note the fields in the detail section: ProductName and UnitsInStock. 5. Note that the group footer contains a control that uses the Count aggregate function to count the records in each group. 6. Print preview the report. 7. With the report open in print preview, type the following expression in the Immediate windows, and then press ENTER: ?Reports("Products By Category").Shape 8. This should return the following ADO shape command: SHAPE (SHAPE {SELECT CategoryName, ProductName, UnitsInStock FROM [Products by Category]} AS rsLevel0 COMPUTE rsLevel0, Count(rsLevel0.[ProductName]) AS __Agg0 BY CategoryName AS __COLRef0) AS RS_230 9. Concentrate on just the SELECT portion of the SQL statement: ...SELECT CategoryName, ProductName, UnitsInStock FROM [Products by Category]... Note that this portion of the statement only selects the fields required by the detail section and grouping levels. It includes the CategoryName field from the group level, and the ProductName and UnitsInStock fields from the detail section. These three fields will make up the child recordset for each parent row in the parent recordset. Note from the original SQL statement that this portion is wrapped in braces {}, and is aliased as rsLevel0. 10. Concentrate on the COMPUTE clause of the original SQL statement: …COMPUTE rsLevel0, Count(rsLevel0.[ProductName]) AS __Agg0 BY CategoryName AS __COLRef0)… This portion of the statement creates the parent band in the hierarchical recordset. The parent recordset will contain a unique row for each field specified Product Support Services White Paper 21 after the BY clause above. In this case, because the report is grouping on CategoryName, the parent recordset will contain 8 rows, because there are 8 unique categories in the Categories table. The parent recordset will also contain three fields: CategoryName, __Agg0, and rsLevel0. The CategoryName field contains the values from each unique occurrence of that field from the underlying table. The __Agg0 field contains the Count([ProductName]) for each band. The rsLevel0 field contains a child ADO recordset that contains the three fields specified in the SELECT portion of the SQL statement. Visually, the hierarchical recordset would be represented as: Note that the parent row contains the information that the group level contains, whereas the child recordset contains the information specific to the detail section. Retrieving the Report Recordset Property After a report has been opened, you can retrieve an ADO recordset from the report's Recordset property. However, you cannot retrieve the report's recordset during the Open event. During this event, the report's recordset does not exist yet. If you try to retrieve the Recordset property of a natively bound report during the Open event, you receive the following error message: Product Support Services White Paper 22 To retrieve the recordset, define a variable as ADODB.Recordset, and then set the variable to the report's Recordset property. For example, this sample code runs during the report header's Format event, retrieves the recordset, and prints out the record count: Private Sub ReportHeader_Format(Cancel As Integer, FormatCount As Integer) Dim rs As ADODB.Recordset Set rs = Me.Recordset rs.MoveLast Debug.Print rs.RecordCount Set rs = Nothing End Sub Conclusion Microsoft Access 2002 offers powerful features through the improved form Recordset property and the Client Data Manager. The form Recordset property and the Client Data Manager provide developers with more flexibility and improved update semantics when binding forms to ADO recordset objects as well as the capability to bind forms to other data sources, such as Jet or Oracle databases, through ADO. The new Recordset property for controls allows developers to bind list box and combo box controls to ADO recordset objects that have already been opened, in order to minimize network traffic. Finally, the new Recordset property for reports allows developers to bind project (.adp) reports to flat and hierarchical ADO recordsets. Product Support Services White Paper 23 FOR MORE INFORMATION For the latest information about Microsoft Access see the following resources: http://msdn.microsoft.com/office/ http://www.microsoft.com/office/access/default.htm Product Support Services White Paper 24