Contents Windows Client Development Previous Next

Using the Workspace Manager

The SAS Workspace Manager creates and manages SAS Workspaces. With the Workspace Manager, you can:

The Workspace Manager can be used from Visual Basic, C, C++, and VBScript (with the help of Scripto).

The SAS Workspace Manager can create a SAS Workspace in one of three ways:

Security

The userid that is used to log on to SAS will be determined when the workspace is launched. Once launched, the userid cannot be changed.

Stored passwords are not encrypted. This applies to both LDAP entries and files.

The information in a file is only restricted by the permissions on the file. If you are concerned about security, you may not want to use files to store LoginDefs.

Administrators of LDAP servers should configure the directory such that the right to read each LoginDef is restricted to the owner of the LoginDef. Granting access to a LoginDef allows a user to start SAS and log on as the User defined in the LoginDef. It also allows the user to view the password. Also note that the password is sent across the wire in plaintext.

Launching IOM Servers

IOM servers can be launched in two ways:

  1. Unadministered, standalone, one-time

    The typical reason this scenario is selected is that it facilitates the use of an OLE DB provider along with a SAS Workspace. While it is possible to use OLE DB without the workspace manager, the only way to use OLE DB with a live SAS Workspace interface is by using the workspace manager.

  2. Administered, networked

    This allows launching of SAS Workspaces from a shared repository (either in a file or LDAP server). This allows you to launch SAS without regard to the hardware platform or communications protocol (COM, DCOM, or the IOM Bridge for COM) that is being used. This also supports simultaneous OLE DB and SAS Workspace interaction.

Definitions

There are three definitions that can be created to assist in launching an IOM Server:

  1. Server definition (ServerDef)

    A server definition must be created before an IOM Server can be launched with the workspace manager. The server definition can either be loaded from persistent storage (a file or LDAP server), or created dynamically. A ServerDef includes a Logical Name attribute. The server definition is independent of the user.

  2. Login definition (LoginDef)

    This is user-specific information such as a user name and password. Login definitions are a convenience and are not required for creating a connection to an IOM server. They provide a mechanism for storing persistent definitions of user names and passwords.

    LoginDefs also allow multiple definitions for the same user on different security domains. For example, you could use one user name and password on MVS and a different one for UNIX. This is also possible without the use of a login definition, but the user will need to enter the username/password each time a server is launched.

  3. Logical name definition (LogicalNameDef)

    The logical Name definition allows a description to be associated with each logical name used in a server definition. Logical name definitions are not used to launch a server. However, a logical name is required to launch a server when using the login definition.

These definitions may be stored in a file on the local system or may be stored in a network directory (LDAP server).

Finding Definitions

The SAS Workspace Manager can access system-wide state information that is stored in the Windows registry. This system-wide state consists of three search specifications for finding launch information:

  1. A per-user file for local computer storage.

  2. A local computer system-wide file.

  3. An LDAP container (network directory folder) in which server definitions may be found. The SASWorkspaceManager will find definitions directly in this folder and can also find pointers to other containers in this folder.

This information is designed to make it easy to find launch information in a standard location. Also, UI applications can immediately list launch definitions found there.

Interfaces

The diagram below shows the relationship of the interfaces provided with the Workspace Manager.

WorkspaceManager Interfaces

IWorkspaceManager

The IWorkspaceManager interface is the top level interface. It provides collection methods for ServerDefs, LoginDefs, LogicalNameDefs, and running SAS Workspaces.

Properties

Scope(enum Scope *pVal)

The scope applies to any function in IWorkspaceManager. It determines where the server and login defs are stored/retrieved. The scope can be set to an enum Scope value (ScopeLocal=1, ScopeSystem=2, ScopeGlobal=3.) This value is stored in the user registry each time Scope is called. Not calling scope causes the last used scope value to be used.

ServerDefs([out, retval] IServerDefs **ppIServerDefs)

Returns a collection of serverdefs.

LoginDefs([out, retval] ILoginDefs **ppILoginDefs)

Returns a collection of logindefs.

LogicalNameDefs([out, retval] ILogicalNameDefs **ppILogilNameDefs)

Returns a collection of logicalNameDefs.

Workspaces

Returns a collection of workspaces, which is the set of workspaces that have been created using the SASWorkspaceManager with VisibilityProcess.

FilePath set and get

The file to read and write SAS Workspace Manager Definition files to/from (suggest *.swd).

