Debugging Visual Basic MTS Components

There are several techniques for debugging a Visual Basic MTS component. Each of these techniques has their place, and so they all need to be understood by someone working with MTS. As long as you have Service Pack 2 installed, you can debug directly from the Visual Basic IDE. Ideally though, you should have Service pack 3 installed.

If service pack 3 is installed, you can go to Help | About and you should see Service Pack 3 in the About box.

Let us take a look at an example that shows how this works. We are going to make a request for an ADO recordset from a MTS middle tier component. As we want our MTS component to be stateless, we will make this a disconnected recordset. This example will illustrate perfectly things do not always work in the debugger as they do when the application is compiled.

Begin by creating a new ActiveX DLL project. Add a reference to ADO 2.x. Name the project prjLog. You should have one class, which you should name LogFile. In the properties of the class, set the MTSTransactionMode property to 3-UsesTransaction. Save the class as Logging and the project as prj Log. We are now ready to start adding code.

We want to see how the context object looks under the debugger, so we will definitively want a reference to the object context.

^s you may recall from Chapter 8, the context object can be thought of as a wrapper MTS places around our components. The context object allows us to share security and a transaction across several objects.

We will need to have a private variable that references the ObjectContext object. We will also implement the ObjectControl interface. The ObjectControl interface will give us access to two events associated with the object context: Activate and Deactivate. The ObjectControl will also give us a property, CanBePooled, which we do not really need but will have to implement.

We must implement all of the methods and properties of an interface, even the ones we may not need.

We will need to retrieve an ADO Connection object, so we will also need a private variable for it. Finally we declare a constant that contains the path and filename of our database. Thus we have the following declarations in our LogFile class:

Option Explicit

Private m_objADOConnection As ADODB.Connection Private m_objContext As ObjectContext

Private Const m_cstrDatabasePath As String = "c:\Northwind.mdb" Implements ObjectControl

Don't forget to add a reference to the Microsoft Transaction Server Type library. As we discussed earlier, the ObjectControl interface will give us three events. Go to the Object drop down list box in the code window, and select ObjectControl. You should now have the Activate event listed in the Procedure list box. Add the following code to this event:

Private Sub ObjectControl_Activate()

Set m_objContext = GetObjectContext End Sub

With this line of code we are setting a reference to the instance of the ObjectContext object that MTS has automatically created for us. Our ADO Connection object, though, does not exist until we initialize it. If we initialize it as soon as the component is created, there may be some amount of time before that Connection object is actually used. As there are a limited number of connections that can be made to the database, Connection objects are a valuable resource that we want to quickly use, and then destroy. Thus, we do not want to initialise our ADO Connection object until we are ready to use it.

Go to the drop down Procedure window and select CanBePooled for the ObjectControl object. We will need to fill out this code as all parts of the interface must be coded:

Private Function ObjectControl_CanBePooled() As Boolean

ObjectControl_CanBePooled = False End Function

Finally, we want to deactivate our ADO Connection object as well as our ObjectContext object. We should destroy our ADO connection as soon as we are done using it, but just in case we forget we include a line of code to set the ADO connection to nothing. If it is already set to nothing, then no harm done. Better to be cautious than take a chance that something is overlooked:

Private Sub ObjectControl_Deactivate() Set m_objADOConnection = Nothing Set m_objContext = Nothing End Sub

We will want to see how MTS works with and without an ObjectContext object. When there is an ObjectContext object, we will want to use its CreateInstance method to create our objects. When we use CreateInstance, MTS will create the new object within the ObjectContext object. Using CreateInstance allows security and transactions to be shared over the different objects.

If for some reason something has gone wrong and we do not have an ObjectContext object, we would still like our component to function so we can see how an MTS component works without an ObjectContext object. Thus, we want the ability to create objects with the New keyword. Thus, we will create a function called CreateInstance that will allow us to create objects under MTS. Using the CreateInstance method when we do have an ObjectContext object, and the New keyword when we do not have one.

We will need to pass in an identifier for the component we want to build. The ID for a Recordset object is ADODB. Recordset, and for a Connection object it is ADODB .Connection. These will be the only two objects we will be creating in our MTS component. Add the following code to your project:

Private Function CreateInstance(ProgID As String) As Object On Error GoTo CreateInstanceError If Not m_objContext Is Nothing Then

Set CreateInstance = m_objContext.CreateInstance(ProgID) Else

Select Case ProgID

