Appendix B

COM Integration

WROX.COM CODE DOWNLOADS FOR THIS APPENDIX

Please note that all the code examples in this appendix are available as a part of this appendix’s code download on the book’s website at www.wrox.com on the Download Code tab.

One of the best practices in programming is to separate your application into workable and separate components — also known as business objects. This makes your applications far easier to manage and enables you to achieve the goal of code reuse because you can share these components among different parts of the same application or between entirely separate applications.

Before the introduction of .NET, many applications were using COM as a way to write business objects. If you are moving any legacy applications or aspects of these applications to an ASP.NET environment, you might find that you need to utilize various COM components. This appendix shows you how to use both .NET and COM components in your ASP.NET pages and code.

This appendix gives you an overview of how COM components can be used in ASP.NET.

COM INTEROP: USING COM WITHIN .NET

When .NET came out for the first time, Microsoft knew that every one of its legions of developers out there would be quite disappointed if they couldn’t use the thousands of COM controls that it has built, maintained, and improved over the years.

To this end, Microsoft has provided us with COM Interoperability. COM Interop (for short) is a technology that enables .NET to wrap the functionality of a COM object with the interface of a .NET component so that your .NET code can communicate with the COM object without having to use COM techniques and interfaces in your code.

Figure B-1 illustrates the Runtime Callable Wrapper (RCW), the middle component that directs traffic between the .NET code and the COM component.

FIGURE B-1

image

The Runtime Callable Wrapper

The Runtime Callable Wrapper, or RCW, is the magic piece of code that enables interaction to occur between .NET and COM. You create one RCW for each COM component in your project, and to do that, you can use Visual Studio 2012.

To add an ActiveX DLL to the References section of your project, choose Website ⇒ Add Reference or choose the Add Reference menu item that appears when you right-click the root node of your project in the Solution Explorer.

Your Interop library is created for you automatically from the ActiveX DLL that you told Visual Studio 2012 to use. This Interop library is the RCW component customized for your ActiveX control, as shown previously in Figure B-1. The name of the Interop file is simply Interop.OriginalName.DLL.

You can also create the RCW files manually instead of doing it through Visual Studio 2012. In the .NET Framework, you will find a method to create RCW Interop files for controls manually through a command-line tool called the Type Library Importer. You invoke the Type Library Importer by using the tlbimp.exe executable.

For example, to create the Interop library for the SQLDMO object, start up a Visual Studio 2012 Command Prompt from the Microsoft Visual Studio 2012 ⇒ Visual Studio Tools group within your Start menu. From the comment prompt, type the following:

tlbimp sqldmo.dll /out:sqldmoex.dll

In this example, the /out: parameter specifies the name of the RCW Interop library to be created. If you omit this parameter, you get the same name that Visual Studio would generate for you.

The Type Library Importer is useful when you are not using Visual Studio 2012 as your development environment, if you want to have more control over the assemblies that get created for you, or if you are automating the process of connecting to COM components.

The Type Library Importer is a wrapper application around the TypeLibConverter class of the System.Runtime.InteropServices namespace.

Using COM Objects in ASP.NET Code

To continue working through some additional examples, you next take a look at a simple example of using a COM object written in Visual Basic 6 within an ASP.NET page.

In the first step, you create an ActiveX DLL that you can use for the upcoming examples. Add the Visual Basic 6 code shown in Listing B-1 to a class called NameFunctionsClass and compile it as an ActiveX DLL called NameComponent.dll.

LISTING B-1: VB6 code for ActiveX DLL, NameComponent.DLL

VB

Option Explicit
        
Private m_sFirstName As String
Private m_sLastName As String
        
Public Property Let FirstName(Value As String)
  m_sFirstName = Value
End Property
        
Public Property Get FirstName() As String
  FirstName = m_sFirstName
End Property
        
Public Property Let LastName(Value As String)
  m_sLastName = Value
End Property
        
Public Property Get LastName() As String
  LastName = m_sLastName
End Property
        
Public Property Let FullName(Value As String)
  m_sFirstName = Split(Value, " ")(0)
  If (InStr(Value, " ") > 0) Then
    m_sLastName = Split(Value, " ")(1)
  Else
    m_sLastName = ""
  End If
End Property
        
Public Property Get FullName() As String
  FullName = m_sFirstName + " " + m_sLastName
