Using A COM Component From A .NET Client


To see how a .NET application can use a COM component, you first have to create a COM component. Creating COM components is not possible with C# or Visual Basic 2005; you need either Visual Basic 6 or C++ (or any other language that supports COM). This chapters uses the Active Template Library(ATL) and C++.

Note

A short note about building COM components with Visual Basic 2005 and C#: With Visual Basic 2005 and C# it is possible to build .NET components that can be used as COM objects by using a wrapper that is the real COM component. It would make no sense that a .NET component that is wrapped from a COM component is used by a .NET client with COM interop.

Note

Because this is not a COM book, it does not discuss all aspects of the code but only what you need to build the sample.

Creating a COM Component

To create a COM component with ATL and C++, create a new ATL Project. You can find the ATL Project Wizard within the Visual C++ Projects group when you select File New Project. Set the name to COMServer. With the Application Settings, select Attributed and Dynamic Link Library and click Finish.

Note

Since Visual Studio .NET 2002, the ATL offers attributes that make it easier to build a COM server. These attributes have nothing in common with the .NET attributes; instead, they are used only with ATL. Instead of writing a separate IDL file and a C++ file defining the interface, only a C++ file is needed that also has attributes required by COM.

The ATL Project Wizard just creates the foundation for the server. A COM object is still needed. Add a class in Solution Explorer and select ATL Simple Object. In the dialog that starts up, enter COMDemo in the field for the Short name. The other fields will be filled automatically, but change the interface name to IWelcome (see Figure 33-7).

image from book
Figure 33-7

The COM component offers two interfaces, so that you can see how QueryInterface() is mapped from .NET, and just three simple methods, so that you can see how the interaction takes place. In the Class View, select the interface IWelcome and add the method Greeting() (see Figure 33-7) with these parameters:

 HRESULT Greeting([in] BSTR name, [out, retval] BSTR* message); 

Your wizard-generated code from the file COMDemo.h should look similar to the following code. The unique identifiers (uuids) will differ. The interface IWelcome defines the Greeting()methods. The brackets before the keyword __interface define some attributes for the interface. uuid defines the interface id and dual marks the type of the interface:

 // COMDemo.h : Declaration of the CCOMDemo #pragma once #include "resource.h"       // main symbols // IWelcome [ object, uuid(""), dual, helpstring("IWelcome Interface"), pointer_default(unique) ] __interface IWelcome : IDispatch { [id(1), helpstring("method Greeting")] HRESULT Greeting([in] BSTR name,  [out, retval] BSTR* message); }; 

The class CCOMDemo is also in the file COMDemo.h. The attribute uuid() in the header section of the class defines the CLSID. The attributes vi_progid and progid name the prog id that will be written into the registry:

 // CCOMDemo [  coclass,  threading(apartment),  vi_progid("COMServer.COMDemo"),  progid("COMServer.COMDemo.1"),  version(1.0),  uuid(""),  helpstring("COMDemo Class") ] class ATL_NO_VTABLE CCOMDemo :   public IWelcome { public: CCOMDemo() { } DECLARE_PROTECT_FINAL_CONSTRUCT() HRESULT FinalConstruct() { return S_OK; } void FinalRelease()  { } public: STDMETHOD(Greeting)(BSTR name, BSTR* message); }; 
Important

With custom attributes, it is possible to change the name of the class and interfaces that are generated by a .NET wrapper class. You just have to add the attribute custom with the identifier , and the name under which it should appear within .NET.

Add the custom attribute with the same identifier and the name Wrox.ProCSharp.COMInterop.Server.IWelcome to the header section of the IWelcome interface. Add the same attribute with a corresponding name to the class CCOMDemo:

// IWelcome [   object,   uuid(""),   dual, helpstring("ICOMDemo Interface"),   pointer_default(unique), custom(,  "Wrox.ProCSharp.COMInterop.Server.IWelcome") ] __interface IWelcome : IDispatch {     [id(1)] HRESULT Greeting([in] BSTR name, [out, retval] BSTR* message); };

Now add a second interface to the file COMDemo.h. You can copy the header section of the IWelcome interface to the header section of the new IMath interface, but be sure to change the unique identifier that is defined with the uuid keyword. You can generate such an id with the guidgen utility. The interface IMath offers the methods Add() and Sub():

 // IMath [ object, uuid(""), dual, helpstring("IMath Interface"), pointer_default(unique), custom(,  "Wrox.ProCSharp.COMInterop.Server.IMath") ] __interface IMath : IDispatch { [id(1)] HRESULT Add([in] LONG val1, [in] LONG val2, [out, retval] LONG* result); [id(2)] HRESULT Sub([in] LONG val1, [in] LONG val2, [out, retval] LONG* result); }; 