Methods

SetLDAPServer([in] BSTR ldapServerURL, [in] VARIANT_BOOL persistInRegistry)

Used to specify which LDAP server to use and where to look in the server. LDAP is used only for SASWorkspaceManagerGlobal scope.

The format of the ldapServerURL is LDAP://server:port/baseDN. BaseDN must be specified, and is the distinguished name of the container for the sasserver and saslogin definitions.

The baseDN should include cn=SAS at the start. (This is different than what is specified in the IT administrator, which does not include cn=SAS.) For example, if the baseDN specified in the IT administrator is cn=Applications,dc=myDomain,dc=myCompany,dc=com, then the baseDN specified for the Workspace Manager must be cn=SAS,cn=Applications,dc=myDomain,dc=myCompany,dc=com.

If the port is 0, the default LDAP_PORT (389) is used.

If persistInRegistry is True, the LDAP server that is specified is stored in the registry (HKEY_LOCAL_MACHINE). This occurs each time SetLDAPServer is called. If it is not called, the data previously stored in the registry will be used when the first method that access LDAP is called. If you get a return code of INVALID_LDAP_SERVER, the information in the registry is either wrong or the LDAP server is not running.

SetLDAPUser([in] BSTR userName,[in] BSTR Password, [in]VARIANT_BOOL persistInRegistry)

Specifies which LDAP userid to use. LDAP is only used for SASWorkspaceManagerGlobal scope.

UserName is a distinguished name of a user to use to connect to LDAP with. Password is the password that corresponds with UserName. If userName and password are both left blank (""), the Workspace Manager will use SSPI to connect to the server.

If persistInRegistry is True, the LDAP user is stored in the registry (HKEY_CURRENT_USER). This occurs each time SetLDAPUser is called. If this is not called, the data previously stored in the registry will be used.

IWorkspaces

This interface is used to create, enumerate, locate, and remove running SAS Workspace instances. The only SAS Workspaces that can be manipulated through IWorkspaces are those which are created from IWorkspaces.

Properties

Count([out, retval] int *iVal)

The number of IWorkspaces in this container

_NewEnum([out, retval] IEnumVARIANT **ppIEnumVariantsOfIWorkspace)

Returns an Enumerator of IWorkspaces. Note that the strange name allows VB to treat this as a collection.

UseXMLInErrorInfo

Determine whether Err.Description (which is really IErrorInfo->Description) is an XML table of all connection attempts (true) or just a description of the last failed connection attempt.  This option is provided to facilitate problem determination in making connections. Each time the Workspace Manager attempts a connection with a server and fails, an additional record is written in the returned XML error info.

Note that VB may display a dialog box that is too small to display all the xml error info.

The xmlInfo string returned from CreateWorkspaceByLogicalName and CreateWorkspaceByServer is always XML.

Methods

CreateWorkspaceByLogicalName([in]BSTR name, [in] enum Visibility visibility, [in]BSTR logicalName, [in]BSTR referenceDN, [out]BSTR *xmlInfo, [out, retval]IWorkspace **newWorkspace)

This method creates a SAS Workspace from a logical name definition. The invocation of this method results in the following sequence of steps to create the new workspace:

  1. Create a list of all ServerDefs that define the provided logicalName

  2. Select the first serverDef in the list (Note that the first may vary depending on the LDAP server.)

  3. Locate a LoginDef that matches both the DomainName (from the ServerDef) and the provided referenceDN

  4. Attempt to create a connection, once for each MachineDNSName in the serverDef, adding the results (either success or failure) into the xmlInfo.

  5. If a connection to SAS could not be established, the next serverDef is tried and the process is repeated from step 3

  6. If no Workspace has been created after going through all the hostnames in each serverDef and all the serverDefs that match the given logicalName, an error is returned (SEE_XML). Also, the table of connectionAttempts is returned in Err.Description (and if IWorkspaces.UseXMLInErrorInfo is true).

  7. If a Workspace is created, the IWorkspace is added to an internal list in the WorkspaceManager. In this case, the errorString will still show all failed attempts, if any.

The name that is passed to the method can be used in other IWorkspaces methods to determine which workspace to locate or remove. The name is also set on IWorkspace->Name before this method returns. The SAS Workspace Manager does not ever look at IWorkspace->Name, so a client could change the name after calling Create. The xmlInfo is only defined when this method returns an IWorkspace. See Error Reporting for details.