Case "ADODB.Connection"

Set CreateInstance = New ADODB.Connection Case "ADODB.Recordset"

Set CreateInstance = New ADODB.Recordset End Select End If

Exit Function

CreateInstanceError:

Err.Raise Err.Number, Err.Source & " CreateInstance", Err.Description End Function

We now want to add three functions to open an ADO connection, close an ADO connection and finally one to set a reference to the ADO connection. Let us begin with the function to set a reference to the ADO connection, which I will call SetADOConnection. Each component - Customers, Orders etc. - will have its own function for calling an ADO connection in the Server component so it makes sense to have one routine which defines and opens the ADO connection, which each function can call (instead of having each function opening connections individually). We will need parameters for the userID and the password in order to make a connection to the database. We will also include an optional parameter for the connection string in case we want to use something other than the default:

Private Sub SetADOConnection(ByVal v_strUserID As String, _ ByVal v_strPassword As String, Optional ByVal _ v_sConnectionString As String = "Empty")

We would like some way of knowing when our variable value has been passed in. The IsMissing function is used to tell if an optional parameter is passed but it only works with variants. As we have made our v_sConnectionString a string, the IsMissing function will not work. Therefore, we have set a default value of Empty for v_sConnectionString so we can know when it actually has a value passed in. We will now put in our error handler:

On Error GoTo SetADOConnectionError Next we want to use our CreateInstance function to create our ADO connection:

Set m_objADOConnection = CreateInstance("ADODB.Connection") We then set the properties of our Connection object.

With m_objADOConnection

.CursorLocation = adUseServer

If v sConnectionString = "Empty" Then

.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;'

' & _

"Persist Security Info=False;Data Source=" &

m cstrDatabasePath

Else

.ConnectionString = v sConnectionString

End If

Finally, we will open the ADO connection:

.Open End With

Exit Sub

The ADO has its own collection of errors, so we will loop through this error collection to get any errors in this collection:

SetADOConnectionError:

Dim lngErrorCounter As Long Dim strErrors As String strErrors = Err.Number & ": " & Err.Description

If m_objADOConnection.Errors.Count > 0 Then

For lngErrorCounter = 0 To m_objADOConnection.Errors.Count - 1 strErrors = strErrors & _

m_objADOConnection.Errors(lngErrorCounter).Number & _ ": " & m_objADOConnection.Errors(lngErrorCounter).Description & _ vbCrLf Next lngErrorCounter

End If

Err.Raise Err.Number,

"SetADOConnection",

strErrors

End Sub

We will create a function to retrieve an ADO connection. We will first make sure the function has been set before we actually return a connection:

Private Function GetADOConnection() As ADODB.Connection If m_objADOConnection Is Nothing Then Err.Raise 2001, "GetADOConnection", _

"Trying to Get Connection prior to setting it"

Else

Set GetADOConnection = m_objADOConnection

End If End Function

Finally, we will close the connection. As an error would be raised if the connection were not open, we will first check to see if the connection is open:

Private Sub CloseADOConnection() With GetADOConnection

If .State = adStateOpen Then .Close End If

End With End Sub

Now that we have everything set up to get a connection, we can retrieve a disconnected recordset. Let us create a function called GetRecordset, which will return a Customer recordset from the Northwind database:

Public Function GetRecordset() As ADODB.Recordset

We will need a string variable to store the connection string, and a recordset variable to build the disconnected recordset:

Dim strADOConnection As String

Dim objCustomersRS As ADODB.Recordset

Now we will set the connection string:

On Error GoTo GetRecordsetError strADOConnection = "Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=C:\Program Files\Microsoft Visual " & _ "Studio\VB98\Nwind.mdb;Persist Security Info=False"

Next we will set the ADO connection, and set all of the properties of the recordset:

SetADOConnection "", "", strADOConnection Set objCustomersRS = New ADODB.Recordset objCustomersRS.CursorLocation = adUseClient objCustomersRS.CursorType = adOpenStatic objCustomersRS.LockType = adLockPessimistic objCustomersRS.Source = "Customers"

Set objCustomersRS.ActiveConnection = GetADOConnection We will open the Recordset object, and disconnect it by setting the ActiveConnection to nothing: ObjCustomersRS.Open

Set objCustomersRS.ActiveConnection = Nothing Finally we will return the recordset and create an error handler:

Set GetRecordset = objCustomersRS

CloseADOConnection

Exit Function

