Building a Better Mousetrap for COM+ (MTS)

couple of months back, I took it upon myself to clean up the VB Sample Bank code. The original goal was to modify the code just enough to use the objects in scalability tests to compare it against pooled objects written in VC++ and Delphi 5. However, once I opened the code and put fingers to keyboard I couldnt help but take a few more liberties, as the original source had a number of opportunities to make it better. For anyone having trouble interpreting PC double-speak – the code was a poor excuse for an MTS demo project. Of course, every module does contain a disclaimer and as the old saying goes, You get what you pay for.

Early last month I dug up the code modified in my scalability tests of the Sample Bank application for Ron Jacobs, the Platform SDK Manager at Microsoft. While I did provide some documentation of the changes, I had to allow the modified code to speak for itself in several areas. Time did not permit me to properly address the original issues, or the code changes made to correct them. As submitted, the code is much better than the original?and perhaps it will influence VB samples included in future releases of the Platform SDK. However, in the mean time I feel it would be beneficial to discuss the changes at length, and share my commentary with as broad an audience as possible.

Issues with the Original Code

The code for the original project was taken from the January 2000 Platform SDK. As no VB project was included with the OPBankSample, I located the equivalent code from the MTS samples from the Platform SDK. What I found was a project that was grossly deficient for deployment under MTS – for example:

  • The ObjectControl interface was not implemented and the associated event procedures were not included. While this is a documented “best practice” by Microsoft Support, the technique is rarely used. Most MSKB articles and Platform SDK samples do not follow the recommendation.

  • All public classes were marked ‘0 – NotAnMTSObject’, and yet GetObjectContext was used to call SetComplete and SetAbort. While this does work to produce a stateless object, it is indicative of sloppy coding practices. In addition, to be equivalent with other Account project samples, most classes need to support transactions.

  • Failure to properly clean up resources; in many cases, the only effort at explicit clean up was made during error processing.

  • Extremely poor error handling. The UpdateReceipt.Update and Account.Post methods contained completely ineffective error handlers, demonstrating that the author lacked understanding of how VB transitions code flow during error processing. Note: this style of error handling leads to the double-snake error (“Method ‘~’ of object ‘~’ failed.”).

  • Multiple code paths and exit points in a single procedure. While VB6 does not include a structured error handling mechanism (such as try..catch..finally), the equivalent can be implemented – though it requires a bit more code.

  • Declaration of ADO objects with the New keyword. While this only has a marginal impact on performance, it is generally considered to be a bad practice. However, given the author’s error handling implementation, this is the only viable technique for minimizing secondary faults during error processing.

  • Repetitive declaration of commonly used constants within each class.

  • Failure to implement the interface used by OPBank.exe. As this was an unknown during original construction, this is not truly a fault with the original code. I could have modified the test harness (OPBank project) to accept the interface as coded. However, I feel it is a better practice to provide an immutable interface, and in this case one that fits the model for the test harness.

Some of the issues taken with the original project code come down to coding style. On the other hand, others will result in resource leaks under load or in the code not functioning as intended. The latter point is the result of totally ineffective error handling due to a failure to understand the impact of VB error handling semantics.

The Revised Code 

The revised project code demonstrates techniques for effective error handling, which as a side effect will eliminate preventable resource leaks. In addition, the objects can be debugged with object context under Windows NT 4 SP4 (or higher) and Windows 2000. Each of the issues mentioned above is addressed individually below, with supporting reference links to Microsoft documents following the article.

Implementing the ObjectControl Interface

It is considered a best practice for developing COM+ (MTS) object with VB to obtain and release context via the ObjectControl interface. Specifically, this should be performed using the ObjectControl event procedures; Class_Initialize and Class_Terminate should not be used. Prior to including the code, the appropriate library reference should be set::

  • MTS: Microsoft Transaction Server Type Library (MTxAS.DLL)

  • COM+:  COM+ Services Type Library (COMSVCS.DLL)

Example of implementing the ObjectControl interface:

Implements ObjectControl

Private m_ctxObject As ObjectContext

Private Sub ObjectControl_Activate()
  Set m_ctxObject = GetObjectContext
End Sub

Private Function ObjectControl_CanBePooled() As Boolean
  ObjectControl_CanBePooled = False
End Function

Private Sub ObjectControl_Deactivate()
  Set m_ctxObject = Nothing
End Sub

A note regarding debugging – unless you are developing on a Windows 9x machine or using VB5, disregard instructions contained in Q188919 related to Registry changes to run without context. Instead, you should correct your development environment if you experience problems, as the recommendations are no longer required for NT 4.0 SP4 (or higher) and Windows 2000.

Appropriate MTSTransactionMode Settings

The original implementation of the code could have executed without transaction support. While this does provide a very good performance boost, it is inappropriate for this implementation. It is true that you can override the settings once the components are installed in a package, but this leads to deployment errors that can be difficult to detect.

