SAS 9.1.3 Integration Technologies » Developer's Guide

Developing Windows Clients
Client Requirements
Client Installation
Selecting a Windows Programming Language
Programming with Visual Basic
Programming in the .NET Environment
Using VBScript
Programming with Visual C++
Using the Object Manager
Creating an Object
Object Manager Interfaces
Using a Metadata Server with the Object Manager
Metadata Configuration Files
Error Reporting
Code Samples
Using Connection Pooling
Choosing Integration Technologies or COM+ Pooling
Using Integration Technologies Pooling
Using Com+ Pooling
Pooling Samples
Using the IOM Data Provider
Using the Workspace Manager
Class Documentation
Windows Clients

Programming in the .NET Environment

.NET Environment Overview

The Windows .NET environment is Microsoft's newest application development platform, superceding technologies such as Visual Basic 6 and Active Server Pages. .NET defines its own object system with the Common Language Specification (CLS). This object system is similar to Java, but contains many enhancements and additional features.

.NET supports many languages, including the new C# language and the latest variant of Visual Basic—VB.NET. The differences among languages are mainly in the syntax used to express the same CLS-defined semantics. For this reason, the choice of language is primarily a matter of personal taste or management standardization.

C# is popular because of its syntactic resemblance to Java and because it was designed specifically for the .NET CLS. VB.NET has a syntax that appeals to devoted VB programmers, although the requirements of the .NET environment have created significant compatibility issues for the existing VB6 code base. Most of the example code in this section was written in C#, but it should be easy to translate to other .NET languages.

Because .NET is a new environment, interoperability between .NET programs and existing programs is very important. Microsoft devoted careful attention to this and uses several technologies to provide compatibility:

  • Web Services (provided via ASP.NET or .NET remoting)
    • (+) theoretically provides connectivity to the broadest range of software implementations. Web services toolkits supporting many different environments and languages should be available from a variety of vendors.
    • (+) can leverage existing Web server infrastructure to provide connectivity.
    • (-) simplistic mapping into the .NET object system. Method calls cannot return an interface (only data can be returned) and interface casts are not supported.
  • COM Interop
    • (+) creates useful .NET classes for most existing COM interfaces—especially for the Automation compatible interfaces used by IOM.
    • (-) some weaknesses in the mapping due to lack of information in COM. For example, specific exception types are not distinguished because they cannot be declared in a COM type library.
  • .NET Remoting binary formatter
    • (+) seamless interfacing when talking to another .NET remoting application. The programming paradigm is very similar to Java RMI.
    • (-) does not support traditional executables or other environments.
  • PInvoke
    • (+) can call existing C APIs in-process.
    • (-) low-level programming required.

COM Interop provides the highest quality interfaces for programs that run outside the .NET environment (like SAS IOM servers). This is the approach used for IOM programming under .NET. While Web Services has received a lot of attention early in the marketing of .NET, it is important to understand that COM Interop with IOM servers actually provides a higher quality .NET interface than Web Service proxies would provide, and that IOM has always supported the connectivity to UNIX and z/OS servers that is a primary goal of the evolving Web Services architectures.

.NET developers wanting to implement a Web Service for use by others have a number of options:

  • Write an ASP.NET Web Service and call IOM interfaces via COM Interop.
  • Write a .NET remoting Web Service and call IOM interfaces via COM Interop.
  • Use the new COM Web Services feature of the SAS Integration Technologies Stored Process Server. This allows you to implement a Web Service with SAS language programming. For more information, see SAS BI Web Services Overview.

IOM Support for .NET

IOM servers are easily accessible to .NET clients, and the IOM Bridge capability allows calls to servers on UNIX and z/OS platforms.

.NET clients will naturally require .NET classes to provide any services that they need. With COM Interop, these .NET classes directly represent the individual IOM interfaces such as IWorkspace and ILanguageService. The .NET SDK and development environments like Microsoft Visual Studio .NET provide the tlbimp ("type library import") utility program to read a COM type library and create a .NET assembly that contains .NET classes corresponding to each of the COM interfaces.