GetRecordsetError:

CloseADOConnection

Err.Raise Err.Number, Err.Source & " GetRecordset",

, Err.Description

End Function

The final part of our middle tier component will be a sub that will change the value of one of the fields of the recordset:

Public Sub ChangeRecordset(ByVal v_oCustomerRS As ADODB.Recordset)

v_oCustomerRS.Fields("ContactTitle") = "NewValue" End Sub

Notice that we intend to pass in the parameter v_oCustomerRS by value (the actual value represented by the variable), as we want to test if the recordset is really going to be passed in by value or by reference.

If a variable is passed into the middle tier by reference, then any changes to the parameter v_oCustomerRS made here will also change the variable value on the client. If we pass it in by value, any changes made on the middle tier will not affect the values of the recordset on the client.

This completes our middle tier component. Go to File | Make PrjLog.dll. Once you do this, go to Project | prjLog Properties. From the Project Properties form select the Component tab, and select binary compatibility (which means that the component will recompile with the same GUID):

Now we need to build a client component that will test this middle tier component. Go to File|Add Project, and add an EXE project. Call this new project prjTest, the form frmTest. Your project explorer should look as follows:

Add a reference to the ADO 2.x library, the Microsoft Transaction Server Type library and to the prj Log project. Open up the code window for frmTest and put this in the declarations:

Option Explicit

Dim WithEvents m oCustomerRS As ADODB.Recordset

We have created a recordset with events so we can use the events associated with the recordset. The event we are interested in is the WillChangeField event. If we pass this recordset into the middle tier object by value, and the middle tier changes a field on the recordset, the field event should not be raised on the client. If the recordset is passed in by reference, and we try to change a field value in the recordset on the middle tier component, the field change event should be raised. Go to the Object drop down of the code window, select m_oCustomerRS and type the following code into the WillChangeField event, by way of notification if the event is raised:

Private Sub m_oCustomerRS_WillChangeField(ByVal cFields As Long, ByVal Fields As Variant, adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)

Beep End Sub

Add a command button onto the form and call it cmdTest. Add the following code to the click event of the button:

Private Sub cmdTest_Click() Dim oLog As LogFile Dim sReturnValue As String Set oLog = New LogFile

Set m_oCustomerRS = New ADODB.Recordset m_oCustomerRS.CursorLocation = adUseClient m_oCustomerRS.CursorType = adOpenStatic

Set m_oCustomerRS = oLog.GetRecordset oLog.ChangeRecordset m_oCustomerRS

End Sub

Set prjTest to be the start up project by placing the cursor over prjTest in the project explorer, and select Set As Start Up:

E"2b PnLog (prjLog.vbp) 3-9 Class Modules

1 LogFile (Logging,els)

E"2b PnLog (prjLog.vbp) 3-9 Class Modules

1 LogFile (Logging,els)

1

> Forms

Set as Start Up

