To create a data source class, you need to follow a few elementary steps. First add a reference to the Microsoft ActiveX Data Objects 2.0 (or 2.1) Library. Then set the DataSourceBehavior attribute of the class to 1-vbDataSource, which automatically adds a reference to the Microsoft Data Source Interfaces type library (Msdatsrc.tlb). You can now use a new GetDataMember event, the DataMembers property, and the DataMemberChanged method of the class. You can set the DataSourceBehavior attribute to the value 1-vbDataSource in Private classes in any type of project or in Public classes in ActiveX DLL projects, but you can't do that in ActiveX EXE projects because the data source interfaces can't work across processes. You can also create a data source class by selecting the appropriate template when you add a new class module to the current project: In this case, you'll get a class with some skeleton code already present, but you have to add a reference to the Msdatsrc.tlb library manually. You can also create a data source class using the Data Form Wizard.
The GetDataMember Event
The key to building a data source is the code that you write in the GetDataMember event. This event receives a DataMember argument—a string that identifies which particular member the data consumer is requesting—and a Data argument declared as Object. In the simplest case, you can ignore the first argument and return an object that supports the necessary ADO interfaces in the Data argument. You can return an ADO Recordset, another data source class, or an OLEDBSimpleProvider class that you've created elsewhere in the application (as described in the "OLE DB Simple Providers" section later in this chapter).
I've prepared a demonstration program that builds on an ArrayDataSource class, whose source code is on the companion CD. The purpose of this class is to let you browse the contents of a two-dimensional array of Variants using bound controls: You can load data into an array, pass the array to the SetArray method of the class, and then display its contents in a DataGrid or another data-aware control. The user can modify existing values, delete records, and even add new ones. When the editing is completed, the client code can call the class's GetArray method to retrieve the new contents of the array.
The ArrayDataSource class, like most data source classes, incorporates an ADO Recordset object. The SetArray method creates the Recordset, adds the fields whose names have been passed in the Fields array argument, and then fills the Recordset with the data contained in the Values array passed as an argument to the method:
Private rs As ADODB.Recordset ' Module-level variable |
The call to the DataMemberChanged method informs bound controls (more generally, data consumers) that a new data set is available. Both arguments to the SetArray method are declared as Variants, so you can pass them an array of any data type. After the Recordset has been created, it can be safely returned in the GetDataMember event. This event fires the first time a data consumer asks for data and whenever the DataMemberChanged method is called:
' Return the Recordset to the data consumer. |
The event procedure references the Private rs variable through the Public Recordset property; this raises an error with a meaningful message instead of the standard "Object variable or With block variable not set" error message that would be raised if the client code assigns the data source to a bound control before calling the SetArray method. A data source class should also expose all the properties and methods that you expect from an ADO source, including all the navigational Movexxxx methods, the AddNew and Delete methods, the EOF and BOF properties, and so on. The following code simply delegates to the inner rs variable through the Recordset property, which ensures that proper error checking is performed:
' Partial listing of properties and methods |
The code in the class needs to convert the data stored in the Recordset back into a Variant array when the client application requests it. This conversion occurs in the GetArray method:
Function GetArray() As Variant |
The complete version of the class on the companion CD supports additional properties, including the BOFAction and EOFAction properties, which let the class behave similarly to a Data control. To test-drive the ArrayDataSource class, create a form with three TextBox controls and a set of navigational buttons, as shown in Figure 18-1. Then add this code in the Form_Load event procedure:
Dim MyData As New ArrayDataSource ' Module-level variable |
When the client program needs to retrieve the data edited by the user, it invokes the GetArray method:
Dim Values() As String |
Figure 18-1. A client form to test-drive the ArrayDataSource class. Support for the DataMember Property
The ArrayDataSource class is the simplest type of data source class that you can build with Visual Basic 6 and doesn't take into account the DataMember argument passed to the GetDataMember event. You can greatly enhance your class by adding support for the DataMember property in bound controls. All you have to do is build and return a different Recordset, depending on the DataMember you receive.
I've prepared a sample data source class, named FileTextDataSource, which binds its consumers to the fields of a semicolon-delimited text file. If you want to bind one or more controls to such a class, you must specify the name of the text file in the control's DataMember property:
' Code in the client form |
The TextFileDataSource class module contains more code than the simpler ArrayDataSource class does, but most of it is necessary just to parse the text file and move its contents into the private Recordset. The first line in the text file is assumed to be the semicolon-delimited list of field names:
Const DEFAULT_EXT = ".DAT" ' Default extension for text files |
The Visual Basic documentation suggests that you return the same Recordset when multiple consumers ask for the same DataMember. For this reason, the class stores the DataMember argument in the m_DataMember private variable and reloads the text file only if strictly necessary. When I traced the source code, however, I found that the GetDataMember event is called just once with a nonempty string in the DataMember argument when the client program assigns the instance of the class to the DataSource property of the first bound control. Each time after that, the event receives an empty string.
The TextFileDataSource class on the companion CD includes many other features that I don't have room to describe here. Figure 18-2 shows the demonstration program, which loads two forms, a record-based view of a text file and a table-based view of the same file. Because the controls on both forms are bound to the same instance of the TextFileDataSource class, any time you move the record pointer or edit a field value in one form the contents of the other form are immediately updated. The class also exposes a Flush method, which writes the new values back to disk. This method is automatically invoked during the Class_Terminate event, so when the last form unloads and the data source object is released, the Flush method automatically updates the data file.
Figure 18-2. The demonstration program of the TextFileDataSource class can open different views of the same data file. If the views use the same instance of the class, they're automatically synchronized.
The TextFileDataSource class also offers an example of how you can add items to the DataMembers collection to inform data consumers about the available DataMembers items. The class module implements this feature in the Property Let FilePath procedure, where it loads the collection with all the data files in the specified directory:
Public Property Let FilePath(ByVal newValue As String) |
The TextFileDataSource class is bound to its consumers at run time. Therefore, there's no point in filling the DataMembers collection because the clients can't query this information. But this technique becomes useful when you're creating ActiveX controls that work as data sources because the list of all available DataMembers items appears right in the Properties windows of the controls that are bound to the ActiveX control.
Custom ActiveX Data Controls
Creating a custom Data control is simple because ActiveX controls can work as data sources exactly as classes and COM components can. So you can create a user interface that meets your needs, such as the one depicted in Figure 18-3, set the UserControl's DataSourceBehavior attribute to 1-vbDataSource, and add all the properties and methods that developers expect from a Data control, such as ConnectionString, RecordSource, EOFAction, and BOFAction. If you exactly duplicate the ADO Data interface, you might even be able to replace a standard ADO control with your custom Data control without changing a single line of code in client forms.
Figure 18-3. A custom Data control that includes buttons to add and delete records.
A custom Data control that connects to regular ADO sources doesn't need to manufacture an ADO recordset itself, as the data source classes I've shown you so far have. Instead, it internally creates an ADO Connection object and an ADO Recordset object based on the values of Public properties and then passes the Recordset to consumers in the GetDataMember event. The following code is a partial listing of the MyDataControl UserControl module. (The complete source code is on the companion CD.)
Private cn As ADODB.Connection, rs As ADODB.Recordset |
A custom Data control also differs from data source classes in that the code to navigate the Recordset is included in the UserControl module. In the MyDataControl module, the six navigational buttons belong to the cmdMove control array, which slightly simplifies their management:
Private Sub cmdMove_Click(Index As Integer) |
Each time the client assigns a value to a property that affects the Connection or the Recordset, the code in the MyDataControl module resets the cn or the rs variables to Nothing and sets the CnIsInvalid or RsIsInvalid variables to True so that in the next GetDataMember event the connection or the Recordset is correctly rebuilt:
Public Property Get ConnectionString() As String |
Remember to close the connection when the control is about to terminate:
Private Sub UserControl_Terminate()
CloseConnection
End Sub
No comments:
Post a Comment