Note that the WorkspaceManager should be notified when a Workspace is no longer in use, so the workspace can be removed from the internal list, and so the reference the WorkspaceManager holds on the Workspace can be released.

CreateWorkspaceByServer([in]BSTR workspaceName, [in] enum SASWorkspaceManagerVisibility visibility, [in]IServerDef *serverDef, [in]BSTR userName, [in]BSTR password,   [out]BSTR *xmlInfo, [out, retval]IWorkspace **newWorkspace)

This method creates a workspace from a ServerDef object instead of a logical name. It also accepts the actual userName and password instead of a reference to a LoginDef. This method is preferred over CreateWorkspaceByLogicalName since it does not require username/password pairs to be written to a file or a network directory.

This method attempts to connect to each of the hosts listed in the provided serverDef, one at a time, until either a successful connection is made or all hosts have failed.  The userName/password is not required for serverDefs that specify ProtocolCom.

GetWorkspaceByName([in]BSTR workspaceName, [out, retval]IWorkspace **ppIWorkspace)

This method locates a workspace by name. This name must have been previously passed in to the workspace manager as the first parameter in the Add (or AddByObjects) method.

GetWorkspaceByUUID([in] BSTR uuidString, [out, retval]IWorkspace **ppIWorkspace)

This method locates a workspace by its UUID. This UUID is created by SAS and is available through IWorkspace->UniqueIdentifier.

RemoveWorkspace([in]IWorkspace *pIWorkspace)

This method removes a workspace from the collection and release the reference. Note that this method does not Quit the workspace.

RemoveWorkspaceByUUID([in]BSTR uuidString)

This method removes a workspace from the collection by UUID. This method performs the same function as the RemoveWorkspace method, but with a different input parameter.

CreateColocatedWorkspace([in]BSTR name, [in] enum Visibility visibility, [in]IWorkspace *existingWorkspace, [out, retval] IWorkspace **newWorkspace)

This method creates a new workspace from an existing workspace, and manages it with the SAS workspace manager.

AddExternalWorkspace([in] enum Visibility visibility, [in]IWorkspace *existingWorkspace)

This method adds a Workspace that was created by something outside of the WorkspaceManager. This allows applications to call CoCreateInstance (or whatever else they may have), and notify the Workspace Manager about the existence of that Workspace. Then, that existing workspace can be associated with the OLE DB provider.

ILoginDef

This interface allows you to create and manipulate login definitions (LoginDefs). A loginDef is only needed for connections using the IOM Bridge for COM. SSPI, the default NT security, is used for COM/DCOM connections.

Properties

Name([out, retval] BSTR *pVal)

Display name of this definition. The definition will persist under this name, so it must be unique. This is the name that is passed to GetWorkspace.

Loginname([out, retval] BSTR *pVal)

Username as known to the server host.

Password([out, retval] BSTR *pVal)

Password as known to the server host.

DomainName([out, retval] BSTR *pVal)

The security domain for which this username and password are valid.

ReferenceDN([out, retval] BSTR *pVal)

All referenceDNs should be the same for any given user across domains.

ILoginDefs

ILoginDefs contains the standard collection methods Count, _NewEnum, Add, Item, and Remove where the key is the loginDefName. It also supports one additional method:

GetLoginByUserAndDomain([in]referenceDN, [in]domainName, [out,retval]ILoginDef**)

Returns the ILoginDef that has the given referenceDN and domain.

IServerDef

The workspace manager uses the server def to determine how to connect to the server. For a local or dcom server, only the Name and Hostname values need be filled out. The IOM Bridge for Com requires Protocol to be set to ProtocolBridge; Port and ServiceName may be used with the IOM Bridge for COM.

Properties

Port([out, retval] short *pVal)

The TCP/IP port number used to connect with the bridge.

ServiceName([out, retval] BSTR *pVal)

TCP/IP Service name to use to lookup the port. Both Port and ServiceName should not be specified.

Name([out, retval] BSTR *pVal)

Display name of this definition. This value must be set before the serverDef can be persisted.

DomainName([out, retval] BSTR *pVal)

The security domain that these machineDNSNames exist in. All hosts listed in the MachineDNSNames must be in the same security domain.

MachineDNSName