It is possible for the creator of a type library to create an official .NET interop assembly for the type library. This is called the "primary interop assembly" for the type library. When the type library creator has not done this, developers should import the type library themselves to make an interop assembly specific to their own project.

SAS 9.1 does not provide a primary COM interop assembly for any of the IOM type libraries or dynamic link libraries (DLL). Thus, the first step in developing a .NET project that will use IOM is to import the desired type libraries or DLLs such as "sas.tlb" (which contains the Workspace interfaces) and "SASOMan.dll" (which contains the SAS Object Manager interfaces).

The following commands create the COM interop assemblies for sas.tlb and SASOMan.dll. You will need to modify the path to your shared files folder if you did not install SAS in the default location.

   tlbimp /sysarray
      "c:\Program Files\SAS\Shared Files\Integration Technologies\SAS.tlb"

   tlbimp /sysarray
      "c:\Program Files\SAS\Shared Files\Integration Technologies\SASOMan.dll"

Because the type library import process is effectively converting between two different object models, there are a number of idiosyncrasies in the process that will be explained in detail in the following sections.

Classes and Interfaces

Before looking at how .NET handles classes and interfaces, it is important to understand how COM interfaces were used in Visual Basic 6, because the .NET was designed to have some continuity with the treatment of COM objects in VB6.

IOM servers provide components that have a default interface and possibly additional interfaces. All access to the server component must occur through a method in one of its interfaces. While this is the same approach used by COM, Visual Basic 6 also allowed access to the default interface via the coclass (component) name. IOM type libraries use the standard COM convention—interface names have a capital "I" prefix, while coclass names do not.

Thus, while the SAS 9 SAS::Fileref component supports both the default SAS::IFileref interface and an additional SAS::IFileInfo interface, a VB6 programmer was allowed (and encouraged) to call SAS::IFileref methods through a variable whose type was declared with the coclass name. A typical example follows:

   ' VB6 code

   ' Primary interface declared as coclass name (without the leading "I").
   Dim ws as new SAS.Workspace
   Dim fref as SAS.Fileref
   Dim name as string
   Set fref = ws.FileService.AssignFileref("myfilref", _
      "DISK", "c:\myfile.txt", "", name)

   ' Secondary interface must use COM interface name (requires "I").
   ' There is no "SAS.FileInfo" because this is only an interface,
   '    not an object (coclass).
   Dim finfo as SAS.IFileInfo

   ' Call a method on the default interface.
   debug.print fref.FilerefName

   ' Obtain a reference to the secondary interface on the object.
   set finfo = fref

   ' Make a call using the secondary interface.
   debug.print finfo.PhysicalName

As a result, when Microsoft designed the rules for how COM type libraries would be represented by .NET classes through COM interop, there was a desire to support not only the fundamental COM idea of calling methods through interfaces, but also to provide the impression that the coclass type was itself an interface—the default interface of the coclass.

The interop assembly for an IOM type library will contain the following elements:

  • A .NET interface for each COM interface. Following the above example, you will see both an IFileref interface and an IFileInfo interface in the assembly.

  • A .NET interface with the same name as the coclass. Thus, for the Fileref coclass, there would also be a Fileref interface in the assembly. This .NET interface is called the "coclass interface."

    The coclass interface is almost identical to the default .NET interface for the coclass. It inherits from the default COM interface's .NET interface and defines no additional members. So the SAS assembly's Fileref interface (a coclass interface) defines no members itself and inherits from the IFileref interface.

    The coclass interface plays an additional role for components that can raise events. For more information, see Receiving Events.

  • A .NET class named with the "Class" suffix. This is represented by the "FilerefClass" in our example. This is called the "RCW class" (Runtime Callable Wrapper).

Given these substitutions, here is the equivalent C# code:

   // C#

   // Using the "coclass interface" for the workspace and fileref.
   // The name without the leading "I" came from the COM coclass,
   // but in .NET it is an interface.
   SAS.Workspace ws2 =
      new SAS.Workspace(); // instantiable interface - see below
   SAS.Fileref fref;
   String name;
   fref = ws2.FileService.AssignFileref("myfilref", "DISK",
      "c:\\myfile.txt", "", out name);

   // Secondary interface must use COM interface name (requires "I")
   // There is no "SAS.FileInfo" because this is just an interface,
   //    not an object (coclass).
   SAS.IFileInfo finfo;

   // Call a method on the default interface.

   // Obtain a reference to the secondary interface on the object.
   finfo = (SAS.IFileInfo) fref;

   // Make a call using the secondary interface.

While the VB6 and C# programs appear to be very similar, there is a difference at a deeper level. Unlike VB6, the .NET CLS makes a distinction between interface types and class types. All of the variables declared in the C# example are interfaces. None are classes. "ws" and "fref" are the "coclass interfaces" because the type library importer created them from the default interface of the "Workspace" and "Fileref" coclasses. "finfo" is a .NET interface variable of type "IFileinfo"—a type which the type library importer created from the COM IFileInfo interface.

COM coclasses that have the "createable" attribute in the type library can be instantiated directly. This is illustrated in the previous example by declaration of the workspace variable:

   Dim ws as new SAS.Workspace

In order to provide further similarity with VB6, the "coclass interface" on a createable coclass (the .NET interface named "Workspace" in our example) has a unique feature. The type library importer adds some extra .NET metadata to the interface so that it can be used for instantiation. Thus, while it would normally be illegal to try to instantiate an interface, the following statement becomes possible:

   SAS.Workspace ws = new SAS.Workspace(); 

Because of the extra metadata added to the SAS.Workspace interface by the type library importer, the C# and VB.NET compilers change this statement to the following:

   SAS.Workspace ws = new SAS.WorkspaceClass(); 

Note that this is a transformation done by the compiler. If you get a compilation error with the former syntax when using a language other than C# or VB.NET, you should try the second approach which includes an actual class name instead of a "coclass interface" name.

In order to complete the emulation of the type names used by VB6, the type library importer makes a transformation of method parameters to use the "coclass interface" where possible. Whenever a COM method signature (or attribute type) refers to a default interface for a coclass in the same type library, the .NET method (or attribute) uses the "coclass interface" type.

So, in the previous example, while the COM IDataService::AssignFileref() method is defined to return an IFileref interface, the .NET IFileref interface is not used. Instead, the type library importer recognizes that the COM IFileref interface is the default interface of the Fileref coclass and substitutes its "coclass interface" (Fileref) as the AssignFileref() return type.

With all of these transformations, the VB6 compatible view of the interface is complete. Let's review the rules as they apply to IOM programming:

  • There are effectively two types of interface—the default interfaces on an IOM component and additional interfaces.
    • default interfaces such as Workspace, FileService and Fileref use the "coclass interface," which has the same name as the component (no extra leading "I").
    • additional interfaces might be used by only one component (like SAS::IFileInfo) or might be used by more than one (like SASIOMCommon::IServerStatus). The names of these interfaces are identical to the COM interface names (and thus use the leading "I" )
  • Use the .NET variables of either type as interfaces (which they are) even if the type does not have the leading "I".
  • When you want to instantiate a component such as the Workspace or the ObjectManager, you can use the "coclass interface" (like "Workspace") in C# and VB6. Other languages might require you to use an actual class name (like "WorkspaceClass").

Simple Data Types

As illustrated in the following table, most of the simple data types from VB6 have the expected equivalents in .NET programming.

unsigned char Byte System.Byte Byte byte
VARIANT_BOOL Boolean System.Boolean Boolean bool
short Integer System.Int16 Short short
long Long System.Int32 Integer int
float Single System.Single Single float
double Double System.Double Double double
BSTR String System.String String string
DATE Date System.DateTime Date System.DateTime

Almost all of the data types are exact equivalents. The most significant difference is that "long", the 32-bit integer type in COM and VB6 has become "integer" in VB.NET and "int" in C#.