The class CCOMDemo must also be changed so that it implements both interfaces IWelcome and IMath:

[   coclass,   threading(apartment),   vi_progid("COMServer.COMDemo"),   progid("COMServer.COMDemo.1"),   version(1.0), custom(,  "Wrox.ProCSharp.COMInterop.Server.COMDemo"),   uuid(""),   helpstring("COMDemo Class") ] class ATL_NO_VTABLE CCOMDemo :  public IWelcome, public IMath {

Now you can implement the three methods in the file COMDemo.cpp with the following code. The CComBSTR is an ATL class that makes it easier to deal with BSTRs. In the Greeting() method just a welcome message is returned that adds the name passed in the first argument to the message that is returned. The Add() method just does a simple addition of two values and the Sub() method does a subtraction, and returns the result:

 STDMETHODIMP CCOMDemo::Greeting(BSTR name, BSTR* message) { CComBSTR tmp("Welcome, "); tmp.Append(name); *message = tmp; return S_OK; } STDMETHODIMP CCOMDemo::Add(LONG val1, LONG val2, LONG* result) { *result = val1 + val2; return S_OK; } STDMETHODIMP CCOMDemo::Sub(LONG val1, LONG val2, LONG* result) { *result = val1 -- val2; return S_OK; } 

Now you can build the component. The build process also configures the component in the registry.

Creating a Runtime Callable Wrapper

You can now use the COM component from within .NET. To make this possible, you must create a runtime callable wrapper (RCW). Using the RCW, the .NET client sees a .NET object instead of the COM component; there is no need to deal with the COM characteristics because this is done by the wrapper. An RCW hides IUnknown and IDispatch interfaces (see Figure 33-8) and deals itself with the reference counts of the COM object.

image from book
Figure 33-8

The RCW can be created by using the command-line utility tlbimp or by using Visual Studio. Starting the command

 tlbimp COMServer.dll /out: Interop.COMServer.dll 

creates the file Interop.COMServer.dll including a .NET assembly with the wrapper class. In this generated assembly you can find the namespace COMWrapper with the class CCOMDemoClass and the interfaces CCOMDemo, IMath, and IWelcome. The name of the namespace can be changed by using options of the tlbimp utility. The option /namespace allows you to specify a different namespace, and with /asmversion you can define the version number of the assembly.

Note

Another important option of this command-line utility is /keyfile, which is used for assigning a strong name to the generated assembly. Strong names are discussed in Chapter 15.

An RCW can also be created by using Visual Studio. To create a simple sample application, create a C# console project. In Solution Explorer, add a reference to the COM server by selecting the COM tab and scroll down to the entry COMServer 1.0 Type Library (see Figure 33-9). Here all COM objects are listed that are configured in the registry. Selecting a COM component from the list creates an assembly with an RCW class.

image from book
Figure 33-9

After creating the wrapper class you can write the code for the application to instantiate and access the component. Because of the custom attributes in the C++ file, the generated namespace of the RCW class is Wrox.ProCSharp.COMInterop.Server. Add this namespace as well as the namespace System.Runtime.InteropServices to the declarations. From the namespace System.Runtime.InteropServices the Marshal class will be used to release the COM object:

 using System; using System.Runtime.InteropServices; using Wrox.ProCSharp.COMInteorp.Server namespace Wrox.ProCSharp.COMInterop.Client { class Program { [STAThread] static void Main(string[] args) { 

Now the COM component can be used similar to a .NET class. obj is a variable of type COMDemo. COMDemo is a .NET interface that offers the methods of both the IWelcome and the IMath interfaces. However, it is also possible to cast to a specific interface such as IWelcome. With a variable that is declared of type IWelcome, the method Greeting() can be called.

Note

Although COMDemo is an interface, you can instantiate new objects of type COMDemo. Contrary to normal interfaces, you can do this with wrapped COM interfaces.

 COMDemo obj = new COMDemo(); IWelcome welcome = (IWelcome)obj; Console.WriteLine(welcome.Greeting("Christian")); 

If the object — as in this case — offers multiple interfaces, a variable of the other interface can be declared, and by using a simple assignment with the cast operator, the wrapper class does a QueryInterface() with the COM object to return the second interface pointer. With the math variable, the methods of the IMath interface can be called:

 IMath math; math = (IMath)obj; int x = math.Add(4, 5); Console.WriteLine(x); 

If the COM object should be released before the garbage collector cleans up the object, the static method Marshal.ReleaseComObject() invokes the Release() method of the component, so that the component can destroy itself and free memory:

 Marshal.ReleaseComObject(math); } } } 

As you can see, with a runtime callable wrapper, a COM component can be used similar to a .NET object.

A special case of a runtime callable wrapper is a primary interop assembly, which is discussed next.

Primary interop assemblies

A primary interop assembly is an assembly that is already prepared by the vendor of the COM component. This makes it easier to use the COM component. A primary interop assembly is a runtime-callable wrapper that might differ from an automatically generated RCW.

You can find primary interop assemblies in the directory <program files>\Microsoft .NET\Primary Interop Assemblies. A primary interop assembly already exists for the use of ADO from within .NET. If you add a reference to the COM library Microsoft ActiveX Data Objects 2.7 Library, no wrapper class is created because a primary interop assembly already exists; the primary interop assembly is referenced instead.

Threading Issues

As discussed earlier in this chapter, a COM component marks the apartment (STA or MTA) it wants to live in, based on whether it is implemented thread-safe or not. However, the thread has to join an apartment. What apartment the thread should join can be defined with the [STAThread] and [MTAThread] attributes, which can be applied to the Main() method of an application. The attribute [STAThread] means that the thread joins an STA, whereas the attribute [MTAThread] means that the thread joins an MTA. Joining an MTA is the default if no attribute is applied.

It is also possible to set the apartment state programmatically with the ApartmentState property of the Thread class. The ApartmentState property allows you to set a value from the ApartmentState enumeration. ApartmentState has the possible values STA and MTA (and Unknown if it wasn't set). Be aware that the apartment state of a thread can only be set once. If it is set a second time, the second setting is ignored.

Important

What happens if the thread chooses a different apartment from the apartments supported by the component? The correct apartment for the COM component is created automatically by the COM runtime. However, the performance decreases if the apartment boundaries are crossed while calling the methods of a component.

Adding Connection Points

To see how COM events can be handled in a .NET application, first the COM component must be extended. Implementing a COM event in an ATL class using attributes looks very similar to the events in .NET, although the functionality is different.

First you have to add another interface to the header file COMDemo.h. The interface _ICompletedEvents is implemented by the client, which is the .NET application, and called by the component. In this example, the method Completed() is called by the component when the calculation is ready. Such an interface is also known as an outgoing interface. An outgoing interface must either be a dispatch or a custom interface. Dispatch interfaces are supported by all clients. The custom attribute with the id 0F21F359-AB84- 41e8-9A78-36D110E6D2F9 defines the name of this interface that will be created in the RCW:

 // _ICompletedEvents [ dispinterface, uuid(""), custom(,  "Wrox.ProCSharp.COMInterop.Server.ICompletedEvents"), helpstring("_ICompletedEvents Interface") ] __interface _ICompletedEvents { [id(1)] void Completed(void); }; 

Apply the attribute event_source(com) to the class CCOMDemo to create a connection point object, and add the __event keyword to the public section of this class as shown in the following code. This keyword __event creates a helper class for all methods of the defined interface that fires events to the client. The event is fired using the __raise keyword inside the method FireCompleted():

[   coclass,   threading(apartment),   vi_progid("COMServer.COMDemo"),   progid("COMServer.COMDemo.1"),   version(1.0),   custom(,       "Wrox.ProCSharp.COMInterop.Server.COMDemo"),   uuid(""), event_source(com),   helpstring("COMDemo Class") ] class ATL_NO_VTABLE CCOMDemo :     public IWelcome, public IMath { public:    CCOMDemo()    {    } __event __interface _ICompletedEvents; void FireCompleted() { __raise Completed(); } 

Finally, the method FireCompleted() can be called inside the methods Add() and Sub() in the file COMDemo.cpp:

STDMETHODIMP CCOMDemo::Add(LONG val1, LONG val2, LONG* result) {    *result = val1 + val2; FireCompleted();    return S_OK; } STDMETHODIMP CCOMDemo::Sub(LONG val1, LONG val2, LONG* result) {    *result = val1 - val2; FireCompleted();    return S_OK; } 

After rebuilding the COM DLL, you can change the .NET client to use these COM events:

   static void Main(string[] args)    {       COMDemo obj = new COMDemo();       IWelcome welcome = (IWelcome)obj;       Console.WriteLine(welcome.Greeting("Christian")); obj.Completed += new ICompletedEvents_CompletedEventHandler(Completed);       IMath math = (IMath)welcome;       int result = math.Add(3, 5);       Console.WriteLine(result);       Marshal.ReleaseComObject(math); } private static void Completed() { Console.WriteLine("Calculation completed"); }  

As you can see, the RCW offers automatic mapping from COM events to .NET events. COM events can be used similar to .NET events in a .NET client.

Using ActiveX Controls in Windows Forms

ActiveX controls are COM objects with a user interface and many optional COM interfaces to deal with the user interface and the interaction with the container. ActiveX controls can be used by many different containers such as Internet Explorer, Word, Excel, and applications written using Visual Basic 6, MFC(Microsoft Foundation Classes), or ATL (Active Template Library). A Windows Forms application is another container that can manage ActiveX controls. ActiveX controls can be used similar to Windows Forms controls as you see shortly.

ActiveX Control Importer

Similar to runtime callable wrappers, you can also create a wrapper for ActiveX controls. A wrapper for an ActiveX control is created by using the command-line utility Windows Forms ActiveX Control Importer, aximp.exe. This utility creates a class that derives from the base class System.Windows.Forms.AxHost that acts as a wrapper to use the ActiveX control.

You can enter this command to create a wrapper class from the Web Forms Control:

 aximp c:\windows\system32\shdocvw.dll  

ActiveX controls can also be imported directly using Visual Studio. If the ActiveX control is configured within the toolbox, it can be dragged and dropped to a Windows Forms control that creates the wrapper.

Creating a Windows Forms application

To see ActiveX controls running inside a Windows Forms application, create a simple Windows Forms application project. With this application, you will build a simple Internet browser that uses the Web Browser control, which comes as part of the operating system.

Create a form as shown in Figure 33-10. The form should include a text box that is used to enter a URL with the name textUrl, three buttons with the names buttonNavigate, buttonBack, and buttonForward to navigate Web pages, and a status bar with the name statusBar.

image from book
Figure 33-10

Using Visual Studio, you can add ActiveX controls to the toolbar to use it in the same way as a Windows Forms control. In the Customize Toolbox context menu, select the Add/Remove Items menu entry and select the Microsoft Web Browser control in the COM Components category (see Figure 33-11).

image from book
Figure 33-11

This way, an icon will show up in the toolbox. Similar to other Windows controls, you can drag and drop this icon to the Windows Forms designer to create (with the aximp utility) a wrapper assembly hosting the ActiveX control. You can see the wrapper assemblies with the references in the project: AxSHDocVw and SHDocVw. Now you can invoke methods of the control by using the generated variable axWebBrowser1 as shown in the following code. Add a Click event handler to the button buttonNavigate in order to navigate the browser to a Web page. The method Navigate() used for this purpose requires a URL string with the first argument that you get by accessing the Text property of the text field textUrl. The following four arguments are all optional with the Navigate() method. Because C# doesn't support optional arguments, you have to pass values. However, passing null values with the noArg variable is good enough:

 private void OnNavigate(object sender, System.EventArgs e) { object noArg = null; axWebBrowser1.Navigate(textUrl.Text, ref noArg, ref noArg, ref noArg,  ref noArg); } 

With the Click event handler of the Back and Forward buttons, call the GoBack() and GoForward() methods of the browser control:

 private void OnBack(object sender, System.EventArgs e) { try { axWebBrowser1.GoBack(); } catch { } } private void OnForward(object sender, System.EventArgs e) { try { axWebBrowser1.GoForward(); } catch { } } 

The Web control also offers some events that can be used just like a .NET event. Add the event handler OnStatusChange() to the event StatusTextChange to set the status that is returned by the control to the status bar in the Windows Forms application:

 private void OnStatusChange(object sender,  AxSHDocVw.DWebBrowserEvents2_StatusTextChangeEvent e) { statusBar.Text = e.text; } 

Now you have a simple browser that you can use to navigate to Web pages (see Figure 33-12).

image from book
Figure 33-12

Using COM Objects from within ASP.NET

COM objects can be used in a similar way to what you have seen before from within ASP.NET. However, there is one important distinction. The ASP.NET runtime by default runs in an MTA. If the COM object is configured with the threading model value Apartment (as all COM object that have been written with Visual Basic 6 are), an exception is thrown. Because of performance and scalability reasons, it is best to avoid STA objects within ASP.NET. If you really want to use an STA object with ASP.NET, you can set the AspCompat attribute with the Page directive as shown in the following snippet. Be aware that the Web site performance might suffer when you are using this option:

 <%@ Page AspCompat="true" Language="C#" %> 




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net