When setting this property, this VARIANT can contain one of:

  1. A single BSTR with the hostname to use.

  2. A SAFEARRAY of BSTRs with the hostnames to use.

  3. A SAFEARRAY of VARIANTs of BSTRs with the hostnames to use.

We retrieving this property, the returned VARIANT will contain one of:

  1. A single BSTR with the hostname to use (if only 1 hostname is set).

  2. A SAFEARRAY of VARIANTs of the BSTRs of hostnames to use (if multiple hostnames are set.)

  3. VT_EMPTY if no hostnames are set.

Specifying more than one machineDNSName is useful only for failover. It is not possible to control the order in which multiple machines are connected.

Using 'localhost' will cause the local machine to be used.

Description

A description of why this serverdef exists.

Protocol

Determines how to communicate with the SAS Server. One of ProtocolCom, ProtocolCorba, or ProtocolBridge.

BridgeEncryptionLevel

Determines what gets encrypted when using the Bridge protocol. One of EncryptNone, EncryptUserAndPassword, or EncryptAll.

BridgeEncryptionAlgorithm

Encryption algorithm to use when using the Bridge. "SASPROPRIETARY" provides the weak SAS encryption algorithm.

DCOMSecurityService

Specify which security service to use when using DCOM. This does not apply to Bridge servers or local COM. This must be one of:

DCOMAuthenticationLevel
The authentication level to be used for DCOM servers. DCOMSecurityService must be NTLM or Kerberos for this property to take effect.

Methods

GetLogicalNames

Returns the logical server names associated with this SAS Server.

SetLogicalNames

Sets the logical server names assoicated with this SAS Server. The order of the names is not important.

IServerDefs

IServerDefs contains the standard collection methods Count, _NewEnum, Add, Item, and Remove where the key is the serverDefName. It also supports one additional method:

GetNamesByLogicalName([in]logicalName, [out, retval]SAFEARRAY(BSTR)*serverDefNames)

Returns an array of serverDef cn's that have the given logicalName.

Definition Persistence

Login and Server definitions can be stored using either LDAP or via an LDIF file.

LDAP

Persistence in LDAP is done through two objectclasses: sasServer and sasLogin. The BaseDN parameter of SetLDAPServer specifies the start of the SAS application tree.

LDIF file

LDAP has defined a standard for the textual interchange of LDAP data: the LDapInterchangeFormat.  The SAS Workspace Manager has the ability to read LDIF files.  This format is also supported by the SAS Spawner.  Note that LDIF specifies that each object definition start with a DN, but we don't actually know the full DN of any object.  The part that is not known is replaced with "$SUFFIX$" to allow administrators to use an automated search/replace mechanism should they want to import a file into LDAP.

Error Reporting

If the call succeeds in obtaining a workspace, the method returns S_OK. However, there are many reasons an error may occur. Sometimes an error can occur even before the connection atempt is made. An example of such an error is:

Other errors can occur during the connection attempt. Examples include:

Errors can occur even though a successful connection is established.  Errors that occur when connecting are reported either through the IErrorInfo mechanism (if no SAS workspace is created) or returned in the errorString parameter (if a SAS workspace is created.) The Err.Description (IErrorInfo->Description() in C) and errorString both use the same format which is XML.

If UseXMLInErrorInfo is set to false (it defaults to true), Err.Description will only contain a single error string, that refers to the last connection attempted.

Error XML

The error information returned through XML allows applications to fix the problems detected. Applications interested in fixing these errors will need to parse the XML and possibly provide a UI to the user, or send a message to the administrator to get the errors fixed so they don't occur again. Here is a sample of the error XML that gets generated (and returned in the errorString parameter of either CreateWorkspaceByLogicalName or CreateWorkspaceByServer) when there is a successful connection made, but the first attempt failed:

<connectionAttempts>
<connectionAttempt>
        <sasservername>MyServer</sasservername>
        <machineDNSName>alpine.com</machineDNSName>
        <saslogin>sascnn1</saslogin>
        <status>0x8004274d</status>
        <description>Could not establish a connection to the SAS server on the requested machine.
        Verify that the SAS server has been started with the -objectserver option or that the
        SAS spawner has been started. Verify that the port Combridge is attempting to connect to is the same as the port SAS (or the spawner) is listening on.</description>
</connectionAttempt>
<connectionAttempt>
        <sasserver>MyServer</sasserver>
        <machineDNSName>alpine.com</machineDNSName>
        <saslogin>sascnn1</saslogin>
        <status>0x0</status>
        <description>Connected</description>