An IOM array is represented as a COM SAFEARRAY. The .NET type library import utility (tlbimp) can convert SAFEARRAYs in one of two ways:

  • using the /sysarray option—converted to a .NET System.Array
  • not using the /sysarray option—converted to a one dimensional array with a zero lower bound

This latter approach is discouraged. It does not work well with IOM type libraries because many arrays in IOM interfaces are two dimensional. However, when you use the Visual Studio .NET IDE to import a type library, Visual Studio supplies the /sysarray option.

This means that, for IOM programming, the resulting .NET arrays are passed in terms of the generic System.Array base class, instead of using the programming language's array syntax. Furthermore, an array variable is needed for input as well as output, because IOM follows the VB6 convention of always passing arrays by reference, even for input parameters.

As an illustration of input and output arrays, here is an example of using the IOM LanguageService in C#.

   SAS.LanguageService lang = ws.LanguageService;
   string[] sasPgmLines = {
      "data _NULL_; ",
      "   infile \'" + filenameBox.Text + "\';",
      "   input;" ,
      "   put _infile_;" ,
      "run;" } ;
   System.Array linesVar = sasPgmLines; // identical to type of ref parm
   lang.SubmitLines(ref linesVar);

   bool bMore = true;
   while (bMore) {
      System.Array CCs;
      const int maxLines = 100;
      System.Array lineTypes;
      System.Array logLines;
      lang.FlushLogLines(maxLines, out CCs,
         out lineTypes, out logLines);
      for (int i=0; i<logLines.Length; i++) {
         fileBox.Text += (logLines.GetValue(i) + "\n");
      if (logLines.Length < maxLines)
         bMore = false;

The example uses the sasPgmLines C# array to set up the array value. However, C# requires the type in the reference parameter declaration be identical to (not just implicitly convertible to) the type of the argument being passed. Thus, the example must use the linesVar variable as the reference parameter.

A similar situation occurs with the output parameters. The parameters must be received into System.Array variables. At that point, if there are to be only a few lines where the array is accessed, it might be most convenient to access the array through the System.Array variable, as shown in the previous example. Note that the Length attribute is primarily applicable to one-dimensional arrays. For two-dimensional arrays, the GetLength() method (which takes a dimension number) is usually needed.

The input parameter in the previous example shows how to interchange arrays with System.Array objects. This could also have been done using the output parameter. In that case, the while loop might be changed as follows:

   while (bMore) {
      System.Array CCs;
      const int maxLines = 100;
      System.Array lineTypes;
      System.Array logLinesVar;
      string []logLines;
      lang.FlushLogLines(maxLines, out CCs,
         out lineTypes, out logLinesVar);
      logLines = (string [])logLinesVar; // explicit conversion
      for (int i=0; i<logLines.Length; i++) {
         fileBox.Text += (logLines[i] + "\n");
      if (logLines.Length < maxLines)
         bMore = false;

The primary benefit from using the previous while loop is the ability to use normal array indexing syntax when accessing the element within the for loop. Note also the assignment from logLinesVar to LogLines required a string[] cast, because the conversion from the more general System.Array to the specific array type is an "explicit conversion."

The examples in this section use for loops to illustrate array indexing with each type of array declaration. In practice, simple loops can be expressed more concisely using the C# foreach statement. For example,

   foreach (string line in loglines){
      filebox.Text += (line + "\n");

Another alternative, which avoids the use of two variables per array, is to use the Array.CreateInstance() method here as illustrated with the "optionNames" variable.

   Array optionNames, types, isPortable, isStartupOnly, values,
      errorIndices, errorCodes, errorMsgs;
   iOS.GetOptions(ref optionNames, out types, out isPortable,
      out isStartupOnly, out values, out errorIndices,
      out errorCodes, out errorMsgs);


Many IOM methods accept or return enumeration types, which are declared in the IOM type library. In the previous while loop, the CCs variable is actually an array of enumeration. The type library importer will create a .NET enumeration type for each COM enumeration in the type library.

Here is an elaboration of the LanguageService example that uses output enumeration to determine the number of lines to skip:

   while (bMore) {
      System.Array CCVar;
      const int maxLines = 100;
      System.Array lineTypes;
      System.Array logLinesVar;
      string []logLines;

      SAS.LanguageServiceCarriageControl []CCs;
      lang.FlushLogLines(maxLines, out CCVar,
         out lineTypes, out logLinesVar);
      logLines = (string [])logLinesVar;
      CCs = (LanguageServiceCarriageControl [])CCVar;
      for (int i=0; i<logLines.Length; i++) {
         // Simulate some carriage control with newlines.
         switch (CCs[i]) {
         case LanguageServiceCarriageControl.
         case LanguageServiceCarriageControl.
            fileBox.Text += "\n\n";
         case LanguageServiceCarriageControl.
            fileBox.Text += "\n";
         case LanguageServiceCarriageControl.
            continue; // Don't do overprints.
         fileBox.Text += (logLines[i] + '\n');
      if (logLines.Length < maxLines)
         bMore = false;

COM does not provide scoping for enumeration constant names, either with respect to the enumeration or with respect to the interface to which an enumeration might be related. Thus, the name for each constant in the type library must contain the name of the enumeration type in order to avoid potential clashes. .NET, on the other hand, does provide scoping for constant names within enumeration names and requires them to be qualified. This combination of factors results in the very long and repetitive names in the previous example.

IOM places loose constants in enumerations ending in CONSTANTS. An enumeration simply named CONSTANTS includes constants for the entire type library and some interfaces, have their own set. LibrefCONSTANTS is an example of this. Enumerations containing the word INTERNAL are collections of constants that, for various reasons, are not documented for customer use.


The COM type library importer maps all failure HRESULTs into the same .NET exception: System.Runtime.InteropServices.COMException. This exception will be thrown whenever an IOM method call fails. The specific type of failure is determined by its HRESULT code, which is found in the ErrorCode property of the exception.

Types of errors fall into two broad categories: system errors and application errors. The system error codes are standard to all Windows programming languages. The most common error codes in IOM applications are:

HRESULT Value Symbolic Name Description
0x8007000E E_OUTOFMEMORY The server ran out of memory.
0x80004001 E_NOTIMPL The method is not implemented.
0x80004002 E_NOINTERFACE The object does not support the requested interface.
0x80070057 E_INVALIDARG You passed an invalid argument.
0x80070005 E_ACCESSDENIED You lack authorization to complete the request.
0x80070532 HRESULT_FROM_WIN32(ERROR_PASSWORD_EXPIRED) The supplied password is expired.
0x8007052E HRESULT_FROM_WIN32(ERROR_LOGON_FAILURE) Either the user name or the password is invalid.
0x800401FD CO_E_OBJNOTCONNECTED You tried to call an object that no longer exists.
0x80010114 RPC_E_INVALID_OBJECT You tried to call an object that no longer exists (equivalent to CO_E_OBJNOTCONNECTED).

In principle these exceptions can be returned from any call, although exceptions that are related to a password will only occur when connecting to a server.

The second broad category of errors consists of those that are specific to particular IOM applications. These are documented by enumerations in the type library, and the IOM class documentation lists which of these (if any) can be returned from a particular method call. The type library typically has one ERRORS enumeration for errors that are relevant to more than one interface. It will also have another enumeration for each interface that has its own set of errors. For example, there is a DataServiceERRORs enumeration for the IDataService interface.

Besides the ErrorCode property, there are several other fields. The most important of these is the ErrorMessage. SAS 9 and later servers return an entire list of messages. Because COM does not support a chain of exceptions, these are returned as an XML-based list in the ErrorMessage field.

If you want to present an attractive error message to your user, you need to parse the error message using an XML parser.

The following code fragment shows error handling for a libref assignment call. This code makes an extra check for an invalid pathname error and illustrates a very simple reformatting of the error message field using XSL.

   bool bPathError;
   string styleString =
      "<?xml version='1.0'?>" +
      "<xsl:stylesheet xmlns:xsl= " +
         "\"\" version=\"1.0\">" +
      "<xsl:output method=\"text\"/>" +
      "<xsl:template match=\"SASMessage\">" +
      "[<xsl:value-of select=\"@severity\"/>] " +
      "<xsl:value-of select=\".\"/> " +
      "</xsl:template>" +

      ws.DataService.AssignLibref(nameField, engineField,
         pathField, optionsField);
   catch (COMException libnameEx) {

      switch ((DataServiceERRORS)libnameEx.ErrorCode)
         case DataServiceERRORS.DataServiceNoLibrary:
         bPathError = true;

         // Load the style sheet as an XmlReader.

         UTF8Encoding utfEnc = new UTF8Encoding();
         byte []styleData = utfEnc.GetBytes(styleString);
         MemoryStream styleStream = new MemoryStream(styleData);
         XmlReader styleRdr = new XmlTextReader(styleStream);

         // Load the error message as an XPathDocument.
         byte []errorData = utfEnc.GetBytes(libnameEx.Message);
         MemoryStream errorStream = new MemoryStream(errorData);
         XPathDocument errorDoc = new XPathDocument(errorStream);

         // Transform to create a message.
         StringWriter msgStringWriter = new StringWriter();
         XslTransform xslt = new XslTransform();
         xslt.Transform(errorDoc,null, msgStringWriter);

         // Return the resulting error string to the user.
         errorMsgLabel.Text = msgStringWriter.ToString();
         errorMsgLabel.Visible = true;

      catch (XmlException)
         // Accommodate SAS V8-style error messages with no XML.
         errorMsgLabel.Text = libnameEx.Message;
         errorMsgLabel.Visible = true;

The error text for the COM exception will only be XML if you are running against a SAS 9 server. If your client program runs against a SAS 8 server or against SAS 9 servers with the V8ERRORTEXT object server parameter, which suppresses the XML in error messages, then the construction of the XPathDocument will throw an exception. The previous example catches this exception and returns the error message unchanged.

Accessing SAS Data with ADO.NET

The SAS Integration Technologies client includes an OLE DB provider for use with the SAS Workspace. This provider makes it possible for your program to access SAS data within an IOM Workspace using ADO.NET's OLE DB adapter. This is a particularly important technique in IOM Workspace programming, because it is often the easiest way to get results back to the client after a SAS PROC or DATA step.

The following examples, written in VB.NET, show how this is done

Copy an ADO.Net Data Set into a SAS Data Set
   ' This method sends the given data set to the provided workspace, and
   ' assigns the WebSvc libref to that input data set
   Private Sub SendData(ByVal obSAS As SAS.Workspace,
      ByVal inputDS As DataSet)

   ' Take the provided data set and put it in a fileref in SAS as XML
   Dim obFileref As SAS.Fileref
   Dim assignedName As String

   ' Filename websvc TEMP;
   obFileref = obSAS.FileService.AssignFileref(
      "WebSvc", "TEMP", "", "", assignedName)

   Dim obTextStream As SAS.TextStream
   obTextStream = obFileref.OpenTextStream(
      SAS.StreamOpenMode.StreamOpenModeForWriting, 2000)

   obTextStream.Separator = " "
   obTextStream.Write("<?xml version=""1.0"" standalone=""yes"" ?>")

   ' An ADO.Net data set is capable of holding multiple tables, schemas,
   ' and relationships. This sample assumes that the ADO.Net data set
   ' only contains a single table whose name and columns fit within SAS
   ' naming rules. This would be an ideal location to use XMLMap to
   ' transform the schema of the provided data set into something that
   ' SAS may prefer.
   ' Here, the default mapping is used. Note that the LIBNAME statement
   ' uses the fileref of the same name because we did not specify a file.
   ' Using the IOM method is cleaner than using the Submit because an
   ' error is returned if there is a problem making the assignment
   obSAS.DataService.AssignLibref("WebSvc", "XML", "", "")
   ' obSAS.LanguageService.Submit("libname webSvc XML;")

   End Sub
Copy a SAS Data Set into an ADO.Net Data Set
   ' Copy a single SAS data set into a .NET data set
   Private Function GetData(ByVal obSAS As SAS.Workspace, ByVal
      sasDataset As String) As DataSet
   Dim obAdapter As New System.Data.OleDb.OleDbDataAdapter("select * from "
      & sasDataset, "provider=sas.iomprovider.1; SAS Workspace ID=" &
   Dim obDS As New DataSet()
   ' Copy data from the adapter into the data set
   obAdapter.Fill(obDS, "sasdata")
   GetData = obDS

   End Function

For more information about using OLE DB with IOM, see "Using the IOM Data Provider".

Object Lifetime

In native COM programming, reference counting—whether it be explicitly written or managed by "smart pointer" wrapper classes—can require careful attention to detail. The VB6 run-time environment did much to alleviate that, and .NET's garbage collection has a similar simplifying effect. When programming with Interop, COM objects are normally released via garbage collection. You can also effect the release at any time by making a sufficient number of calls to System.Runtime.InteropServices.Marshal.ReleaseComObject().

With most IOM objects, the exact timing of releases is unimportant. In the workspace, for example, the various components maintain references among themselves, so that they do not get destroyed, even if the client releases them. The workspace as a whole will shut down (and be disconnect from its clients) when the client calls its Close() method. This will also cause the process to shut down in the typical (not pooled) situation where there is only one workspace in the process. Releases only become significant when all COM objects for the workspace hierarchy are released. When all objects are released, the objects cannot be used again (nor can the Close() method be called) and thus the server shuts down. If you do not call Close(), then the SAS process (sas.exe) will not terminate until .NET runs garbage collection.

Receiving Events

When a COM component raises events, the type library provides helper classes to make it easy to receive those events using delegates and event members, which are the standard .NET event handling mechanisms.

The first (and often the only) event interface supported by a component is included in the coclass interface. You can add event listener methods to the event members of the coclass interface, which will then be called when the IOM server raises the events.

Here is an example of collecting SAS Language events.

   private void logDSStart() {
      progress.Text += "[LanguageService Event] DATASTEP start.\n";
   private void logDSComplete() {
      progress.Text +=
         "[LanguageService Event] DATASTEP complete.\n";
   private void logProcStart(string procName) {
      progress.Text += "[LanguageService Event] PROC " +
         procName + " start.\n";
   private void logProcComplete(string procName) {
      progress.Text += "[LanguageService Event] PROC " +
         procName + " complete.\n";
   private void logStepError() {
      progress.Text += "Step error.\n";
   private void logSubmitComplete(int sasrc) {
      progress.Text +=
         "[LanguageService Event] Submit complete return code: " +
         sasrc.ToString() + ".\n";

   // Event listeners use the LanguageService coclass interface.
   // The Language Service also includes events for the default event interface.
   SAS.LanguageService lang = ws.LanguageService;

   lang.DatastepStart += new

   lang.DatastepComplete += new

   lang.ProcStart += new

   lang.ProcComplete += new

   lang.StepError += new

   lang.SubmitComplete += new

   // Submit source, clear the list and log, etc...

   // Stop listening.
   // The "new" operator here is confusing.
   //   Event removal does not really care about the particular
   //   delegate instance. It just looks at the identity of the
   //   listening object and the method being raised. Getting a
   //   new delegate is an easy way to gather that together.
   SAS.LanguageService lang = ws.LanguageService;

   lang.DatastepStart -= new

   lang.DatastepComplete -= new

   lang.ProcStart -= new

   lang.ProcComplete -= new

   lang.StepError -= new

   lang.SubmitComplete -= new

Note that IOM event interfaces begin with "CI" (not just "I"), because they are not dual interfaces. They are instead, ordinary COM vtable interfaces, which makes them easier for some types of clients to implement.

COM Interop also has support for components that raise events from more than one interface. In this case, you must add your event handlers to the RCW interface (such as LanguageServiceClass in the previous example). But note that currently, there are no SAS IOM components with a publicly documented interface that is not the default.