Variant arrays and GetRowsAn efficient way of passing larger amounts of data across process and machine boundaries is as Variant arrays. It is possible to make your own arrays using the Array function. But Variant arrays gained in popularity mainly because they were coded directly into the data access interfaces that most Visual Basic developers use. GetRows exists on RDO's rdoResultset objects and both DAO's and ADO's Recordset objects. The ADO version is more flexible than the DAO and RDO implementations-check out its extra parameters. Using Getrows in the data access code looks like this:
Public Function GiveMeData() As Variant Dim rsfADO As New ADODB.Recordset 'The Recordset object Dim vResult As Variant 'to put data in. rsfADO.Open strSQL, strCon 'Open a connection. vResult = rsfADO.GetRows 'Park the results in a Variant array. GiveMeData = vResult 'Send the Variant array back. End Function
Finally the Variant array reaches the user interface (having perhaps passed through a number of hands, classes, and components) and is used.
Public Sub GetData() Dim nRowCount As Integer 'A row count holder. Dim nColCount As Integer 'A row count holder. Dim sCurrentRow As String 'A string to build each row in. Dim vData As Variant 'A Variant to hold our data. 'Call the server and get back a Variant array. vData = oDataServer.GiveMeData 'For each row (rows are dimension 2 of the array). For nRowCount = LBound(vData, 2) To UBound(vData, 2) sCurrentRow = "" 'Initialize the current row. 'For each column in the row. For nColCount = LBound(vData, 1) To UBound(vData, 1) 'Add the column data to the current row. sCurrentRow = sCurrentRow & Space(2) & _ CStr(vData(nColCount, nRowCount)) Next nColCount 'Add the row to a list box - or use it somehow. lstDataRows.AddItem sCurrentRow Next nRowCount End Sub
In each case, GetRows creates a two-dimensional array, which it puts into a Variant. The first array subscript identifies the column and the second identifies the row number. The only downside of GetRows is that, when you have grown used to manipulating collections of objects and accessing their data through their properties, going back to arrays feels clumsy. It is a specific and increasingly unusual implementation. As a result, at TMS we often use a DLL that turns a two-dimensional Variant array into either a recordset object again (either our own implementation, or less frequently one of the standalone implementations available in ADO and RDO) on the client-side, purely for ease of coding. (See the Gendata and DataArray projects in the samples directory for this chapter. These projects use GetRows with ADO, and a custom implementation of the recordset.)
UDTs and LSET Some people (I was not among them) so mourned the passing of the user-defined type (UDT) into history (since UDTs could be passed as a parameter) that they came up with work-arounds, to which they became devoted. The UDT has its Visual Basic roots in file access, particularly where files were structured as records.
Imagine a UDT for an Account record:
Public Type AccountStruct AccountID As Long Balance As Currency Status As AccountStatus AccountTitle As String OverdraftAmount As Currency AvailableFunds As Currency End Type
Since a UDT cannot be passed as a parameter, it has first to be converted. Therefore, a corresponding user-defined type that only has one member, but is the same size as the original is defined:
Type Passer Buffer As String * 36 End Type
Caution
Beware on sizing that in 32-bit Windows, strings are Unicode. Therefore each character = 2 bytes, so if you need an odd number size (such as 17 bytes because you have a byte member in the original type) you would actually have a spare byte.
We then copy the original user-defined type into this temporary one. We can do this with LSet. In the Client component we have:
Dim pass As Passer Dim oUDTer As udtvb5er Set oUDTer = New udtvb5er LSet pass = myorig oUDTer.GetUDT pass.Buffer
In the server we need the same user-defined types defined, along with the following method in our udtvb5er class:
Public Sub GetUDT(x As String) Dim neworig As AccountStruct Dim newpass As Passer newpass.Buffer = x LSet neworig = newpass MsgBox neworig.AccountID & Space(2) & Trim(neworig.AccountTitle)_ & Space(2) & "Current Balance: " & CStr(neworig.Balance) End Sub
To do this we are copying from one type to the other, passing the data as a string, and then reversing the process at the other end. However, as Visual Basic's Help warns, using LSet to copy a variable of one user-defined type into a variable of a different user-defined type is not recommended. Copying data of one data type into space reserved for a different data type can have unpredictable results. When you copy a variable from one user-defined type to another, the binary data from one variable is copied into the memory space of the other, without regard for the data types specified for the elements.
Note
There is an example project in the code samples for this chapter, called UDTPass.vbg. This is not a recommendation, merely a recap of what was possible!
Visual Basic 6 and remoting
With a few exceptions, this chapter has up until now dealt in long-term truths rather than cool new Visual Basic 6 features. I haven't done this to hide things from you deliberately. However passing data from one thing to another is an area where Visual Basic 6 has added quite a few new features, and it's pretty confused in there at the moment. I've attempted to sort them out for you here.
New ways of passing variables (arrays and UDTs) Visual Basic 6 has made it possible to return arrays from functions. In the spirit of variable passing, rather than property touching, this is likely to be of some help. The new syntax looks like this in the function you are calling:
Public Function GetIDs() As Long() Dim x() As Long Dim curAccount As Account Dim iCount As Integer ReDim x(1 To mCol.Count) iCount = 1 For Each curAccount In mCol x(iCount) = curAccount.AccountID iCount = iCount + 1 Next curAccount GetIDs = x End Function
From the client's view it is like this:
Private Sub cmdGetIDs_Click() Dim x() As Long Dim i As Integer x() = oAccounts.GetIDs For i = LBound(x()) To UBound(x()) lstIDs.AddItem CStr(x(i)) Next i lstIDs.Visible = True End Sub
This example has been included in the project group UDTGrp.vbg in the samples directory for this chapter.
Visual Basic 6 has also added the ability to have public UDTs and to pass them between components. Thus a UDT structure such as we looked at earlier:
Public Type AccountStruct AccountID As Long Balance As Currency Status As AccountStatus AccountTitle As String OverdraftAmount As Currency AvailableFunds As Currency End Type
can be passed back and forth to the server thus in the server class's code:
Public Function GetallData() As AccountStruct Dim tGetallData As AccountStruct tGetallData.AccountID = AccountID tGetallData.Balance = Balance tGetallData.Status = Status tGetallData.AccountTitle = AccountTitle tGetallData.OverdraftAmount = OverdraftAmount tGetallData.AvailableFunds = AvailableFunds GetallData = tGetallData End Function Public Sub SetAllData(tAccount As AccountStruct) AccountID = tAccount.AccountID Balance = tAccount.Balance Status = tAccount.Status AccountTitle = tAccount.AccountTitle OverdraftAmount = tAccount.OverdraftAmount End Sub
and called like this from the client:
Dim oAccount As Account Dim tAccount As AccountStruct For Each oAccount In oAccounts tAccount = oAccount.GetallData lstAccounts.AddItem tAccount.AccountID & _ Space(4 - (Len(CStr(tAccount.AccountID)))) & _ tAccount.Balance & Space(3) & tAccount.OverdraftAmount Next oAccount
Remoting ADO recordsets The combination of ADO and the Remote Data Service (RDS) client-side library-intended for speedy, lightweight, disconnected data access for Web applications-can be particularly useful for any distributed client/server system, regardless of its user interface type. The client needs a reference to the Microsoft ActiveX Data Objects Recordset 2.0 Library, while the server has a reference to the Microsoft ActiveX Data Objects 2.0 Library. At its simplest, the client code looks like this:
Private oDD As DataDonor Private Sub cmdGetData_Click() Dim oRS As ADOR.Recordset Set oDD = New DataDonor Set oRS = oDD.GiveData Set oDD = Nothing Set oDataGrid.DataSource = oRS End Sub
While in the server component the DataDonor Class's code looks like this:
Public Function GiveData() As ADODB.Recordset 'A very boring query we can use on any SQL Server. Const strSQL As String = "SELECT * FROM authors" 'We'll use this DSN. Const strCon As String = _ "DSN=pubsit;UID=lawsond;PWD=lawsond;" & _ "DATABASE=pubs;APP=DudeCli;" Dim ors As New ADODB.Recordset ors.LockType = adLockBatchOptimistic ors.CursorLocation = adUseClient ors.CursorType = adOpenStatic ors.Open strSQL, strCon Set ors.ActiveConnection = Nothing Set GiveData = ors End Function
In order to create a disconnected recordset, you must create a Recordset object that uses a client-side cursor that is either a static or keyset cursor (adOpenStatic or adOpenKeyset) with a lock type of adLockBatchOptimistic.
(This example is in the samples directory for this chapter, as RemRset.vbg).
If you return a disconnected recordset from a function, either as the return value, or as an output parameter, the recordset copies its data to its caller. If the caller is in a different process, or on a different machine, the recordset marshals the data it is holding to the caller's process. In so doing it compresses the data to avoid occupying substantial network bandwidth, which makes it an ideal way to send large amounts of data to a client machine. Remoting ADO recordsets really has to be done.
The end resultset is a recordset which has been instantiated on the server, then physically passed down to the client. It is no longer connected to the database at all, but can be used as a recordset, and if changes are made to it the recordset could be passed back to a server and reassociated with a database connection to allow updates to take effect. This avoids the penalty of continued network overhead because each column and field is referenced for its data, and is a strong contender as a replacement for passing Variant arrays.
Returning changes from a disconnected recordset: batch updating When a recordset is disconnected and has been remoted to a different machine, it is possible to make changes to it using its Edit, Update, and Delete methods. In fact, it is one of the only times when it makes sense to use these methods on a cursor, since we are not using up all the normal resources or actually talking to the database. When you are finished changing things, you pass the recordset back to a component that has a live connection to the database. It uses the UpdateBatch method to put in all your changes in a single batch.
Public Sub ReconUpdate(rs As ADODB.Recordset) Dim conn As ADODB.Connection Set conn = New ADODB.Connection conn.Open "DSN=Universe" Set rs.ActiveConnection = conn Rs.UpdateBatch
Fear not the score of other untrustworthy users having updated the same records as you! Just like batch updating in RDO, you have all the tools to sort out collisions, if they occur. However, beware if you are expecting to marshal a recordset from the middle tier to the client machine to resolve any conflicts you have. The three versions of the data (original, value, and underlying) are not marshalled, so you need to do some work yourself. (See Q177720 in the Knowledge Base for more on this.)
Creatable ADO recordsets ADO recordsets can act as a standard interface, even when data is not being accessed from a database using an OLE DB provider, since ADO recordsets can be created independently and filled with data by direct code manipulation. Here is server code for this in a class:
Private rs As ADODB.Recordset Private Sub Class_Initialize() Dim strPath As String, strName As String Dim i As Integer ' Create an instance of the Recordset. Set rs = New ADODB.Recordset ' Set the properties of the Recordset. With rs .Fields.Append "DirID", adInteger .Fields.Append "Directory", adBSTR, 255 .CursorType = adOpenStatic .LockType = adLockOptimistic .Open End With ' Loop through the directories and populate ' the Recordset. strPath = "D:\" strName = Dir(strPath, vbDirectory) i = 0 Do While strName <> "" If strName <> "." And strName <> ".." Then If (GetAttr(strPath & strName) And _ vbDirectory) = vbDirectory Then i = i + 1 With rs .AddNew .Fields.Item("DirID") = i .Fields.Item("Directory") = strName .Update End With End If End If strName = Dir Loop ' Return to the first record. rs.MoveFirst End Sub
This code is in the DataAware.vbp sample project in the samples directory for this chapter.
Persisting recordsets ADO Recordset objects can be saved to a file by using their Save method. This can be valuable if you have a requirement to store data for longer than a run of a program, but without being able to do so in a data source. Imagine a user has made changes to a recordset, and then cannot reconnect to a data source such as a remote database. Persisting data can also be useful for a disconnected recordset, since the connection and the application can be closed while the recordset is still available on the client computer. The code for persisting a Recordset object looks like this:
rsDudes.Save "c:\tms\dudestuff.dat", adPersistADTG
and to get the data out from the file again the following code would do it:
rsDudes.Open " c:\tms\dudestuff.dat "
Files and persistable classes Visual Basic 6 gave classes the capabilities that some other ActiveX instantiables (such as ActiveX Documents and User Controls) have had for a version already-namely the capability to persist their properties through the PropertyBag object. This allows us to store a class's properties between instances. The Persistable property in conjunction with the PropertyBag object lets a class instance be persisted almost anywhere: a file, the Registry, a database, a word-processor document, or a spreadsheet cell.
Why persist? Most components have properties; one of the great annoyances of Visual Basic's Class_Initialize event procedure is that you can't get parameters into it. Typically Class_Initialize is used to set up default values for a class instance. The default values you use are frozen in time when you compile the component, unless you use something like INI settings, Registry entries, files, or command line arguments to vary them. This is where the Visual Basic 6 class's Persistable property comes in, allowing you to save a component's values between runs. To be persistable, a class has to be public and createable. When you set a class's Persistable property to Persistable, three new events are added to the class: ReadProperties, WriteProperties, and InitProperties. Just like in an ActiveX User Control, you can mark a property as persistable by invoking the PropertyChanged method in a Property Let or Property Set procedure, as in the following example:
Private mBattingAverage As Decimal Public Property Let BattingAverage (newAverage As Decimal) mBattingAverage = newAverage PropertyChanged "BattingAverage" End Property
Calling the PropertyChanged method marks the property as dirty. The WriteProperties event will fire when the class is terminated if any property in the class has called PropertyChanged. Then we use the events and the PropertyBag object almost the same way as in a User Control.
There is a twist however: we need a second instance of a PropertyBag object, so that when the object goes away, and takes its PropertyBag object with it, there is a persisted set of properties. The following code shows persisting an object to a text file, but remember that they can be persisted wherever you like, even in a database:
Private pb As PropertyBag ' Declare a PropertyBag object. Private oBatsman As Batsman ' Declare a Cricketer. Private Sub Form_Unload(Cancel As Integer) Dim varTemp as Variant ' Instantiate the second PropertyBag object. Set pb = New PropertyBag ' Save the object to the PropertyBag using WriteProperty. pb.WriteProperty "FirstManIn", oBatsman ' Assign the Contents of the PropertyBag to a Variant. varTemp = pb.Contents ' Save to a text file. Open "C:\tms\FirstBat.txt" For Binary As #1 Put #1, , varTemp Close #1 End Sub
The Contents property of the PropertyBag object contains the Batsman object stored as an array of bytes. In order to save it to a text file, you must first convert it to a data type that a text file understands-here, that data type is a Variant.