</connectionAttempt>
</connectionAttempts>

Administration

For the SAS Workspace Manager, administration tasks include creating and saving LoginDefs, ServerDefs, and LogicalNameDefs. There are several alternatives for the administration of definitions. Definitions can be administrated directly through the IWorkspaceManager interfaces. These interfaces can write to LDAP and files. Note that the WorkspaceManager will only read/write those attributes that are of interest to it. If you are using files, any existing definitions or attributes that are not used by the Workspace Manager will be deleted, so caution should be used. Additional attributes that are defined for these objects are ignored.

Administration can also be performed on LDAP using a generic LDAP modification tool, such as LDIFDE (included with Windows2000) or ldapadd and ldapmodify (included with the UMich slapd distribution.)  LDIF files can also be imported to LDAP.

A favorite text editor could be used to modify the LDIF files.

Most (if not all) administration should be done using the SAS Integration Technologies Administrator, which consolidates the administration chores of the SAS Workspace Manager, the SAS Spawner, and SAS Publish/Subscribe technologies.

Note that it is not necessary to perform any administration at all. Definitions can be created, used, then discarded without ever being persisted.

Implementation Scenarios and Samples

Use Local SAS with ADO

        Dim obSAS As SAS.Workspace
        Dim obWorkspaceManager As New SASWorkspaceManager.WorkspaceManager
        Private Sub Form_Load()
        Dim obConnection As New ADODB.Connection
        Dim obRecordSet As New ADODB.Recordset
        Dim errorString As String

        Set obSAS = obWorkspaceManager.Workspaces.CreateWorkspaceByServer("MyServerName", VisibilityProcess, Nothing, "", "", errorString)

        obSAS.LanguageService.Submit "data a; x=1; y=100; output; x=2; y=200; output; run;"
        obConnection.Open "provider=sas.iomprovider.1; SAS Workspace ID=" + obSAS.UniqueIdentifier
        obRecordSet.Open "work.a", obConnection, adOpenStatic, adLockReadOnly, adCmdTableDirect
        obRecordSet.MoveFirst
        Debug.Print obRecordSet(1).Value
        End Sub

        Private Sub Form_Unload(Cancel As Integer) ' If we don't close SAS, the SAS process
                                                                                           ' may stay around forever
        If Not (obSAS is Nothing) Then
                obWorkspaceManager.Workspaces.RemoveWorkspace obSAS
                obSAS.Close
        End If
        End Sub

Using Remote SAS with ADO and No Persisted ServerDefs or LoginDefs

        Dim obSAS As SAS.Workspace
        Dim obWorkspaceManager As New SASWorkspaceManager.WorkspaceManager
        Private Sub Form_Load()
        Dim obConnection As New ADODB.Connection
        Dim obRecordSet As New ADODB.Recordset
        Dim obServerDef As New SASWorkspaceManager.ServerDef
        Dim xmlString As String

        obServerDef.Port = 6903
        obServerDef.Protocol = ProtocolBridge ' Multiple hostNames can be provided
                                                                                  ' for failover; order is unreliable.
        obServerDef.MachineDNSName = "bucky.alpine.com"

        Set obSAS = obWorkspaceManager.Workspaces.CreateWorkspaceByServer("MyServerName", VisibilityProcess, obServerDef, "UserABC", "ABC'sPassword", xmlString)
        obSAS.LanguageService.Submit "data a; x=1; y=100; output; x=2; y=200; output; run;"
        obConnection.Open "provider=sas.iomprovider.1; SAS Workspace ID=" + obSAS.UniqueIdentifier
        obRecordSet.Open "work.a", obConnection, adOpenStatic, adLockReadOnly, adCmdTableDirect
        obRecordSet.MoveFirst
        Debug.Print obRecordSet(1).Value
        End Sub

        Private Sub Form_Unload(Cancel As Integer)
        If not (obSAS is Nothing) Then
                obWorkspaceManager.Workspaces.RemoveWorkspace obSAS
                obSAS.Close
        End If
        End Sub
        

Establishing LDAP parameters

This example assumes that an administrator has already defined the "mvs" sasserver and the "BuckyBadger" login.

