Click to return to the Server Technologies home page    
Web Workshop  |  Server Technologies

Developing a Visual Basic Component for IIS/MTS


Troy Cambra
Support Engineer
Microsoft Corporation

June 19, 1998

Contents
Introduction
Why Write Components?
Why Use Microsoft Transaction Server?
Why Use Visual Basic?
Using the MTS Objects
Using the ASP Intrinsic Objects
Performance and Process Isolation Tips
Tips for Building MTS Components
Debugging Tips

Introduction

The integration of Microsoft® Internet Information Server (IIS) 4.0 with Microsoft Transaction Server (MTS) 2.0 yields a powerful server environment. The integration provides process isolation, transactional Web page support, and a rich framework for building components into IIS. This article describes how to build components that take advantage of the new features in IIS and MTS, and provides an extensive list of tips on building components, performance and process isolation, and debugging.

This article assumes that you have a solid working knowledge of Internet Information Server, Active Server Pages (ASP), Microsoft Transaction Server, Visual Basic® 5.0 and the Component Object Model (COM) Non-MSDN Online link. You can install IIS 4.0 and MTS 2.0 from the Windows NT® Option Pack, which is available from http://www.microsoft.com/windows/downloads/default.asp Non-MSDN Online link.

Why Write Components?

With ASP, it is entirely possible to build rich applications using only the features provided in IIS 4.0. You can access context, perform transactions, access databases, and so forth, without having to write your own components. So why, you might ask, would you want to write components? The three most compelling reasons are:

Performance and scalability. Executing native code is faster and typically less memory-intensive than interpreting scripts.

Business logic encapsulation. This creates better code organization, and also improves code debugging, distribution, and upgrading. In addition, you can reuse components within and across your applications.

Separation of data and user interface (UI). Components make it easy to add a fresh UI to applications without having to touch business logic and data. This is important in a fast-changing Web-based application.

For more information on components, see:

Agility in Server Components

ASP Components Catalog

Enabling the Use of Out-of-Process Components in ASP Using the Metabase

Web Workshop's Component Development area (on the home page, click the "show contents" link.)

TopBack to top

Why Use Microsoft Transaction Server?

If you are using distributed transactions on your site, MTS is the obvious choice. However, even if you don't use transactions or databases, you will find that MTS is a very useful environment in which to run your components. MTS provides object brokering and run-time services as well as transaction monitoring. Consider MTS if your components require any of the following:

For more information on MTS, see:

IIS 4.0 and MTS 2.0: Technology for the Web Non-MSDN Online link

TopBack to top

Why Use Visual Basic?

Any language capable of creating apartment-threaded in-process COM components is suitable for developing MTS components. The choice of language should be primarily based on your familiarity with it and its suitability for the task. Familiarity with a language is the most important factor. Knowing how to use a language correctly and to its greatest efficiency and power can determine, in large part, the success of a project. However, suitability for the task also needs to be considered. For example, if raw speed were of utmost importance, a lower-level language such as C++ would be the better language to use.

Visual Basic is a solid tool for most applications. It is mature, stable, and proven. It is a sound rapid application development (RAD) tool, but has enough flexibility to get down to the underlying APIs and enable the developer to tweak performance or provide advanced features.

TopBack to top

Using the MTS Objects

An MTS component is simply an ActiveX® DLL project. Any class in an ActiveX DLL project can potentially be an MTS component. However, to take full advantage of the MTS features, it is necessary to use the MTS programmer's interface. The first step in doing this is to reference the Microsoft Transaction Server Type Library. This enables access to the MTS objects, the most important of which is ObjectContext. ObjectContext provides access to your object's MTS context, which provides the core MTS functionality.

In MTS, a logical thread of work is called an activity. An activity can span multiple objects and multiple method calls to any object. An activity's scope spans from the first call from the base client until either the root object calls ObjectContext.SetComplete and exits the method or the base client releases the root object. Although it is possible to have activities that span multiple method calls on any object in the activity, it is best to limit it to a single method call per object if possible. If this is done, you would simply obtain context in the method and release it before exiting. The code would look something like this:

Public Sub SomeMethod()

    Dim objCtx as ObjectContext

    Set objCtx = GetObjectContext
    If objCtx Is Nothing Then _
        Err.Raise 91 'error if failed

    'Perform work.
    'VB will release the
    'reference upon method exit

End Sub
  

If the activity will span several method calls and the calls all require access to the object's context, it is generally a good idea to store it in a class member variable. It is bad form to obtain and release the object's context in the Initiate and Terminate events. These events should be considered the constructor and destructor of the class, so context should not be assumed to be available. This is not always the case for Visual Basic, but should be followed for consistency with all other languages, for better error handling and reliability.

The best way to obtain and release context is to use the ObjectControl interface methods. ObjectControl provides a solid mechanism for context handling, and also allows the component to take advantage of object pooling when it becomes available in future releases.

Code using the ObjectControl interface methods to handle context in the class module might look something like this:

Option Explicit
'Private member variables
Private m_objCtx As ObjectContext

'Public class functions
'Implementation of ObjectControl interface
Implements ObjectControl

Private Sub ObjectControl_Activate()
    Set m_objCtx = GetObjectContext
    If m_objCtx Is Nothing Then _
        Err.Raise 91 'error if failed
End Sub

Private Function ObjectControl_CanBePooled() As Boolean
    ObjectControl_CanBePooled = False  'don't pool for now
End Function

Private Sub ObjectControl_Deactivate()
    Set m_objCtx = Nothing 'release
End Sub

'Actual public class functions
'TBD
  

Once the context is obtained, you can use it to control transaction outcome and object recycling, to query context properties, and to perform various functions. The following function shows a typical use of the context properties and methods:

  Public Function BasicFunction(ByVal blnAudit As Boolean) As Variant

      'sample MTS function
      Dim strAuditString As String
      Dim strErrString As String

      'This function will optionally log auditing information
      On Error GoTo ErrHand
      If blnAudit Then
          strAuditString = "Method: BasicFunction " & vbCrLf & _
              "Thread ID: 0x" & Hex(App.ThreadID) & vbCrLf & _
              "Transactional: " & m_objCtx.IsInTransaction & vbCrLf & _
              "DirectCaller: " & m_objCtx.Security.GetDirectCallerName & vbCrLf & _
              "OriginalCaller: " & m_objCtx.Security.GetOriginalCallerName
          Call App.LogEvent(strAuditString, vbLogEventTypeInformation)
      End If

      'Perform the actual work
      'Actual work TBD

      'Finish up
      BasicFunction = "Something Wonderful Happened"
      'Commit the transaction
      m_objCtx.SetComplete
      Exit Function

  ErrHand:
      'log the actual error
      strErrString = "Error Code: 0x" & Hex(Err.Number) & vbCrLf & _
          "Error Description: " & Err.Description & vbCrLf & _
          "Error Source: " & Err.Source
      On Error Resume Next
      Call App.LogEvent(strErrString, vbLogEventTypeError)
      'Abort the transaction - if one
      m_objCtx.SetAbort
      'Raise a friendly error to the caller. Minimizing the number of
      'errors returned to the client makes programming at that level
      'easier. For example, you typically have two types of errors
      'recoverable and non recoverable. You could simply send back
      'one or the other so the caller can easily make a decision
      'about how to handle it.
      Call Err.Raise(vbObjectError + 1001, _
          "Basic Component", _
          "Basic Error. Please contact the system administrator")

  End Function

    

 

TopBack to top

Using the ASP Intrinsic Objects

The ASP intrinsic objects are available within MTS objects through the object's context. Making use of the ASP intrinsic objects from within an MTS object is simple and can be quite effective. For example, if you have an ASP page that is building a complicated HTML page using the Response object, you can encapsulate the code in a component that will execute much faster, and be much easier to debug. Here is a sample function from a component and a sample ASP script that calls it:

The Component

  Public Function ASPVarUse() As Variant

      'sample mts function
      Dim strErrString As String
      Dim objApplication As Object
      Dim objResponse As Object
      Dim objRequest As Object
      Dim objServer As Object
      Dim objSession As Object
      Dim objItem As Object

      'Get the IIS intrinsic objects
      If m_objCtx.Count < 5 Then _
          Call Err.Raise(vbObjectError + 1002, "Basic Function", _
          "This object is meant to be called by ASP pages only")

      Set objApplication = m_objCtx.Item("Application")
      Set objResponse = m_objCtx.Item("Response")
      Set objRequest = m_objCtx.Item("Request")
      Set objServer = m_objCtx.Item("Server")
      Set objSession = m_objCtx.Item("Session")

      'Access the ASP objects
      objResponse.Write "<p> Outputting from the MTS component </p>"
      Call objServer.HTMLEncode("The paragraph tag: <P>")
      objApplication("AppTest") = "Application Test"
      objSession("SessionTest") = "Session Test"
      For Each objItem In objRequest.QueryString("RequestTest")
          objResponse.Write objItem & "<BR>"
      Next

      ASPVarUse = "Successful Test"
      'Commit the transaction
      m_objCtx.SetComplete
      Exit Function

  ErrHand:
      'log the actual error
      strErrString = "Error Code: 0x" & Hex(Err.Number) & vbCrLf & _
          "Error Description: " & Err.Description & vbCrLf & _
          "Error Source: " & Err.Source
      On Error Resume Next
      Call App.LogEvent(strErrString, vbLogEventTypeError)
      'Abort the transaction - if one
      m_objCtx.SetAbort
      'Raise a friendly error to the caller.
      Call Err.Raise(vbObjectError + 1001, _
          "Basic Component", _
          "Basic Error. Please contact the system administrator")

  End Function

  

The ASP Script

  <%
  Dim objTest

  On Error Resume Next
  'Create the MTS object
  Set objTest = Server.CreateObject("PMTSIISVB.IMTSIISVB")
  If Err.Number <> 0 Then
      Response.Clear
      Response.Write "<p> " & Err.Description & " </p>"
      Response.End
  End If

  Response.Write "<p> Starting Test </p>"
  Response.Write "<p> ASPVarUse Return: " & objTest.ASPVarUse() & " </p>"
  If Err.Number <> 0 Then
      Response.Clear
      Response.Write "<p> " & Err.Description & " </p>"
      Response.End
  End If
  Response.Write "<p> Session Variable = " & Session("SessionTest") & " </p>"
  Response.Write "<p> Application Variable = " & Application("AppTest") & " </p>"
  Response.Write "<p> Ending Test </p>"
  %>
    

TopBack to top

Performance and Process Isolation Tips

Balancing fault tolerance, programming ease, and performance can be tricky. There are countless things that can be done to fine-tune performance. I've put together a list of some of the more common things you can do or avoid doing to improve performance. For a more detailed discussion of IIS and ASP performance tuning, please see the Internet Information Server Resource Kit Non-MSDN Online link.

It is important to fully analyze the entire server. You might think that running every ASP page in every application or virtual Web site in-process would create better performance than running some or all out-of-process. This is not always true. There are literally thousands of variables in the performance equation. The only way to be sure of optimum performance is to plan performance testing into your application development cycle. For example, I have had some larger Web sites actually see a substantial performance increase by moving applications out-of-process.

If process isolation between the application and the IIS server is important, run the IIS application in a separate process, but use a library package for the MTS components. This is typically a good tradeoff. A problem in a component could bring down the application, but even if they were in two different processes, the problem would probably propagate back to the ASP in some form anyway. Further, this configuration will typically outperform running the ASP in the IIS process and the MTS components in a server process.

ASP pages are going to use the IDispatch interface of the component. This causes two calls per method call. If you use early binding in your intermediate component, you can limit the network calls to one call per method. Further, you can design your intermediate component to hold references, cache data, and so forth. This can minimize the amount and frequency of data accessed over the network.

TopBack to top

Tips for Building MTS Components

Here is a list of suggestions for building MTS components for use in IIS. This is not a set of rules, but flexible guidelines.

TopBack to top

Debugging Tips

Visual Basic has a strong interpreted debugger in the integrated development environment (IDE). Unfortunately, this doesn't help when you're trying to run in the MTS environment. When running in MTS, it is necessary to debug the executable code directly. Although Visual Basic is not capable of debugging MTS components directly, it can generate debugging symbols that can be used in the Visual C++® debugger (Developer Studio). Although limited compared to the Visual C++ debugging functionality, this is still one of the best ways to debug Visual Basic components in MTS.