-0, frmTest (frr

prjTest Properties...

Save Project Remove Project Publish ►

Add ►

^ Print...

✓ Dockable Hide

Place a breakpoint in the cmdTest click event:

Run the project and click the cmdTest button. When you get to the break point, step through the code using the F8 key on your keyboard. This project runs even though we did not set up the package in MTS. You should notice a few very strange things happen. To begin with, you should not have entered into the ObjectControl event. You will not enter the middle tier component until you reach the Set m_oCustomerRS = oLog .GetRecordset line of code.

Step through the code until you get to the SetADOConnection function. Put the cursor over the m_objConext variable in the If Not m_objContext Is Nothing Then line of code. You should see the following that the variable m_obj Context is equal to nothing:

\ prjLog - LogFile [CodeJ

Create Instance

Private Sub ObjectControl_Deactivate() Set m objADOConnection = Nothing Set m_objContext = Nothing

End Sub

Private Function Create Instance(ProgID As String) As Object On Error GoTo CreatelnstanceError If Not ra ^jlzijContext Is Nothing Then

Set | m_oblContext = NQthing| = 111 objContext. Create Instance (ProgID) Else

Select Case ProgID

Case "ADODB.Connect ion"

Set Createlnstance = New ADODB.Connection Case "ADODB.Recordset"

Set Createlnstance = New ADODB.Recordset

This is why we did not get the Obj ectControl events; there is no ObjectContext object. Thus, we have found out something interesting about the Visual Basic debugger: If you do not register the component in MTS, you do not have a reference to the ObjectContext object.

Keep stepping through the code until you reach the point where the client calls the ChangeRecordset sub of the middle tier component. In theory, when we call the ChangeRecordset sub, and the sub changes the value of the field, the FieldChanged event of the recordset on the client should not be raised. Step through the code and you will discover that the FieldChanged event is raised. This means that the parameter has not been passed by value, but actually by reference.

The reason this happens is not important, it has to do with the properties of the ADO recordset. What is important to realize is that very strange things happen in the debug environment that will probably not happen with the final, compiled DLL. In this case, a by value parameter became by reference and we did not get an ObjectContext object for our MTS component. If you were not careful, you may think the problem is with your code, and not with the debugger. This in turn could result in hours of wasted testing and rewriting of your code.

This perfectly illustrates the point we made earlier: we need to have several methods of debugging to test our components. The Visual Basic IDE is great for finding errors cause by typos, improper logic (setting the wrong path to the database, not initialising an object, etc.). Yet, it is not our definitive test, as things may behave very differently in the compiled component than in the Visual Basic IDE.

Before we look at some other debugging methods, let us stop for a moment and find out how to improve the situation with our component. The first thing we need to do is add the component to MTS. Open up the Transaction Server Explorer. Drill down until you get to the package explorer. Select Packages Installed right-click on the mouse. Select New|Package then click the Create an empty package button. Call the package Log, click Next and Finish. Expand the new Log package, click on Components, right mouse click on Components and select New|Component. Click the Install New Component button, and select the prj Log DLL you made earlier. Your MTS explorer should look as follows:

jg mtxexp - [Console Root\Microsoft Transaction Server\Computers\My... HEEI

tÈ] Console Window Help

1 object(s)

_I Console Root j

- _I Microsoft Transaction Server

Transaction Server Home Page Jt] Transaction Server Support

_I Computers

- M My Computer

_I Packages Installed

Benefit b ExAIr b Flight b IIS In-Process Applications b IIS Utilities

±t-®: IIS-{Default Web Slte//Root/IISSamples/EyAlr/Catalc ^ IIS-{DefaultWeb Site//root/iissarnples/issarnples/oo

+ G prjLog.LogFlle + _l Roles & Sample Bank ^ System ^ TakeANumber ■yffe Tlc-Tac-Toe b UML b Utilities k Visual Rturiin APF PnnknriR

prjLog LogFile

Now run your project again. You will notice that nothing has changed, we are still referencing the ADO recordset by reference and we are still not getting an ObjectContext object. What is wrong here? The answer to this question is simple: we are running the client and server components together. When we do this, we will never get an ObjectContext object, nor will our by value ADO objects be by value.

To fix this problem, all you have to do is run the two projects in separate instances of Visual Basic. Go to the project explorer; place the cursor over PrjTest and right mouse click. Select Remove Project:

Now, run the prjLog project. You will see the following screenshot:

Now, run the prjLog project. You will see the following screenshot:

Click OK. Open another instance of the Visual Basic IDE, open the prjTest project. Put the break point in the cmdTest click event again. Run prjTest, click the command button and step through the code. This time you will find that your ADO recordset is actually passed in by value (the FieldChanged event on the client will not be raised when you change the field). You may not actually enter the ObjectControl events, but when you go into the CreateInstance function you can check and you will see that m_objContext is no longer Nothing.

Thus, we have discovered that to properly debug our MTS component using the Visual Basic IDE we need to run our client and server component in two different instances of the Visual Basic IDE, and the component needs to be registered in MTS. Once we do this, we can actually get the behaviour we expect.

You can play around with the code here. You will something very interesting: now that the parameter is behaving properly, that is, it is now being passed by value, Visual Basic still allows you to change the values of the parameter in the sub. These changes, though, are not passed back to the client.

By the way, if you are wondering why we are concerned with using by reference parameters versus using by value parameters, remember that the communication between the client and server is over the network. This means that every change you make to a by reference parameter on the server needs to be passed back to the client. This is not very efficient and limits the scalability of your MTS component. Let us now take a look at some other ways of debugging our component.

Was this article helpful?

0 0
365 Days Of Motivation

365 Days Of Motivation

Stop Wasting Time And Learn How To Stay Motivated. Finally! Discover How To Stop Your Mind From Wandering, And Upgrade Your Motivation. You Can Hack Your Motivation Levels, Allowing You To Take Your Life To The Next Level.

Get My Free Ebook


Post a comment