End Property
        
Public Property Get FullNameLength() As Long
  FullNameLength = Len(Me.FullName)
End Property

Now that you have created an ActiveX DLL to use in your ASP.NET pages, the next step is to create a new ASP.NET project using Visual Studio 2012. Replace the HTML code in the Default.aspx file with the HTML code shown in Listing B-2. This code adds a number of textboxes and labels to the HTML page, as well as the Visual Basic or C# code for the functionality.

LISTING B-2: Using NameComponent.dll

VB

<%@ Page Language="VB" %>
        
<script runat="server">
  Protected Sub AnalyzeName_Click(ByVal sender As Object,
     ByVal e As System.EventArgs)
        
    Dim Name As New NameComponent.NameFunctionsClass()
    If (FirstName.Text.Length > 0) Then
      Name.FirstName = FirstName.Text
    End If
        
    If (LastName.Text.Length > 0) Then
      Name.LastName = LastName.Text
    End If
        
    If (FullName.Text.Length > 0) Then
      Name.FullName = FullName.Text
    End If
        
    FirstName.Text = Name.FirstName
    LastName.Text = Name.LastName
    FullName.Text = Name.FullName
    FullNameLength.Text = Name.FullNameLength.ToString
        
  End Sub
</script>
        
<html xmlns="http://www.w3.org/1999/xhtml" >
  <head runat="server">
     <title>Using COM Components</title>
  </head>
  <body>
    <form id="form1" runat="server">
      <p>
        <asp:Label ID="Label1" runat="server">First Name:</asp:Label>
         
        <asp:TextBox ID="FirstName" runat="server"></asp:TextBox>
      </p>
      <p>
        <asp:Label ID="Label2" runat="server">Last Name:</asp:Label>
         
        <asp:TextBox ID="LastName" runat="server"></asp:TextBox>
      </p>
      <p>
        <asp:Label ID="Label3" runat="server">Full Name:</asp:Label>
         
        <asp:TextBox ID="FullName" runat="server"></asp:TextBox>
      </p>
      <p>
        <asp:Label ID="Label4" runat="server">Full Name Length:</asp:Label>
         
        <asp:Label ID="FullNameLength" runat="server"
         Font-Bold="True">0</asp:Label>
      </p>
      <p>
        <asp:Button ID="AnalyzeName" runat="server"
         OnClick="AnalyzeName_Click" Text="Analyze Name"></asp:Button>
      </p>
    </form>
  </body>
</html>

C#

<%@ Page Language="C#" %>
        
<script runat="server">
  protected void AnalyzeName_Click(object sender, System.EventArgs e)
  {
    NameComponent.NameFunctionsClass Name =
       new NameComponent.NameFunctionsClass();
        
    if (FirstName.Text.Length > 0)
    {
      string firstName = FirstName.Text.ToString();
      Name.set_FirstName(ref firstName);
    }
        
    if (LastName.Text.Length > 0)
    {
      string lastName = LastName.Text.ToString();
      Name.set_LastName(ref lastName);
    }
        
    if (FullName.Text.Length > 0)
    {
      string fullName = FullName.Text.ToString();
      Name.set_FullName(ref fullName);
    }
        
    FirstName.Text = Name.get_FirstName();
    LastName.Text = Name.get_LastName();
    FullName.Text = Name.get_FullName();
    FullNameLength.Text = Name.FullNameLength.ToString();
  }
</script>

Next you add the reference to the ActiveX DLL that you created in the previous step. To do so, follow these steps:

1. Right-click your project in the Solution Explorer dialog box.
2. Select the Add Reference menu item.
3. In the Add Reference dialog box, select the Browse tab.
4. Locate the NameComponent.dll object by browsing to its location.
5. Click OK to add NameComponent.dll to the list of selected components and close the dialog box.