As the purpose of my testing was to compare VB objects relying on JIT activation with VC++ objects that were pooled, it was critical to avoid introducing an apples to oranges comparison of the transaction settings. To ensure that the results were measuring the expected behavior, the settings were changed as follows:

  • IAccount – ‘2 – RequiresTransactions’

  • ICreateTable – ‘1 – NoTransactions’

  • IGetReceipt – ‘3 – UsesTransactions’

  • IMoveMoney – ‘2 – RequiresTransactions’

  • IUpdateReceipt – ‘4 – RequiresNewTransaction’

Implementing Effective Error Handling

VB and its associated developer community are often slammed by developers using other languages (VC++ programmers in particular). VB is often referred to as a “tinker toy” language – and I’ve heard much more harsh criticism over the years. Perhaps the single most significant reason is that a large percentage of VB developers fail to incorporate robust error handling into the applications, with the end result being errors are not appropriately trapped and handled.

Error handling in VB has always been a poorly covered topic, and proper treatment is not given in code samples by Microsoft. The VB6 documentation does explain the behavior to expect, but the text is obviously confusing as I continue to receive requests from participants in the VB related newsgroups on this issue. I recently wrote code to demonstrate the behavior to one gentleman who quoted a documentation snippet that seemed to contradict my experience. To quote from the VB documentation:

An “enabled” error handler is one that is turned on by an On Error statement; an “active” error handler is an enabled handler that is in the process of handling an error. If an error occurs while an error handler is active (between the occurrence of the error and a Resume, Exit Sub, Exit Function, or Exit Property statement), the current procedure’s error handler can’t handle the error. Control returns to the calling procedure.

As you should note, there is no mention of using a GoTo statement to resume execution at another point within a procedure while an error handler is active. In addition, one very important point is missing that clarifies the intent of the quoted documentation – you cannot use another On Error statement within an active error handler to trap errors that subsequently occur. GoTo is not a substitute for Resume – in order to change the label for the error handler in effect, it is first necessary to use a Resume to disable the active error handler; at this point an On Error statement will function as intended.

Example of an ineffective error handler:

Public Function Ineffective() As String
  On Error GoTo ErrorCreateTable

  Dim bFirstTimeThru As Boolean
  bFirstTimeThru = True

TryAgain:
  ' conditionally force an error
  If bFirstTimeThru Then
    bFirstTimeThru = False
    Err.Raise -1, , "Code transitions to ErrorHandler label"
  End If

  ' next statement has no effect if an
  ' error handler is already active

  On Error GoTo ErrorHandler

  ' alternative with same effect; uncomment to test
  ' On Error Resume Next

  ' force a secondary error
  Err.Raise -2, , "Code flow returns to caller; no return value set."

  Ineffective = "Exit with return value."
  Exit Function

ErrorCreateTable:
  ' following statement is totally ineffective
  On Error GoTo ErrorHandler

  ' do some additional work

  ' does not disable active error handler, but
  ' simply continues execution at label
  GoTo TryAgain

ErrorHandler:
  Err.Raise -3, , "As implemented, label is unreachable."
End Function

The demonstration of an ineffective error handling technique uses the labels and basic logic from Account.Post, though the code has been streamlined to clarify the point. There are a couple of different ways that the code could be modified to correct the behavior. I will demonstrate one based on what I consider to be the “proper” way, as it is my practice to only permit a single point of exit, thus ensuring that any clean up processing is always executed.

The next code sample will demonstrate the changes necessary to produce the original intent. Also, note how the code flow leaves but a single point of exit for the procedure. By implementing this as a standard practice in your procedures you can effect the equivalent of VB7’s structured error handling (albeit, the code is not as clean).

Example modified to demonstrate effective error handling:

Public Function Effective() As String
  On Error GoTo ErrorCreateTable

  Dim lngError As Long
  Dim strError As String 
  Dim bFirstTimeThru As Boolean
  bFirstTimeThru = True

TryAgain:
  ' conditionally force an error
  If bFirstTimeThru Then
    bFirstTimeThru = False
    Err.Raise -1, , "Code transitions to ErrorHandler label"
  End If

  ' next statement is now effective

  On Error GoTo ErrorHandler

  ' alternative with same effect; uncomment to test
  ' On Error Resume Next

  ' force a secondary error
  Err.Raise -2, , "Code flow returns to caller; no return value set."

ExitPoint:
  On Error Resume Next
  ' perform all cleanup, such as object destruction; explicit
  ' cleanup of all objects can prevent the double-snake error
  If lngError <> -3 Then  ' explicit test for demonstration
  'If lngError <> 0 Then  ' normal test value is 0 (no error)
    On Error GoTo 0
    Effective = vbNullString
    Err.Raise lngError, , strError
  Else
    Effective = "Exit with return value - now reachable, if no errors."
  End If
  Exit Function

ErrorCreateTable:
  ' following statement is totally ineffective
  On Error GoTo ErrorHandler

  ' do some additional work

  ' disable active error handler and resume
  ' processing at designated label
  Resume TryAgain

ErrorHandler:
  lngError = Err.Number - 1
  strError = Err.Description
  Resume ExitPoint
End Function

