Programming in the .NET Environment

.NET Environment Overview

The Windows .NET environment is a Microsoft application development platform that supersedes technologies such as VB6 and Active Server Pages. The .NET environment 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.
The .NET environment 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 that is used to express the same CLS-defined semantics.
The C# language 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 Visual Basic 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)
    • provides connectivity to the broadest range of software implementations. Web services toolkits supporting many different environments and languages are available from a variety of vendors.
    • can leverage existing Web server infrastructure to provide connectivity.
    • has a 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.
    • has 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
    • provides 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.
    • requires low-level programming.
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. COM Interop with IOM servers provides a .NET interface that is superior to the .NET interface that Web Service proxies. IOM supports the connectivity to UNIX and z/OS servers that is a primary goal of Web Services architectures.
.NET developers who want 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 enables you to implement a Web Service with SAS language programming. For more information, see the SAS BI Web Services: Developer's Guide.

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.
Your .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 Software Development Kit (SDK) and development environments, such as 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 that correspond 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 does not provide a primary COM interop assembly for any of the IOM type libraries or dynamic link libraries (DLLs). 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 must modify the path to your shared files folder if you did not install SAS in the default location as follows:
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 converts 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 VB6, 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, VB6 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 previously called SAS::IFileref methods through a variable whose type was declared with the coclass name. Here is a typical example:
' 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
The interop assembly for an IOM type library contains the following elements:
  • a .NET interface for each COM interface. For example, .NET code that is equivalent to the preceding VB6 code contains 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 is also 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. 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, which is called the RCW (Run-time Callable Wrapper) class. This class is represented by the FilerefClass in the following example.
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.
Trace.Write(fref.FilerefName);
// Obtain a reference to the secondary interface on the object.
finfo = (SAS.IFileInfo) fref;
// Make a call using the secondary interface.
Trace.Write(finfo.PhysicalName);
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 of the variables 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 that the type library importer created from the COM IFileInfo interface.
COM coclasses that have the Creatable attribute in the type library can be instantiated directly. This is illustrated in the previous example by declaration of the following workspace variable:
Dim ws as new SAS.Workspace
In order to provide further similarity with VB6, the coclass interface on a creatable 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();
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, then 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 that are 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:
    • 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 (such as SAS::IFileInfo) or might be used by more than one (such as 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".
  • Instantiate a component such as the Workspace or the ObjectManager by using the coclass interface (such as Workspace) in C# and VB6. Other languages might require you to use an actual class name (such as 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.
Data Types
COM
VB6
.NET
VB.NET
C#
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#.

Arrays

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.
Therefore, 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; ilogLines.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 that is 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. The output parameter can also be used. If the output parameter is used, then 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; ilogLines.Length; i++) {
fileBox.Text += (logLines[i] + "\n");
}
if (logLines.Length  maxLines)
bMore = false;
}
The primary benefit of using this while loop is the ability to use normal array indexing syntax when accessing the element within the for loop. 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. Here is an 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;
optionNames=Array.CreateInstance(typeof(string),1);
optionNames.SetValue("MLOGIC",0);
iOS.GetOptions(ref optionNames, out types, out isPortable,
out isStartupOnly, out values, out errorIndices,
out errorCodes, out errorMsgs);

Enumerations

Many IOM methods accept or return enumeration types, which are declared in the IOM type library. In the preceding while loop, the CCs variable is actually an array of enumeration. The type library importer creates 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.
LanguageServiceCarriageControlNewPage:
fileBox.Text+="\n\n\n";
break;
case LanguageServiceCarriageControl.
LanguageServiceCarriageControlSkipTwoLines:
fileBox.Text += "\n\n";
break;
case LanguageServiceCarriageControl.
LanguageServiceCarriageControlSkipLine:
fileBox.Text += "\n";
break;
case LanguageServiceCarriageControl.
LanguageServiceCarriageControlOverPrint:
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 preceding example.
IOM places loose constants in enumerations that end in CONSTANTS. An enumeration simply named CONSTANTS includes constants for the entire type library. Some interfaces have their own set. For example, LibrefCONSTANTS contains the constants for the Libref interface. Enumerations that contain the word INTERNAL are collections of constants that are not documented for customer use.

Exceptions

The COM type library importer maps all failure HRESULTs into the same .NET exception: System.Runtime.InteropServices.COMException. This exception is 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 following table shows common error codes in IOM applications.
Error Codes
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 occur only when connecting to a server.
The second broad category of errors consists of those that are specific to particular IOM applications. These errors are documented by enumerations in the type library. 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 also has 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 messages 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 by 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 by using XSL.
bool bPathError;
string styleString =
"" +
"<xsl:stylesheet xmlns:xsl= " +
"\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\">" +
"<xsl:output method=\"text\"/>" +
"<xsl:template match=\"SASMessage\">" +
"[<xsl:value-of select=\"@severity\"/>] " +
"<xsl:value-of select=\".\"/> " +
"</xsl:template>" +
"</xsl:stylesheet>";
try
{
ws.DataService.AssignLibref(nameField, engineField,
pathField, optionsField);
}
catch (COMException libnameEx) {
switch ((DataServiceERRORS)libnameEx.ErrorCode)
{
case DataServiceERRORS.DataServiceNoLibrary:
bPathError = true;
break;
}
try
{
// 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.Load(styleRdr);
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 be XML only 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 technique is particularly important 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 example shows how to 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"" ?>")
obTextStream.Write(inputDS.GetXml())
obTextStream.Close()
' 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
The following example shows how to 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=" &
obSAS.UniqueIdentifier)
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 SAS IOM Data Provider .

Object Lifetime

In native COM programming, reference counting, whether it is 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 release the objects 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. For example, in the workspace, 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 shuts down (and is disconnected from its clients) when the client calls its Close() method. This also causes 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, and the Close() method cannot be called. Therefore, the server shuts down. If you do not call Close(), then the SAS process (sas.exe) does 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 by using delegates and event members, which are the standard .NET event handling mechanisms.
The first (and often the only) event interface that is 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 are then 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
CILanguageEvents_DatastepStartEventHandler(this.logDSStart);
lang.DatastepComplete += new
CILanguageEvents_DatastepCompleteEventHandler(
this.logDSComplete);
lang.ProcStart += new
CILanguageEvents_ProcStartEventHandler(this.logProcStart);
lang.ProcComplete += new
CILanguageEvents_ProcCompleteEventHandler(this.logProcComplete);
lang.StepError += new
CILanguageEvents_StepErrorEventHandler(this.logStepError);
lang.SubmitComplete += new
CILanguageEvents_SubmitCompleteEventHandler(
this.logSubmitComplete);
// 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
CILanguageEvents_DatastepStartEventHandler(this.logDSStart);
lang.DatastepComplete -= new
CILanguageEvents_DatastepCompleteEventHandler(
this.logDSComplete);
lang.ProcStart -= new
CILanguageEvents_ProcStartEventHandler(this.logProcStart);
lang.ProcComplete -= new
CILanguageEvents_ProcCompleteEventHandler(this.logProcComplete);
lang.StepError -= new
CILanguageEvents_StepErrorEventHandler(this.logStepError);
lang.SubmitComplete -= new
CILanguageEvents_SubmitCompleteEventHandler(
this.logSubmitComplete);
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). Currently, there are no SAS IOM components with a publicly documented interface that is not the default.