NOTE If you are not using Visual Studio 2012 or code-behind pages, you can still add a reference to your COM control by creating the RCW manually using the Type Library Converter and then placing an Imports statement (VB) or using statement (C#) in the page.

After you have selected your component using the Add Reference dialog box, an RCW file is created for the component and added to your application.

That’s all there is to it! Simply run the application to see the COM Interoperability layer in action.

When the Analyze Name button is clicked, the fields in the First Name, Last Name, and Full Name textboxes are sent to the RCW to be passed to the NameComponent.DLL ActiveX component. Data is retrieved in the same manner to repopulate the textboxes and to indicate the length of the full name.

Accessing Tricky COM Members in C#

Sometimes, some members of COM objects do not expose themselves properly to C#. In the preceding examples, the String properties did not expose themselves, but the Long property (FullNameLength) did.

You know when there is a problem because, although you can see the property, you cannot compile the application. For example, instead of the code shown in Listing B-2 for C#, use the following piece of code to set the FirstName property of the NameComponent.dll ActiveX component:

if (FirstName.Text.Length > 0)
  Name.FirstName = FirstName.Text.ToString();

When you try to compile this code, you get the following error:

c:\inetpub\wwwroot\wrox\Default.aspx.cs(67): Property, indexer, or event
'FirstName' is not supported by the language; try directly calling accessor methods
'NameComponent.NameFunctionsClass.get_FirstName()' or
'NameComponent.NameFunctionsClass.set_FirstName(ref string)'

The FirstName property seems to be fine. It shows up in IntelliSense, but you can’t use it. Instead, you must use set_FirstName (and get_FirstName to read). These methods do not show up in IntelliSense, but rest assured, they exist.

Furthermore, these methods expect a ref string parameter rather than a String. In the example from Listing B-2, two steps are used to do this properly. First, String is assigned to a local variable, and then the variable is passed to the method using ref.

Releasing COM Objects Manually

One of the great things about .NET is that it has its own garbage collection — it can clean up after itself. This is not always the case when using COM Interoperability, however. Because a COM object does not have the built-in garbage collection mechanism that .NET relies on, .NET has no way of knowing when to release a COM object from memory.

Because of this limitation, you should release COM objects from memory as soon as possible using the ReleaseComObject class of the System.Runtime.InteropServices.Marshal class:

System.Runtime.InteropServices.Marshal.ReleaseComObject(Object);

Note that if you attempt to use this object again before it goes out of scope, you would raise an exception.

ERROR HANDLING

Error handling in .NET uses exceptions instead of the HRESULT values used by Visual Basic 6 applications. Luckily, the RCW does most of the work to convert between the two.

Take, for example, the code shown in Listing B-3. In this example, a user-defined error is raised if the numerator or the denominator is greater than 1000. Also notice that it is not capturing a divide-by-zero error. Notice what happens when the ActiveX component raises the error on its own.

Begin this example by compiling the code in Listing B-3 into a class named DivideClass within an ActiveX component called DivideComponent.dll.

LISTING B-3: Raising errors in VB6

VB

Public Function DivideNumber(Numerator As Double, _
                             Denominator As Double) As Double
        
  If ((Numerator > 1000) Or (Denominator > 1000)) Then
      Err.Raise vbObjectError + 1, _
                "DivideComponent:Divide.DivideNumber", _
                "Numerator and denominator both have to " + _
                "be less than or equal to 1000."
        
  End If
        
  DivideNumber = Numerator / Denominator
        
End Function

Next, create a new ASP.NET project; add a reference to the DivideComponent.dll (invoking Visual Studio 2012 to create its own copy of the RCW). Remember, you can also do this manually by using the tlbimp executable.

Now add the code shown in Listing B-4 to an ASP.NET page.

LISTING B-4: Error handling in .NET

VB

<%@ Page Language="VB" %>
        
<script runat="server">
  Protected Sub Calculate_Click(ByVal sender As Object,
     ByVal e As System.EventArgs)
        
    Dim Divide As New DivideComponent.DivideClass()
        
    Try
       Answer.Text = Divide.DivideNumber(Numerator.Text, Denominator.Text)
    Catch ex As Exception
       Answer.Text = ex.Message.ToString()
    End Try
        
    System.Runtime.InteropServices.Marshal.ReleaseComObject(Divide)
        
  End Sub
</script>
        
<html xmlns="http://www.w3.org/1999/xhtml">
  <head runat="server">
     <title>Using COM Components</title>
  </head>
  <body>
    <form id="form1" runat="server">
      <p>
        <asp:Label ID="Label1" runat="server">Numerator:</asp:Label>
         
        <asp:TextBox ID="Numerator" runat="server"></asp:TextBox>
      </p>
      <p>
        <asp:Label ID="Label2" runat="server">Denominator:</asp:Label>
         
        <asp:TextBox ID="Denominator" runat="server"></asp:TextBox>
      </p>
      <p>
        <asp:Label ID="Label3" runat="server">
         Numerator divided by Denominator:</asp:Label>
         
        <asp:Label ID="Answer" runat="server" Font-Bold="True">0</asp:Label>
      </p>
      <p>
        <asp:Button ID="Calculate"
         runat="server"
         OnClick="Calculate_Click"
         Text="Calculate">
        </asp:Button>
      </p>
    </form>
  </body>
</html>

C#

<%@ Page Language="C#" %>
        
<script runat="server">
  protected void Calculate_Click(object sender, System.EventArgs e)
  {
        
    DivideComponent.DivideClass myDivide = new DivideComponent.DivideClass();
        
    try
    {
      double numerator = double.Parse(Numerator.Text);
      double denominator = double.Parse(Denominator.Text);
      Answer.Text = myDivide.DivideNumber(ref numerator,
         ref denominator).ToString();
    }
        
    catch (Exception ex)
    {
      Answer.Text = ex.Message.ToString();
    }
        
    System.Runtime.InteropServices.Marshal.ReleaseComObject(myDivide);
        
  }
</script>

The code in Listing B-4 passes the user-entered values for the Numerator and Denominator to the DivideComponent.dll ActiveX component for it to divide. Running the application with invalid data gives an exception message as shown in Listing B-4.

Depending on the language that you are using to run the ASP.NET application, you will see different values for different sets of data. For valid inputs, you will always see the correct result, of course, and for any input that is over 1000, you see the Visual Basic 6–appointed error description of Numerator and denominator both have to be less than or equal to 1000.

However, for invalid strings, Visual Basic 2012 reports Cast from string "abc" to type 'Double' is not valid, whereas C# reports Input string was not in a correct format. For a divide by zero, they both report Divide by Zero because the error is coming directly from the Visual Basic 6 run time.

DEPLOYING COM COMPONENTS WITH .NET APPLICATIONS

Deploying COM components with your .NET applications is very easy, especially when compared to just deploying ActiveX controls. Two scenarios are possible when deploying .NET applications with COM components:

Private Assemblies

Installing all or parts of the ActiveX component local to the .NET application is considered installing private assemblies. In this scenario, each installation of your .NET application on the same machine has, at least, its own copy of the Interop library for the ActiveX component you are referencing, as shown in Figure B-2.

FIGURE B-2

image

Whether you decide to install the ActiveX component as local to the application or in a shared directory for all calling applications is up to you.


NOTE It was once considered proper practice to separate ActiveX components into their own directory so that if these components were referenced again by other applications, you did not have to register or install the file for a second time. Using this method meant that when you upgraded a component, you automatically upgraded all the applications using this component. However, this practice didn’t work out so well. In fact, it became a very big contributor to DLL hell and the main reason why Microsoft began promoting the practice of installing private .NET component assemblies.

After you have your components physically in place, the only remaining task is to register the ActiveX component using regsvr32, just as you would when deploying an ActiveX-enabled application.

Public Assemblies

The opposite of a private assembly is a public assembly. Public assemblies share the RCW Interop DLL for other applications. To create a public assembly, you must put the RCW file into the Global Assembly Cache (GAC), as shown in Figure B-3.

FIGURE B-3

image

You can find the GAC at C:\Windows\assembly. Installing items in the GAC can be as simple as dragging-and-dropping an item into this folder through Windows Explorer. Although the GAC is open to everyone, blindly installing your components into this section is not recommended unless you have a very good reason to do so.

You can also add items to the GAC from the command line using the Global Assembly Cache Tool (Gacutil.exe). It enables you to view and manipulate the contents of the GAC and download cache. Although the Explorer view of the GAC provides similar functionality, you can use Gacutil.exe from build scripts, makefile files, and batch files.

Finding a very good reason to install your ActiveX Interop assemblies into the GAC is hard. If we had to pick a time to do this, it would be if and when we had a highly shared ActiveX component that many .NET applications would be utilizing on the same machine. In a corporate environment, this might occur when you are upgrading existing business logic from ActiveX to .NET enablement on a server that many applications use. In a commercial setting, we avoid using the GAC.

SUMMARY

In this appendix you learned how COM Interop provided access to existing COM components so you could use .NET and COM components in ASP.NET.