Solved

converting from com to com+

Posted on 2002-03-12
15
372 Views
Last Modified: 2007-12-19
We have a com object that is called in ASP pages, Access and VB Programs. We decided to move over to COM+ in order to be able to be able to benefit from COM+ transactions. Since I converted the dll I've been running into problems like not being able to say myRecordset.close after an insert fails (maybe this has something to do with passing the recordset reference between procedures.) We are using W2K Server, Our workstations are W2K and W98.
 These are the steps I took to convert. Please tell me what I might have missed and what other code has to be changed in order to convert properly.

Changed each class to RequireNewTransaction

Added IMPLEMENTS OBJECTCONTEXT and included the 3 methods:
Activate, Deactivate and AllowObjectPooling (or whatever - I'm not in front of the code now)

I changed all the Dim rs as New ADODB.Recordset to
Dim rs  as ADODB.Recordset
set rs = objcont.CreateInstance("ADODB.Recordset")
-btw, is it correct to do this? Do I only have to use create Instance for myObject?

I added methods to SetAbort and SetComplete
 
I set references to the COM+ library (there are a few - which is the right one?)

I compiled as Binary

I created a new COM+ package and added  the DLL (sometimes I see that If I make changed to the DLL it is not reflected in the package unless I delete the package and recreate. Then i will see changes that I made - why is that?)

Ok that is a mouthful. I wouldn't mind if someone could direct me to a TO THE POINT MSDN article that spells this out clearly. There are articles out but I found none that spell out the necessary steps and well as problems to watch out for.

I would appreciate as much guidance as possible as SOON AS POSSIBLE (I have a dealine!!!!!! (don't we all?))

Thanks
0
Comment
Question by:kellykln
  • 9
  • 6
15 Comments
 
LVL 18

Expert Comment

by:mdougan
Comment Utility
Congratulations, it looks like you figured out the hardest parts.  Now, we can just compare some notes and see what little things might be different from what I've run across.

First, RequireNewTransaction is necessary if you are going to be updating the database, otherwise, UsesTransaction is good.

Second, I Implemented IObjectConstruct not OBJECTCONTEXT, and I only did this where I wanted to read in a construction parameter:

Implements IObjectConstruct

Private Sub IObjectConstruct_Construct(ByVal pCtorObj As Object)
    m_ConnectionString = pCtorObj.ConstructString
End Sub

This let me set the database connect string through the Component Manager.

Third, when creating a recordset or command object, I'd use:
    Set m_Cmd = GetObjectContext.CreateInstance("ADODB.Command")

(make sure this component has a reference to Microsoft Active Data Objects Library)

You just said objCont.CreateInstance... you probably meant GetObjectContext, but if you're not in front of the code..

Make sure that your method calls one or the other of the SetAbort or SetComplete every time.  You'll have problems if you don't call one of them.  So, double-check your error handling to make sure SetAbort gets called.

Fourth, the only reference I used was COM+ Services Type Library which points to a file C:\WINNT\System32\COMSVCS.DLL

Fifth, if you change the interface of your component it probably wont be reflected in the COM+ Application correctly until you delete and re-add it.  You don't have to delete the whole application, just the component, and then re-add the component.  Always use the Browse button to browse to the DLL that you just compiled, never pick it from a list (same goes for inside of your program if you have one component that references another component).  Just get in the habit of doing this after every compile, it will make your life easier.  Actually, just to be safe, I'd usually delete the component from my COM+ application BEFORE I compiled.  I think that in doing so, it removes the registry entries and keeps you from getting multiple registry entries for the same component.

OK, just a few more things.... if you happen to call another COM+ component from inside of the first component that you called from your UI then when you create those objects do it this way:

Dim oClient as MyClass.MyClients

    Set oClient = GetObjectContext.CreateInstance("MyClass.MyClients")

If you do a CreateObject then it messes up the transaction I think.

Occasionally, you'll get some inexplicable problem with creating instances of other COM+ objects, if so, try diming the local variable as object:

Dim oClient as object
    Set oClient = GetObjectContext.CreateInstance("MyClass.MyClients")


I don't know why, but this got us by some problems.

Finally, keep in mind that if you are trying to pass a recordset object from one component to another component these objects have to be "marshaled".  Basically, you can't pass pointers to objects that are running in different processes, so, COM+ copies the object and then sends the data in the object over to the copy (as you can guess, this is very inefficient).  However, it doesn't copy everything in the recordset.  It just copies the data.  So, a lot of the recordset properties get whacked, like recordcounts, filters, really any property of the recordset that doesn't include the data.

If you absolutely have to pass a recordset back and forth, what we did was issue the rs.Save method and save the recordset to a microsoft DOM Document.  Then, pass the DOM Document's XML property to the other component.  Then, either we'd load that XML into another DOM Document in the other component and work with it from there (generally that is how we did it), or, I think you might be able to load the XML back into a recordset on the other end.

The XML solution was very fast and efficient, and we ended up using it for all of our communication between components.

Make sure that any Parameters you pass to a method in one component from another (or from the UI) are passed ByVal - the default in VB is ByRef if you don't specify it.  If you pass a parameter ByRef, then COM+ marshals the data to the procedure and then back again(!), just in case you updated it (horribly inefficient).  So, only pass the parameter ByRef is you know your method is going to update it.

Well, I'll guess that there will be some more dialog before we're done, but hopefully this will get you started, and/or raise some more questions for you.
0
 

Author Comment

by:kellykln
Comment Utility
Hi, I did most of your suggestions.
I wasn't able to get  the Implements IObjectConstruct to work.  What are the differeces? I used the following in my class:

Private mObjectContext As ObjectContext
Implements ObjectControl

Private Sub ObjectControl_Activate()
   Set mObjectContext = GetObjectContext()
End Sub
Private Sub ObjectControl_Deactivate()
   Set mObjectContext = Nothing
End Sub

Private Function ObjectControl_CanBePooled() As Boolean
   
End Function
Public Sub SetComplete()
   mObjectContext.SetComplete
End Sub
Public Sub SetAbort()
   mObjectContext.SetAbort
End Sub

I also have  changed DataErrors = SaveObject(Me, mrsdata)
to DataErrors = SaveObject(SafeRef(Me), mrsdata)

 If I am passing Me to a Public Function within my same dll do I have to do this?

The error message that I am getting is when I try to close a recordset:
Operation not allowed in this context.

It seems to me that  there is some sort of failure and a setabort is implicitly called and then I can not issue
rs.close.



0
 
LVL 18

Expert Comment

by:mdougan
Comment Utility

Private mObjectContext As ObjectContext
Implements ObjectControl

You shouldn't have to do this.  I think by referencing the COM+ services library you can just say GetObjectContext.SetAbort etc.  You don't have to declare a memember variable as an ObjectContext and/or use ObjectControl to get a reference to it.  Also, if you're doing it to affect pooling, you can set pooling options under the properties of the component in the component manager.

The only reason we had to implement IObjectConstruct is that we wanted an event that fires when the component started up that could read some input parameters from a constructor string, no need to implement that if you don't

I also have  changed DataErrors = SaveObject(Me, mrsdata)
to DataErrors = SaveObject(SafeRef(Me), mrsdata)

If I am passing Me to a Public Function within my same dll do I have to do this?

Well, Me will have a scope within a class.  If your DLL only has one class, then yes, any Function within that class can use Me inside of it without having to pass it to the function.  But, if the function is in a bas module or another class, then you will need to pass it.

It seems to me that  there is some sort of failure and a setabort is implicitly called and then I can
not issue
rs.close.

I know I've seen this issue too, but I'll have to think about it.  In general, you should always code to assume that your recordset might not have ever been opened properly, and make sure to only close it when you know it was opened properly.  You can use code like this:

rs.Open
If rs.State = adStateOpen then
   if Not rs.EOF and Not rs.BOF then
      While Not rs.EOF
         .....
         rs.MoveNext
      Wend
      rs.close
   Else
      debug.print "no records were returned"
   End if
Else
   debug.print "error opening recordset"
end if

Set rs = Nothing

Notice how the rs.close is only issued after the state has been checked for adStateOpen.

I don't think that the error message has to do with the SetComplete or SetAbort.  That is purely for the transaction.
0
 

Author Comment

by:kellykln
Comment Utility
I already tried testing for adStateOpen which returned true and still gave me an error when I say rs.close. I am getting frustrated. I think I've got to buy a book that will help me get a handle on this whole concept of com+. Any suggestions? Do you know of any good web references for VB and COM+?
0
 
LVL 18

Expert Comment

by:mdougan
Comment Utility
Well, here is the white paper that we used extensively for our development.  Download the complusprint2.exe which is a zipped document of the 80 page white paper.

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncomser/html/complus.asp

I saw some stuff about problems with opening connections in the class_initialize method, as this executes before the component is enlisted in the transaction... could have something to do with your problem.

Maybe if you could show an example of one of your components methods, I could see the problem.

Now, the other thing that we followed very closely is that our components were designed to be stateless, so, each time into a method, we'd be responsible to establish a connection, declare our local variables for database stuff, open, get the results and close etc..  if you're opening a recordset in one call to your component and then trying to come back in to the component on another call and try to use the same recordset that might cause you some problems....

I'll look at one of our samples at work tomorrow and post it, maybe you can see something we're doing that you're not...
0
 
LVL 18

Accepted Solution

by:
mdougan earned 200 total points
Comment Utility
OK, sorry for the voluminous code, but here is what I have.  In our application, we created a COM+ object who was responsible for doing all of our database communication.  We would create an instance of this object from inside of one of our other COM+ business objects and pass it along the information to run the query.  It would then pass back a disconnected recordset.  Usually, our business object would then convert the recordset to XML and pass that back to the UI.

I left out some code specific to creating and working with parameters, as the example of the call from the business component to the query component doesn't use parameters.  There was also a lot of care given to parameters because some calls may want to return Output parameters in addition to the resultset, so the query component went through the list of parameters and stripped out any Input parameters and only returned the output paramters to the calling business object.

Take a look, maybe you'll see something we're doing that you were not.  If you have any questions about the code, let me know.....

This is some of the Query component code

Option Explicit

Private m_Con As ADODB.Connection
Private m_Cmd As ADODB.Command
Private m_Parm As ADODB.parameter
Private m_ConnectionString As String
Private m_bUpdateOnly As Boolean
Private m_Options As ADODB.CommandTypeEnum
Private m_Results As ADODB.Recordset
Private m_DisconnectRS As Boolean
Private m_Mode As ADODB.ConnectModeEnum
Private m_CursorType As ADODB.CursorLocationEnum

Implements IObjectConstruct

'object construct allows us to administratively pass in the connection string
Private Sub IObjectConstruct_Construct(ByVal pCtorObj As Object)
    m_ConnectionString = pCtorObj.ConstructString
End Sub


' user passed in either a plain sql sp string i.e. sp_name @par1=val, @par2=val2 etc.....
' or an encoded string for speed (adodb doesn't need to type check parameters) of the form
' sp_name @par1:type1:in=val1, @par2:type2:out=val2, @par2:type2:inout=val2  etc....
' direction is optional, will default to in if not given
Public Function execSP(errorString As String, _
                       ByVal commandText As String, _
                       ByVal parameters As Variant, _
                       ByRef outParameters() As Variant, _
                       Optional ByVal mode As ADODB.ConnectModeEnum = adModeRead, _
                       Optional ByVal cursorType As ADODB.CursorLocationEnum = adUseClient) As ADODB.Recordset
       
On Error GoTo errorHandler:
    'this returns a disconnects rs
    m_DisconnectRS = True

    ' setup options
    m_CursorType = cursorType

    ' this does all the hard work
    If makeSPCall(errorString, commandText, parameters, outParameters) = True Then
        Set execSP = m_Results
        GetObjectContext.SetComplete
    Else
        GetObjectContext.SetAbort   'failure, bomb the transaction
    End If
       
    ' always tidy up
    Set m_Results = Nothing
    Set m_Cmd = Nothing
    Exit Function

errorHandler:
    ' always tidy up
    Set m_Results = Nothing
    Set m_Cmd = Nothing
   
    Set execSP = Nothing
    errorString = Error
    GetObjectContext.SetAbort 'bomb transaction
   
    Exit Function
   
End Function


Private Function makeSPCall(errorString As String, _
                            ByVal commandText As String, _
                            ByVal parameters As Variant, _
                            ByRef outParameters() As Variant) As Boolean

    'create the command obeject
    Set m_Cmd = GetObjectContext.CreateInstance("ADODB.Command")
    m_Cmd.commandText = commandText
    Dim allOk As Boolean
    allOk = True
   
    m_Cmd.commandText = commandText
    ' see if we've just been given a straight string to execute or any parameters
    If Not IsArray(parameters) Then
        m_Cmd.commandType = adCmdText
    Else
        ' been given a parameter list
        m_Cmd.commandType = adCmdStoredProc
       
        Dim i As Integer
        Dim param As Parameter_v1
        For i = LBound(parameters) To UBound(parameters)
            Set param = parameters(i)
            ' error if we're given empty parameters
            If Not (param Is Nothing) Then
                allOk = addParameter(errorString, param)
            Else
                errorString = "Error, a parameter of nothing was given at location " & CStr(i) & " in the input parameters."
            End If
            'break if we can't add the parameter
            If allOk = False Then
                Exit For
            End If
        Next i
    End If
   
    ' if setup was ok, go fo it
    If allOk = True Then
        'run it, and see what happens
        If execute(errorString) = True Then
            'remove any input only parameters for efficiency
            If IsArray(parameters) Then
                allOk = resetParameters(parameters, outParameters)
            End If
            makeSPCall = allOk
            'success, transaction can be committed as far as we're concerned
        Else
            makeSPCall = False
        End If
    End If
   

End Function

Private Function execute(errorString As String) As Boolean
On Error GoTo errorHandler

    Dim lAffected As Long
   
    ' setup connection
    Set m_Con = GetObjectContext.CreateInstance("ADODB.Connection")
       
    ' select a mode that allows us to diconnect the recordset
    m_Con.mode = m_Mode
    m_Con.CursorLocation = m_CursorType
    m_Con.Open m_ConnectionString
    m_Cmd.ActiveConnection = m_Con
    m_Cmd.CommandTimeout = 30
   
    'now we're in a state to make a decision on whether to commit or not.
    GetObjectContext.EnableCommit
   
    Set m_Results = m_Cmd.execute(lAffected, , m_Options)
       
    If Not m_Results Is Nothing Then
        If m_Results.State = adStateClosed Then
            GoTo errorHandler
        End If
   
        ' disconnect recordset before returning, only need to do this if command state is valid
        ' throw away closed recordsets (they are valid for update-only queries)
        If m_Results.State <> adStateClosed Then
            If m_DisconnectRS = True Then
                m_Results.ActiveConnection = Nothing
                m_Con.Close
            End If
        End If
    ElseIf m_Options <> adExecuteNoRecords Then
        ' no results from the command, need to check the command state to see if the command execute OK
        IIf lAffected < 0, execute = False, execute = True
        handleErrors
    Else
        execute = True
    End If

    ' close connection before returning
    Set m_Con = Nothing
   
    execute = True

Exit Function
   
errorHandler:
   
    'cleanup
    Dim sError As String
    errorString = handleErrors
    ' if no errors found, try the general error message
    If errorString = "" Then
        errorString = Error
    End If
    Set m_Con = Nothing
   
    execute = False
   
End Function

***********************************************************************************************

This is the business object calling the query component

Public Function CategoryList(errorString As String) As String

' Returns a list of all available Categories
Dim domDoc As MSXML.DOMDocument
Dim sqlInt As CFS_Query.Interactor_v1
Dim results As ADODB.Recordset
Dim XML As String
Dim spParams() As Object
Dim outParams() As Variant
   
    On Error GoTo ErrorRtn

' Setup the Query object and run the query
    errorString = ""
   
' Setup the Query object and run the query
' create the SQL interactor object, which will do the db stuff for us
    Set sqlInt = GetObjectContext.CreateInstance("CFS_Query.Interactor_v1")
 
    Set results = sqlInt.execSP(errorString, "CFS_Client_selCategories", spParams, outParams)
   
    If Not results Is Nothing Then
' If the query returned results, then save the results as an XML string
        Set domDoc = CreateObject("MSXML.DOMDocument")
        results.Save domDoc, adPersistXML
        CategoryList = domDoc.documentElement.XML
        results.Close
        errorString = ""
    ' Do house cleaning
        Erase spParams
        Erase outParams
        Set results = Nothing
        Set sqlInt = Nothing
        Set domDoc = Nothing
       
    ' Commit Transaction
        GetObjectContext.SetComplete
    Else
    ' Do house cleaning
        CategoryList = ""
        Erase spParams
        Erase outParams
        Set results = Nothing
        Set sqlInt = Nothing
        Set domDoc = Nothing
       
    ' Commit Transaction
        GetObjectContext.SetAbort
    End If

ExitRtn:
   
    Exit Function
   
ErrorRtn:
' Do house cleaning
    Erase spParams
    Erase outParams
    Set results = Nothing
    Set sqlInt = Nothing
    Set domDoc = Nothing

' Return Error String
    CategoryList = ""
    errorString = Error

' Abort Transaction
    GetObjectContext.SetAbort
    Resume ExitRtn

End Function
0
 

Author Comment

by:kellykln
Comment Utility
Thanks for your help. Unfortunately I have to push off dealing with this issue for about 2 weeks. I hope then to be able to devote myself to getting this up and running. You'll hear from me then. Please don't give up on me. Thanks
0
What Should I Do With This Threat Intelligence?

Are you wondering if you actually need threat intelligence? The answer is yes. We explain the basics for creating useful threat intelligence.

 
LVL 18

Expert Comment

by:mdougan
Comment Utility
OK, I'll wait for your next post.....
0
 

Author Comment

by:kellykln
Comment Utility
Ok, I'm back thanks for waiting. I've done a lot of reading and research since my last post. Your info was useful and I have worked out many issues. Just to remind you I am creating my objects from an ASP page. When I do a setabort, I wipe out my session variable that I use to reload info when there is an error on the page. Something like
if not emty(Master.DataErrors)then
  set session("tempmaster")=Master
  Master.DisableCommit  'does a getobjectcontext.setabort
  Cleanup()
  Response.Redirect Request("SCRIPT_NAME") & "?submit=" & UPDATEERRORTEXT
end if

When I tried using disablecommit the transaction wasn't rolled back.
 How should I be doing this.

Thanks
0
 
LVL 18

Expert Comment

by:mdougan
Comment Utility
I've only used these transactions within "stateless" calls to my object's methods.  So,

X.DoUpdate....


Public Sub DoUpdate()
.....

...
if Error then
GetObjectContext.SetAbort
Else
0
 
LVL 18

Expert Comment

by:mdougan
Comment Utility

I've only used these transactions within "stateless" calls to my object's methods.  So,

X.DoUpdate....


Public Sub DoUpdate()
.....

...
if Error then
  GetObjectContext.SetAbort
Else
  GetObjectContext.SetComplete
End if
End sub


So, in the object, the transaction begins when you call the method, and it ends at the end of the method.

In your case, it looks like you're trying to make the transaction span several calls to your middle-tier component and I don't know if it can work that way or not.

Also, make sure that you are not doing any begin trans, Commit Trans inside of any database stored procs, as all of that is done outside of the transaction manager, and will really foul up the COM+ transaction.

Generally, you should try to design your components to be completely stateless, because then they are scaleable.
0
 

Author Comment

by:kellykln
Comment Utility
Are you saying that every method that I have in my object will begin a trans and then end a trans when it returns to the client? I thought that the transaction begins when I create the object and ends when the object is destroyed. Is that not correct? That is definately the assumtion I am working under. The reason that I am trying to save state is in case of an error on a field I want to be able to recall the page with out wiping out all the other fields that were ok. Do you have any ideas for doing this in ASP?
Thanks
0
 
LVL 18

Expert Comment

by:mdougan
Comment Utility
Well, a quick read over this might shed some light on this:

ftp://ftp.microsoft.com/bussys/viper/docs/faq%20mts%20databases%20and%20transactions.htm#_Transactions

It sounds like a transaction could span multiple calls to the object, but a lot depends on how the object is coded.

First, if your component is marked as Requires Transaction, then I think that it will try to use an existing transaction on each call to the instance of the component, but if it can't find an existing transaction it will start one up.  I think that there is an option for Requires New Transaction, which will cause a new transaction for each call....

In our application, since we had designed it to be stateless, every method that did any database querying or updating called another component to open an ADODB connection.  According to this faq, that will cause the component to start a transaction unless there is already one active (we might also have had Requires New Transaction set).  So, for all practical purposes, we coded under the assumption that each method call was a new transaction, and each time we returned from the method, we voted to either complete or abort it.

I know that there was another reference that I'd read on transactions from the microsoft site that pretty clearly explained it all, but I'd have to root around some more to find it....
0
 

Author Comment

by:kellykln
Comment Utility
Thanks for your help. Sorry for the delay in giving you your points. I actually thought that I gave you the points some time ago but I guess it didn't go through.
0
 
LVL 18

Expert Comment

by:mdougan
Comment Utility
No problem, good luck getting your component to work.
0

Featured Post

Highfive Gives IT Their Time Back

Highfive is so simple that setting up every meeting room takes just minutes and every employee will be able to start or join a call from any room with ease. Never be called into a meeting just to get it started again. This is how video conferencing should work!

Join & Write a Comment

Introduction While answering a recent question (http://www.experts-exchange.com/Q_27402310.html) in the VB classic zone, I wrote some VB code in the (Office) VBA environment, rather than fire up my older PC.  I didn't post completely correct code o…
Enums (shorthand for ‘enumerations’) are not often used by programmers but they can be quite valuable when they are.  What are they? An Enum is just a type of variable like a string or an Integer, but in this case one that you create that contains…
As developers, we are not limited to the functions provided by the VBA language. In addition, we can call the functions that are part of the Windows operating system. These functions are part of the Windows API (Application Programming Interface). U…
This lesson covers basic error handling code in Microsoft Excel using VBA. This is the first lesson in a 3-part series that uses code to loop through an Excel spreadsheet in VBA and then fix errors, taking advantage of error handling code. This l…

771 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

7 Experts available now in Live!

Get 1:1 Help Now