If the persistInRegistry parameter is true on SetLDAPServer, the provided LDAP URL is written to the system registry (HKEY_LOCAL_MACHINE).  If persistIRegistry is true on SetLDAPUser, then the LDAP User and Password are persisted in the CurrentUser registry.  An error will occur if persistInRegistry is True and the user does not have permission to write to the registry.

        set obWorkspaceManager = CreateObject("SASWorkspaceManager.WorkspaceManager")
        ' Remember that VBScript can't use VB enums...replace "ScopeGlobal" with 3,
        ' or add this line for VBSCript ScopeGlobal = 3
        obWorkspaceManager.Scope = ScopeGlobal
        obWorkspaceManager.SetLDAPServer "LDAP://mymachine.alpine.com:8001/o=Alpine Airways,c=US", True
        obWorkspaceManager.SetLDAPUser "cn=Dan,o=Alpine Airways,c=US", "DansLDAPPassword", True
        

List All Defined Server Defs in the Local Scope

This example assumes that an administrator has already defined the "mvs" sasserver and the "BuckyBadger" login.

        set obWorkspaceManager = CreateObject("SASWorkspaceManager.WorkspaceManager")
        obWorkspaceManager.Scope = ScopeUser
        for each def in obWorkspaceManager.ServerDefs
                debug.print def.Name
        next

ASP: Sharing a SAS Server on a Single Session

        if (session("sas") = nothing) then
                set obWorkspaceManager = CreateObject("SASWorkspaceManager.WorkspaceManager")
                set session("sas") = obWorkspaceManager.Workspaces.CreateWorkspaceByLogicalName("MyServer", VisibilityProcess, "mvs", "BuckyBadger", "")
        end if 

ASP: Sharing a SAS Server With All Sessions

' In the Global.asp file, OnApplication_Start() sub
set obWorkspaceManager = CreateObject("SASWorkspaceManager.WorkspaceManager")
set application("sas") = obWorkspaceManager.Workspaces.CreateWorkspaceByLogicalName("MyServer", VisibilityProcess, "mvs", "BuckyBadger", "")

Parsing the returned XML


        On Error GoTo CREATEERROR
        ' Establish LDAP/File parameters
        Set obSAS = obWorkspaceManager.Workspaces.CreateWorkspaceByLogicalName("mine", VisibilityProcess, "", "", errorXML)
        ' errorXML only gets returned when there is a successful connection, otherwise err.description gets the XML
        If (Len(errorXML) > 0) Then ParseXML errorXML


CREATEERROR:
        If (Err.Number <> SASWorkspaceManager.Errors.SEE_XML) Then
                GoTo TROUBLE
        End If

DISPLAYXML:
        ParseXML Err.Description
        GoTo NOTROUBLE

TROUBLE:
        Debug.Print Str(Err.Number) + ": " + Err.Source + ": " + Err.Description

NOTROUBLE:
End Sub


Private Sub ParseXML(xml As String)

        ' Another option would be to create some XML to make this look nice.
        ' Write the XML to a file so it can be parsed
        ' Form2 simply has a WebBrowser control in it
        Dim f As New Scripting.FileSystemObject
        Dim fname As String
        Dim tstream As Scripting.TextStream
        Dim xmlDoc As Object ' New MSXML.XMLDocument
        Dim ele As IXMLElement2

        fname = f.BuildPath(f.GetSpecialFolder(TemporaryFolder), f.GetTempName)
        Set tstream = f.OpenTextFile(fname, ForWriting, True)
        tstream.Write ("<html><body><xml id=""combridgeOutput"">")
        tstream.Write (xml)
        tstream.Write ("</xml><table datasrc=""#combridgeOutput""><thead><th>sasServer</th><th>sasLogin</th>")
        tstream.Write ("<th colspan=50>machineDNSName</th><th colspan=20>port</th><th colspan=40>status</th>")
        tstream.Write ("<th colspan=200>description</th></thead><tbody><tr><td><span datafld=""sasserver"">")
        tstream.Write ("</span></td><td><span datafld=""saslogin""></span></td><td colspan=50>")
        tstream.Write ("<span datafld=""sasmachinednsname""></span></td><td colspan=50><span datafld=""sasport"">")
        tstream.Write ("</span></td><td colspan=40><span datafld=""status""></span></td><td colspan=200>")
        tstream.Write ("<span datafld=""description""></span></td></tr></tbody></table></body></html>")
        tstream.Close
        Form2.WebBrowser1.Navigate "file://" + fname
        Form2.Show

End Sub


Contents Windows Client Development Previous Next