Windows Clients
Programming in the .NET Environment.NET Environment OverviewThe 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 BasicVB.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:
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:
IOM Support for .NETIOM 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 InterfacesBefore 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 conventioninterface 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 interfacethe default interface of the coclass. The interop assembly for an IOM type library will contain the following elements:
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 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:
Simple Data TypesAs illustrated in the following table, most of the simple data types from VB6 have the expected equivalents in .NET programming.
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#. ArraysAn IOM array is represented as a COM SAFEARRAY. The .NET type library import utility (tlbimp) can convert SAFEARRAYs in one of two ways:
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 (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 The examples in this section use 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); EnumerationsMany IOM methods accept or return enumeration types, which are declared in the IOM type library. In the previous 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 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. ExceptionsThe 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:
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= " + "\"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 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.NETThe 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"" ?>") 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 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 IOM Data Provider". Object LifetimeIn native COM programming, reference countingwhether it be explicitly written or managed by "smart pointer" wrapper classescan 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 EventsWhen 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 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); 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. |