I believe that the above procedures should clearly demonstrate the underlying behavior of VB error handling. My intent for this article was not to get completely wrapped up in error handling, but I feel that it is an important topic for COM+ (MTS) development that is often given too little focus. For a more in-depth look at real world code, compare the Post and Update methods of the sample projects that come with this article.

ADO Variable Declarations and Instantiation

The ADO object declarations with “As New” in the original code caught my eye. This is not only a bad practice for ADO, but for declaring objects in general. Using “Dim .. As New ..” incurs additional processing every time that the object is referenced, creating the object if it does not exist. A more efficient approach is to use “Dim As ..”, and instantiate the object in the procedure body.

Example of ADO object declaration and instantiation:

  Dim cn As ADODB.Connection

  ' preferred instantiation using New keyword;
  ' should be used in VB
  Set cn = New ADODB.Connection

  ' alternate instantiation using CreateObject;
  ' should *only* be used in scripting languages
  ' (such as ASP)
  Set cn = CreateObject("ADODB.Connection")

  ' instantiation via the context should *never*
  ' be performed (even under MTS 2.0)
  Set cn = GetObjectContext.CreateInstance("ADODB.Connection")

I included the latter method of instantiation because this is discussed in the newsgroups on a weekly basis. I am aware that there are MSKB articles and a number of books by noted authors that use this technique. However, any document that demonstrates this type of code simply demonstrates that the author was remiss in his research at the time of publication, as ADO is not MTS aware.

Just because you see it in print doesn’t mean it is accurate. Remember that there is an “ex” in expert, and humans have been known to make mistakes (I know I certainly have). My advice – question what you read, and write code samples to validate any point that you are unsure of. Simply mimicking code written by others does not lead to an understanding of the technology.

Avoiding Duplicate Constant Declarations

This is a rather trivial point to correct, as it simply required moving common declarations to a utility module and changing the scope from Private to Public. What raises my ire (and I am part Irish) is that the practice of duplicate declarations goes hand in glove with failing to write procedures to handle commonly performed sequences.

As all experienced developers know, duplication of code leads to long-term maintenance problems. Inevitably, a change will be made in one module that does not get fully propagated and a new bug will result. With adequate testing it will be caught before deployment, but the catch, fix, retest and release cycle still adds cost to the project.

I would contend that Microsoft should not publish sample code that demonstrates poor coding practices. This leads to a longer learning cycle for Johnny Newbie (we’ve all been there), lowers productivity, and increases software development costs. I do not expect bug free samples, but simply that the author demonstrate basic proficiency with the language of implementation.

Implementing Interfaces

Rather than modify the source code to the client application extensively, I chose to implement the interfaces exposed by the Sample Bank Account (VC version) Type Library (account.tlb). This required only minor changes to OPBank, but immediately allowed the VB objects to be instantiated. However, implementing the “immutable” interface exposed via the type library presents a problem for scripting languages, as they cannot detect the interface. To get around this issue, I simply exposed public methods that wrap the private methods.

Example of implementing an interface and exposing scripting compatible methods:

' must add account.tlb to project references
Implements CUpdateReceipt

' exposed for scripting language compatibility
Public Function Update() As Long
  Update = CUpdateReceipt_Update()
End Function

' implements the method for the "immutable" interface
Private Function CUpdateReceipt_Update() As Long
  ' code body removed for brevity
End Function

As I previously indicated, this feature was not an omission on the part of the original author. The choice to modify the code in this manner was due to personal preference, as I found it to be the shortest path to my goal – which was benchmarking comparable objects written in VB, Delphi, and VC++. These objects were used in a previous article published at vb2themax.

Conclusion

By taking a sample project and explaining both the original problems (as I saw them) and the actions taken to correct the deficiencies, I hope to eliminate some confusion for COM+ (MTS) developers that have yet to master the tools and technologies. I am active in the MTS and COM+ related newsgroups, and see the confusion and misinformation that plagues this topic.

On behalf of myself, Jimmy Nilsson and Enrico Sabbadin (as I’m sure I speak for them as well), I wish to express our most sincere thanks to Francesco Balena for allowing access to this forum. While the newsgroups are a good forum for discussion, the value of an individual’s contribution is greatly diminished because of the transient nature of the posts. Participants often miss out on relevant discussions, and many topics are rehashed all too often – sometimes several times per week.

References

[1] Platform SDK – OPBankSample
     Program FilesPlatform SDKSamplesCOM ServicesObject_Pooling
[2] Platform SDK – Account.VB
     Program FilesPlatform SDKSamplesMTS_OnlyInstallation_Verification
[3] Q170156 – INFO: VB 6.0 Readme Part 12: Transaction Server (MTS) Issues
[4] Developing a Visual Basic Component for IIS/MTS, June 1998
     by Troy Cambra, Support Engineer, Microsoft Corporation
[5] Q250309 – INFO: Do Not Use ObjectContext in Class_Initialize and Class_Terminate Events
[6] Q259382 – INFO: Microsoft Transaction Server (MTS) Knowledge Base Article Index

 

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: