Using the PI-SDK with Microsoft .Net


The PI-SDK can be used in programs built with Microsoft .NET compilers by making use of COM Interop libraries. Version 1.3 of the PI-SDK delivers pre-built, strong named COM Interop libraries for its libraries as follows:

PI-SDK Library Interop Library
PISDK.dll OSIsoft.PISDK.dll
PISDKCommon.dll OSIsoft.PISDKCommon.dll
PITimeServer.dll OSIsoft.PITimeServer.dll
PISDKDlg.dll OSIsoft.PISDKDlg.dll
PISDKCtl.ocx OSIsoft.PISDKCtl.dll

Starting with version 1.3.0 of the PI-SDK these strong named libraries are installed in the Global Assembly Cache (GAC) during setup if the .Net Framework is installed on the machine. If the Framework is installed at a later date, these libraries can be installed in the GAC by running the Repair feature on the PI-SDK from the Add/Remove Programs applet. Manual installation in the GAC is described in this section as an aid to troubleshooting.

Why do I need Interop Libraries?

The Interop libraries are used to translate between data types exposed in the PI-SDK type library and those in the .NET Common Type System.

Using Visual Studio .NET it is possible to add a reference directly to a COM object by using the menu entry Project | Add Reference, selecting the COM tab, scrolling to and selecting the desired COM server and hitting OK. This generates an Interop library locally. While this is useful for local development, Interop libraries generated this way are not strongly named and therefore cannot be added to the GAC where they can be shared, and multiple versions maintained.

The strongly named versions of the Interop libraries installed with the PI-SDK contain an encrypted hash using public/private key technology. Building an application against a strongly named version results in the public key being recorded in the application's manifest. This allows the .NET runtime to ensure that the correct assembly has been loaded.

Installing Interop Libraries in the GAC

Starting with version 1.3.0, the PI-SDK installs the Interops in the GAC during setup if the .Net runtime is already installed. If .Net is installed later the Repair feature from Add/Remove Programs can be used.

The easiest way to install an Interop library in the GAC is to use the file system extensions, installed with the .NET tools. Using Windows Explorer, navigate to your Windows directory and click on the subdirectory named "assembly". The right pane, which normally shows files, should list the Global Assemblies installed on your computer along with their version, public key, and other details. Leave this window open and open another Windows Explorer window that points to the PISDK installation (e.g. Program Files\PIPC\PISDK). Select and drag the PI-SDK Interop assemblies (those files starting with "OSIsoft.") to the assembly window. Also navigate to the directory where the PITimeServer is installed (PIPC\library) and drag the OSIsoft.PITimeServer.dll to the assembly window. This will install the libraries in the GAC.

To perform this function from a command file you can use the utility gacutil.exe delivered with VisualStudio.Net.

gacutil /I filepath

Making Interop Libraries Available from the GAC

Starting with version 1.3.0, the PI-SDK handles these registry entries during setup if the .Net runtime is already installed. If .Net is installed later the Repair feature from Add/Remove Programs can be used.

Simply installing the Interop libraries into the GAC does not make them available in the list of .NET components in the Visual Studio.Net dialog accessed by the Project | Add Reference menu item. To make these libraries available in that context, changes need to be made to the registry.

Using regedit.exe navigate to the key

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders\

Under this key you should see pre-existing keys for Cyrstal.Net and Primary Interop Assemblies. The default value of each of these keys is a directory on your machine where the corresponding .NET libraries have been installed.

To enable the display of the PISDK Interop libraries, simply create two new keys in parallel with Crystal.Net and the Primary Interop Assemblies keys and set their default values to a REG_SZ value containing the full path to the directories where the PI-SDK Interop libraries were installed (PIPC\pisdk, PIPC\library). For example a key for the PITimeServer could look like:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders\PITimeServer

with the value:

d:\Program Files\PIPC\library

Once these keys are in place you must exit Visual Studio.Net and restart it for it to pick up these new references. At that point you can go to the Project | Add References dialog and see the PI-SDK Interop libraries under the .NET tab of the dialog.

Comments on Building .NET applications

Once the Interop libraries are installed you can begin writing code in .NET that makes use of the PI-SDK objects in managed code (VB.Net, C#, ASP.NET, etc.). This manual does not provide a tutorial on writing applications in .NET however a few differences are worth noting.

Top Level PISDK object

In VB.Net you can no longer rely on the automatic creation and destruction of the application object (PISDK). You must create the PISDK application object using the "new" keyword in order to access the objects in the PISDK hierarchy. Due to namespace collisions you will need to refer to the object with the fully qualified name, PISDK.PISDK when you "new" it.

Important: You should keep a reference to this top-level object while you need to use PI-SDK objects in your program and release it when your program is terminating or when you no longer need access to PI-SDK objects. This was handled automatically for you when using VB6.

What may not be obvious is that .NET can make variables declared on the stack available for release as soon they are no longer referenced in your code, even if they are still 'in-scope'. Thus, the PISDK object can be destroyed before the routine it is in completes. This may cause your program to fail. This optimization only occurs in release builds, so the problem may not be evident to the developer for some time. Below is an example of code that causes this problem. The code is a simple routine for posting message to the PI Message Log. Without modification, this routine would fail after a short period of time because the PISDK object will be destroyed by .NET.

private void STAThreadMain()
{
    PISDK.PISDK piSDK= new PISDK.PISDKClass();
    PISDK.MessageLog piMessageLog= piSDK.MessageLog;
    while (messageEvent.WaitOne())
    {
        if (piMessage.Length > 0)
        {
            piMessageLog.PutString(piMessage);
        }
        else
        {
            return; // shut down thread
        }
    }
}

One way to fix this is to reference the PISDK object at the end of the routine:

private void STAThreadMain()
{ 
    PISDK.PISDK piSDK= new PISDK.PISDKClass();
    PISDK.MessageLog piMessageLog= piSDK.MessageLog;
    while (messageEvent.WaitOne())
    {
        if (piMessage.Length > 0)
        {
            piMessageLog.PutString(piMessage);
        }
        else
        {
            piSDK= null; // this line added to keep piSDK from being released
            return; // shut down thread
        }
    }
}

Another way is to use the "using" statement:

private void STAThreadMain()
{ 
   PISDK.PISDK piSDK= new PISDK.PISDKClass();
   {
      PISDK.MessageLog piMessageLog= piSDK.MessageLog;
      while (messageEvent.WaitOne())
      {
         if (piMessage.Length > 0)
         {
            piMessageLog.PutString(piMessage);
         }
         else
         {
            return; // shut down thread
         }
      }
   }
   // this prevents garbage collection until
   // after this line.
   GC.KeepAlive(piSDK);
}

C# Item syntax

The VB syntax of using parenthesis for automatically accessing the Item method of a collection has been adopted and modified to a bracket syntax in C sharp, similar to C arrays. This works well in most instances, however if you are using an object whose Item interface uses a reference for the Index argument then this syntax won't work. Instead you need to call the Item method directly. This is true for the NamedValues collection and the PointList collection. For example:

using System;
namespace ConsoleApplication1
{
     /// <summary>
     /// Summary description for Class1.
     /// </summary>
     class Class1
     {
           /// <summary>
           /// The main entry point for the application.
           /// </summary>
           [STAThread]
           static void Main(string[] args)
           {
              object bar = 1;
              PISDKCommon.NamedValues foo = new
PISDKCommon.NamedValuesClass(); foo.Add ("foobar",ref bar); PISDKCommon.NamedValue nv; object baz = "foobar"; //nv = foo["foobar"]; This Doesn't Work nv = foo.get_Item(ref baz); } } }

Namespaces, PISDK and Server objects

When programming in .Net you can indicate that you do not intend to use the fully qualified name for objects in certain namespaces ("using" in C#, "Imports" in VB.Net). You can do this with the PI-SDK Interop libraries as well however you will still need to fully qualify references to PISDK (PISDK.PISDK) because the name is the library name as well as the top level object. You may also need to qualify the Server object (PISDK.Server) if you use libraries that make use of this relatively common object name.

Boxing

.NET divides data into value types and reference types. Languages prior to .NET allowed you to pass arguments by reference or by value as necessary (VB used ByRef and ByVal, C used pointers or values). In .NET, when passing a value type variable to a method that requires a reference (the method wants to change the value), a technique called "boxing" must be used. Boxing is explicitly converting a value type into a corresponding reference type. For example in C#:

short sTemp = 100;

// boxing

object objTemp = sTemp;

objTemp can now be passed to a method requiring a reference to a short.

Similarly you can "unbox" a variable to get the value type from a reference type, assuming the variable you are unboxing is of the desired type.

short sTemp2 = (short)objTemp;

In VB boxing looks like:

Dim iVal as Integer

iVal = 5

Dim objTemp as Object

objTemp = iVal

Unboxing in VB uses the Ctype function to coerce the type back to a value type

Dim iVal2 as Integer

iVal2 = Ctype(objTemp,Integer)

Output Arguments in a Failed Method Call

When making a method call on a COM object through .Net, the interops are responsible for marshaling the arguments from .Net to COM and back again when the method completes.  For standard types (blittable), marshaling can use references to the original variable, but with data types with a different representation in .Net than in COM, the marshaler copies and converts.  Standard marshaling, used in the PI-SDK interops ,does not bother marshaling back output arguments when a call fails, assuming this is needless work.  However, in certain PI-SDK calls, additional information is returned on a method failure, typically data clarifying  the error cause.  Unfortunately, because the interop marshaling decides not to copy this information, it is not returned when called through .Net. 

The methods that exhibit this behavior fall into three categories:

Calls that return additional error information specific to the problem encountered:
(Here the extra information is helpful but not essential)

PointAttributes.ModifyAttributes
PIModule.ModifyAttributes
PIHeading.ModifyAttributes
PIHeadingSet.ModifyAttributes
PIAlias.ModifyAttributes
PIProperty.ModifyAttributes
PIUnitBatch.ModifyAttributes
PISubBatch.ModifyAttributes
PIBatch.ModifyAttributes
PITransferRecord.ModifyAttributes
PICampaign.ModifyAttributes
IPIServers.GetObjServers
PIGlobalRestorer.RestoreServers
ListData.Snapshot
ListData.ArcValue
IPIPoints2.AddTags
IPIPoints2.EditTags
IPIPoints2.RemoveTags
IPointListEvPipeAccess.GetSnapshotEventPipe2
IPointListEvPipeAccess.GetArchiveEventPipe2

Calls that return additional information that one would expect to be empty in an error condition:
(Here the extra information is irrelevant when the method fails)

IPIValues2.GetValueArrays (arrays not returned on error)
PIAnnotations.GetItemWithIndex (index argument not updated if call fails)
PIModuleVersionList.Value (index argument is not updated if call fails)
PIAliasList2.GetItemWithIndex (index argument not updated if call fails)
PITransferRecordDB.GetTransferRecordsByItemUID (source and destination list not returned on error)
IEventPipe2.ListSignUps (primary and secondary signup IDs not returned on error)
PIPoints.PointLoaded (output PIPoint is not returned on error)
 

Calls that would return additional error information but are not currently implemented:

ListData.InterpolatedValues
ListData.PlotValues
ListData.RecordedValues
ListData.RecordedValuesAvailable
ListData.RecordedValuesByCount
ListData.RemoveValues
ListData.Summary
ListData.TimedValues
ListData.UpdateValue
PIIdentity.ModifyAttributes
PIIdentityMapping.ModifyAttributes

 

Enabling Operational Intelligence