While the Visual C++ debugger is recommended because it's easy to use, the latest WinDGB.EXE can also be used to debug Visual Basic components. WinDBG is more difficult to use, but is in some ways more powerful. It is also free. The latest version is available from http://msdn.microsoft.com/developer/sdk/windbg.htm.

Any language that uses a large run-time or virtual machine (such as Visual Basic or Java) can make tracking bugs more difficult, because there's a lot going on below the level at which you are working. Further, the use of libraries that wrap underlying APIs such as ActiveX Data Objects (ADO), Remote Data Objects (RDO), and Oracle Power Objects can further complicate things. It would be nice to think that they are bug-free, but the reality is that they are not -- and finding those bugs without source code and symbols is very difficult. Finally, the run-times and wrappers can do things that are programmatically convenient but cause performance or functionality problems in the MTS environment. One such example is the querying of the database system tables that many of the data-access objects perform. This can make programming easier, but can also cause serious blocking in the database.

It is not impossible to properly debug Visual Basic components, but it is a little more difficult than some other kinds of debugging. Extra care must be taken to assure robustness. I typically follow the these guidelines when building and debugging an MTS component in Visual Basic:

  1. Build the component in the Visual Basic IDE with the following registry key set:
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Transaction Server\Debug\RunWithoutContext
      

    This allows the code to run as if it were in MTS. See the "Debugging Visual Basic MTS Components" help topic in the MTS help file for details on this setting.

  2. Once it is running in the IDE as well as possible, compile to native code with optimizations off and Create Symbolic Debug Info on. This will create a component that can be debugged in the Visual C++ 5.0 debugger or WinDBG. See Knowledge Base article Q172869 on the Microsoft Technical Support site Non-MSDN Online link for further details.

    Use tracing and assert "macros" throughout the code. The built-in Visual Basic support for debugging will not work when using the Visual C++ debugger. The following debugging functions can be very useful when debugging in this manner:

    #If DEBUGGING Then
        'API Functions
        Public Declare Sub OutputDebugStringA _
            Lib "KERNEL32" ( _
            ByVal strError As String)
    
        Public Declare Function MessageBoxA _
            Lib "USER32" ( _
            ByVal hwnd As Long, _
            ByVal lpText As String, _
            ByVal lpCaption As String, _
            ByVal uType As Long) As Long
    
        Public Declare Sub DebugBreak Lib "KERNEL32" ()
    
        Public Declare Function IsDebuggerPresent Lib "KERNEL32" () As Long
    
        'API Constants
        Private Const API_NULL As Long = 0
        Private Const API_FALSE As Long = 0
        Private Const API_TRUE As Long = 1
        Private Const MB_ICONERROR As Long = &H10
    
        Private Const MB_SERVICE_NOTIFICATION As Long = &H200000
    
        Private Const MB_YESNO As Long = 4
        Private Const IDYES As Long = 6
        Private Const IDNO As Long = 7
    
        'Other Constants
        Private Const ASSERT_DIALOG_STRING As String = _
    "The Component has thrown an assert." _
    & vbCrLf & "Press Yes to debug the application." _
    & vbCrLf & "Press No to continue running the application"
        Private Const ASSERT_DIALOG_TITLE As String = "Component Assert"
        Private Const DEBUG_DIALOG_TITLE As String = "Component Debug Message"
    
    #End If
    
    'Exposed functions
    Public Sub DebugPrint(ByVal strError As String)
    #If DEBUGGING Then
        Call OutputDebugStringA(strError)
    #End If
    End Sub
    
    Public Sub DebugMessage(ByVal strError As String)
    #If DEBUGGING Then
        Dim lngReturn As Long
    
        lngReturn = MessageBoxA(API_NULL, strError, _
                                DEBUG_DIALOG_TITLE, _
                                MB_ICONERROR Or MB_SERVICE_NOTIFICATION)
    #End If
    End Sub
    
    Public Sub DebugAssert(ByVal vntExpr As Variant)
    #If DEBUGGING Then
        Dim lngReturn As Long
        Dim blnAssert As Boolean
    
        On Error GoTo ExecAlways
        blnAssert = True
        Select Case VarType(vntExpression)
        Case vbEmpty
            blnAssert = True
        Case vbNull
            blnAssert = True
        Case vbString
            If vntExpr = vbNullString Then
         blnAssert = True
    Else
        blnAssert = False
    End If
        Case vbObject
            If vntExpr Is Nothing Then
        blnAssert = True
    Else
        blnAssert = False
    End If
        Case vbError
            blnAssert = True
        Case vbBoolean
            blnAssert = Not vntExpr
        Case vbDataObject
            If vntExpr Is Nothing Then
        blnAssert = True
    Else
        blnAssert = False
    End If
        Case Is > vbArray
            blnAssert = False
            On Error GoTo ErrArray
            lngReturn = UBound(vntExpr) 'assert if empty
            On Error GoTo ExecAlways
        Case Else 'numeric, byte
            If vntExpr = 0 Then
        blnAssert = True
    Else
        blnAssert = False
    End If
        End Select
    
    ExecAlways:
        If blnAssert = True Then
            If IsDebuggerPresent = API_TRUE Then
                Call DebugBreak
            Else
                If IDYES = MessageBoxA(API_NULL, ASSERT_DIALOG_STRING, _
                                ASSERT_DIALOG_TITLE, _
                                MB_YESNO Or MB_ICONERROR Or MB_SERVICE_NOTIFICATION) Then
                    Call DebugBreak
                End If
            End If
        End If
    
        Exit Sub
    
    ErrArray:
        blnAssert = True
        Resume Next
    
    #End If
    End Sub
    
    
        

    Use the DebugAssert() "macro" throughout the project code. The other two functions are less useful, but can be useful when trying to track a stress-related bug. When building a debug version, define a conditional compile argument as DEBUGGING=-1, and compile. When an assert is hit, the debugger should break the application. When compiling for release code, set the conditional compile argument to DEBUGGING=0 or remove it completely.

    The lack of true macro capability in Visual Basic means that even in the release code there is still a method call for each of the above debug calls. The method call does nothing, but there is still some overhead in making the call. If performance is of utmost concern, it is possible to conditionally compile the debug calls so that they are not part of the release code:

    #If DEBUGGING Then
      DebugAssert(SomeValue)
      #End If
      
  3. Test extensively with the debug code. Often, stress will uncover many bugs that single-user and unit testing do not catch.
  4. Run the same testing cycle on the release code. Sometimes removing the debugging code can unmask subtle timing problems. Finding problems of this type must be taken on a case-by-case basis. I have yet to find a technique that works for all cases -- or even most cases.
  5. Always check the event log. MTS, IIS, and ASP log many error conditions. It can often be quite helpful in tracking down many types of errors.

Sometimes there are bugs that the above simply does not catch. Tracking down hangs and performance problems is always fun. There are two things to look at first when doing so: CPU utilization and database blocking. If CPU utilization is normal on both the MTS server and the database server, blocking in the database is often the cause. Finding the underlying cause typically requires looking at the database and ODBC logs and traces. If the CPU utilization is peaking on one machine, start looking there for bottlenecks. There is no simple solution to this problem. You have to look at everything: it could be disk utilization on the database server, an indication that the objects are too large, simply an underestimate of necessary server hardware, and so forth -- all a little beyond the scope of this paper.

Tracking down sporadic exceptions is yet another time-consuming and less than enjoyable experience. The MTS, ASP, or COM environments typically handle exceptions by trapping the error, logging the event, and cleaning up as necessary. It is possible to catch these by running the application with a debugger attached. Although you can debug these, you can also simply call product support. Be prepared to install symbols, debuggers, and even RAS on the machine.

Debugging components from ASP pages adds yet another layer of difficulty. However, the Script Debugger Non-MSDN Online link and the above debugging code can make it much easier to debug problems right in the application process. Copying the ASP code into a Visual Basic project and creating a client executable that can then be used to debug the component can be a useful (and sometimes necessary) trick. This can be especially useful when developing ASP scripts and Visual Basic components. Overall, Visual Basic is still the best editor for creating and testing Visual Basic code.



Back to topBack to top

Did you find this material useful? Gripes? Compliments? Suggestions for other articles? Write us!

© 1999 Microsoft Corporation. All rights reserved. Terms of use.