Skip navigation
All Places > PI Developers Club > Blog > Authors cescamilla

PI Developers Club

8 Posts authored by: cescamilla

Ever wondered (like I did) when migrating from Unix and Linux as to what happened to the Soft Links and Hard Links? Those little things were so useful to have the same file copied all over the place without having to sacrifice the space you would otherwise need to keep real copies of that file?


I do miss them, in fact, I miss them quite a lot.


I was sharing some files (from a network share, a simple, samba, windows share) and I wanted a program to be able to access those files as if they were local (think of it kind of what the PI Server needs in order to access the subfolders in its root directory) when I stumbled upon the problem that linking is not available in windows. As such I started experimenting with NTFS and having a blast at the way it is so underused, to my amazement, NTFS does support Linking files!


Afterwards I found a program called "Hardlink Shell Extension" (which you can find here: *) and I played with it a bit. I realized that you can actually fool the operating system into liking windows shared files as normal files, this is useful for locking mechanisms, sharing media, video and data files, and a pletora of things. I wondered, could this be (mis)used  and fooling the PI Server into thinking that the PI Server is not in a share? Could it be a solution to have a shared folder running a cheap cluster? (note that there is no need for this since the PI Server has HA and it works great!) Could this be a solution to moving the configuration files into a single location in order to make editing easier?


I found a lot of questions but haven't tried everything that came to mind, however I would like to share this little tool with you, perhaps you find an interesting and useful way to use it.


Happy coding!




* please note that this is by no means endorsed by or supported by OSIsoft, Microsoft or any other party and that what is expressed here is the point of view of just one person

OSIsoft vCampus Live! 2010 was a big success, thanks to all the support and involvement from the community - vCampus members provided ideas on topics they'd like to see covered, and many of them even participated as speakers during the event. Congratulations and thanks to all of those who participated -directly or indirectly- in making this event such a great success, thanks go to all the speakers, presenters and attendees!


Remember, OSIsoft vCampus Live! this is YOUR event... help us make it better! Simply give us feedback and write your suggestions, by starting a discussion thread or dropping us a note at Got something you'd like to present at next year's event? Tell us have about it! We've already started thinking about 2011, so the sooner you let us know your suggestions, the better the event will be. And a big thank you to all who have already given us their comments, feedback and requests - keep it coming!


One example of the changes we made based on your feedback, is the new 'Vox Pop' open sessions - these were a success and are a clear example of how much we value your suggestions and react on them! We are looking forward to getting the community even more involved for OSIsoft vCampus Live! 2011, scheduled to take place on the week of September 12th at the Palace Hotel in San Francisco. Mark your calendars now and join us!!





Hello fellow vCampus members!


As Matt pointed out in his blog post, we will be recognizing the most helpful members in our community by making them vCampus All-Stars for this year! And that's where you can help make somebody proud and happy: we invite you to nominate YOUR All-Stars – simply email us with the name of the person(s) that helped you/the community the most (preferably not a vCampus Team Member, so please exclude Steve and the rest of the team from this award ).


What will these members get? In addition to being recognized by the community, they get:

  • Personal vCampus blog (if desired)
  • Moderate vCampus forums (if desired)
  • Voluntary participation to team meetings
  • Free admissions for the year to come
    • OSIsoft vCampus & OSIsoft vCampus Live
    • Users Conference
  • A few more surprises…

We will take into consideration each and every nomination you send so let the nomination begin!


The official version of this document available in the vCampus Library check under White Papers and Tutorials, PI WebParts. Feedback on either the official version or this blog post is welcome and appreciated. This blog post will be updated soon to reflect all the changes included in the vCampus Library version. Happy Coding! .- Kryz


Using SharePoint Services is really fulfilling when you are doing simple things, but to really use it at all its possibilities you would need to get additional Web Parts, either created by someone else (like OSIsoft's RtWebParts) or creating one Web Part yourself. OSIsoft provides a great range of Web Parts that allows the end user to do a great deal of things, but there is always one specific need that is not shared and that does not fit with what we currently have at our disposal, is in this case where we need to connect to (or from) a different data source to the PI Server data and integrate all of this in a seamless/Client&Server/web interface.


In order to integrate all of this into Sharepoint you will need to create a Custom Web Part that connects with OSIsoft's RtWebParts and understand a little bit of the fundamental parts of an RtWebpart, this will not be an easy path, but I'm sure you'll find a great guide in this tutorial.

The Contents of a Web Part Page

A WebParts Page has zones, and each zone may have WebParts, the webparts interact with each other sending and receiving values, parameters, row values, etc. Some webparts can even send parameters across diferent WebPart Pages.




We will be using a modified version of this step by step tutorial, the main reason for this is that it somewhat does what we need, but in order to make it really compatible we need to make a few changes that are depicted in full detail here. Please keep this in mind as there will be some steps that won't be needed here.


  • Visual Studio 2008 and at least the Service Pack 2
  • Windows Server 2003 or  2008 (or any other version that supports SharePoint Services)
  • Visual Studio Extensions for SharePoint Services 1.3 or later.
  • SQL Server [Express] 2005 or 2008 with latest service pack
  • OSIsoft's RtWebParts installed on the SharePoint Server
  • PI Server in the same network (or in the same computer, note that having the server in the same computer is not recommended for a production enviroment)

Creating a base Web Part

  • I will be using Visual Studio 2008 with Extensions for SharePoint Services 1.3, this is how the splash screen for my visual studio 2008 looks in my computer.

  • I created a new project using the new project menu as illustrated in the following picture

  • Navigate to Visual C#, locate SharePoint, and select WebPart

  • When you get asked to select trust level please select Full trust (Deploy to GAC) and click Ok. (this will only appear if you have Service Pack 2 installed in Visual Studio 2008)

  • You will be greeted with a default WebPart already created, your screen should look a lot like this:
  • I selected the WebPart1.cs and renamed it to 'Tut_WP_OSI.cs', when you get asked if you want to rename all associated Web Parts, click YES. (this will only appear if you have Service Pack 2 installed in Visual Studio 2008)

  • I changed the base class from System.Web.UI.WebControls.WebParts.WebPart to the Microsoft.SharePoint.WebPartsPages.WebPart (which is a little bit older, but compatible with OSIsoft WebParts) doing the following code:

    //Change it from this ...
    public classTut_WP_OSI : System.Web.UI.WebControls.WebParts.WebPart

    //... to this
    public classTut_WP_OSI : Microsoft.SharePoint.WebPartPages.WebPart

    I also uncommented the provided 'Hello world' code and compiled it. Please note that it will say that our base class is not CLS-compliant. I sincerely do not know what this means (evil grin)

    //Uncomment those lines:
    Label label = new Label();
    label.Text = "Hello World";


  • Testing the base WebPart

  • For the fun part, go to your sharepoint site and navigate to the Document Center, mine is conveniently located at localhost

  • Under Site Actions select Create as illustrated in the next picture

  • Under Web Pages select Web Part Page

  • I named the WebPage tut_webpart_conn.aspx and selected Header, Footer, 3 Columns then clicked create

  • I wanted to add the recently compiled webpart to the Left Column

  • I found the WebPart under Miscellaneous as expected.

  • Everything seems to work fine in edit mode. and I sincerely believe it to be better to build upon a working webpart than walking blindfolded.

  • I was kindly greeted by the 'Hello world' comment, that means that everything is working in run mode too.

  • Making the WebPart Connectable

    (Note that the steps on this section correspond to the ones in this web page)
  • Step 1: Create the Interface Class This is achieved by adding the interface IParametersOutProvider to the base clase

    //add this to the 'using' definitions
    using Microsoft.Sharepoint.WebPartPages.Communication;

    //Change it from this ...
    public classTut_WP_OSI : Microsoft.SharePoint.WebPartsPages.WebPart

    //... to this
    public classTut_WP_OSI : Microsoft.SharePoint.WebPartsPages.WebPart,
      IParametersOutProvider//step 1

  • Step 2: Declare Events We need to add the events that this WebPart will handle and we need to include the WebPartPages.Communication namespace as follows:

    //add this to the WebPart Class
    public event NoParametersOutEventHandler NoParametersOut;
    public event ParametersOutProviderInitEventHandler ParametersOutProviderInit;
    public event ParametersOutReadyEventHandler ParametersOutReady;

    Those are the events that are expected by this interface (and by the client side scripting later on).

  • Step 3: Override the EnsureInterfaces method, and then call the RegisterInterace method
    We need to register the interface we want to expose to other webparts.

    //Add this to our WebPart class
    public override void EnsureInterfaces()
      RegisterInterface("TagListProvider_WPQ_", InterfaceTypes.IParametersOutProvider,
                        UnlimitedConnections, CanRunAt(), this, "TagListProvider_WPQ_",
                        "Provide a list item to", "Provides a list item to a connected webpart");

    It is important to pass the first parameter just as it is, we need the '_WPQ_' or SharePoint will refuse to load it as it won't have a unique name. also it has to be the same name the client side scripting has or it won't get called.

  • Step 4: Override the CanRunAt Method

    //Add this to our WebPart class
    public override ConnectionRunAt CanRunAt()
      return ConnectionRunAt.Server;


  • Step 5: Override the PartCommunicationConnect method

    //Add this to our WebPart class
    private bool paramsOutConnected = false;

    public override void PartCommunicationConnect(
      string interfaceName, Microsoft.SharePoint.WebPartPages.WebPart connectedPart,
      string connectedInterfaceName, ConnectionRunAt runAt)
      paramsOutConnected = true;

    This tells the SharePoint Server that we need a full refresh to show the information (a fact that we will change later with client side scripting)

  • Step 6: Override the PartCommunicationInit method

    //Add this to our WebPart class
    public override void PartCommunicationInit()
      ParametersOutProviderInitEventArgs parametersOutProviderInitEventArgs =
      parametersOutProviderInitEventArgs.ParameterOutProperties = newParameterOutProperty[1];
      parametersOutProviderInitEventArgs.ParameterOutProperties[0] = newParameterOutProperty();
      parametersOutProviderInitEventArgs.ParameterOutProperties[0].ParameterName =
      parametersOutProviderInitEventArgs.ParameterOutProperties[0].ParameterDisplayName =
        "Selected Tag";
      parametersOutProviderInitEventArgs.ParameterOutProperties[0].Description =
        "The selected tag name";
      ParametersOutProviderInit(this, parametersOutProviderInitEventArgs);

    I hate telling you a bounch of "do nots" but be really sure that this section matches the text you put on the client side JavaScript file, because if it is different by any bit it won't get called even if it is on the webpage.

  • Step 7:  Override the PartCommunicationMain method

    //step 7
    private DropDownList dropDownList;

    public override void PartCommunicationMain()
      ParametersOutReadyEventArgs parametersOutReadyEventArgs = newParametersOutReadyEventArgs();
      parametersOutReadyEventArgs.ParameterValues = new string[1];
      parametersOutReadyEventArgs.ParameterValues[0] = dropDownList.SelectedValue;
      ParametersOutReady(this, parametersOutReadyEventArgs);


  • Step 8: Override the GetInitEventArgs method
    There is no need to override the GetInitEventArgs method as we will not be providing conversion mechanisms to connect to different types of webparts.
  • Step 9:Implement the interface event handlers
    The interface we are using has no must-implement event handlers, so we can skip this step as well.
  • Step 10: Override the RenderWebPart method

    //Step 10
    protected override void RenderWebPart(HtmlTextWriter output)
      if (paramsOutConnected)
        dropDownList.Attributes["onchange"] =
          "javascript:__doPostBack('" + this.UniqueID + "', '');";

    We need to tell the webpage to do a full page refresh.

  • Step 11: Implement supporting methods

    //Step 11
    using System.Web.UI.HtmlControls;

    textData = "Sinusoid\r\nCDT158\r\nSinusoidU";

    protected override void CreateChildControls()

        HtmlTable htmlTable = newHtmlTable();
        HtmlTableRow tr = newHtmlTableRow();
        HtmlTableCell tc = newHtmlTableCell();
        Label titleLabel = newLabel();
        titleLabel.Font.Bold = true;
        titleLabel.Text = "Value";
        tc = newHtmlTableCell();

        dropDownList = new DropDownList();

        string[] arrText =
          textData.Split(new string[1] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
        for (int i = 0; i < arrText.Length; i++)
            dropDownList.Items.Add(newListItem((string)arrText[ i ],
                                                (string)arrText[ i ]));

    Here we define the controls we want to see when the webpart is rendered, we could even hide that if the webpart is not connected. But for the time being we will just use that as it is.

  • Testing the Connectable WebPart

  • Once again I do prefer to see it live and kicking, so here I went back to the website to see if the changes we have made so far are working correctly, and I can see the dropdown and the content just fine. We are on the right track.

  • I would like to see if we can connect this webpart to another webpart (an OSIsoft one) so I'll be adding a RtTrend for the sake of testing. Under Site actions click on EditPage

  • Click on the center webpart zone that reads Add a webpart

  • Look for and select RtTrend then click the button Add

  • Back on the newly added webpart click under Edit go to Connections select Receive parameter values from... and finally click on the Tut_WP_OSI Web Part

  • Now that it is connected, we need to select were we want to use the parameter that comes in, so we select edit from the RtTrend Web Part and then click on Modify Shared Web Part

  • Look for the Selected Data section in the RtTrend Property pane and click on the ligthning bolt

  • You'll be presented with another window that has all the Web Parts that are connected to this one and, after selecting a webpart, all the connection fields (parameters) that it is exposing. Click on Tut_WP_OSIWebPart and then on Value close the window by pressing the Ok button

  • Close the property pane by pressing the OK button

  • It is working, at least for the first parameter!

  • Clicking on the second one shows the second tag, after that I clicked on the dropdown again just to show it off.

  • Adding a property and making the WebPart web editable

  • Since we would not want our webpart to have the same taglist all the time and sure it would be nice to let the end-user select the tags they want to display, then we will make this webpart web editable.
    Step 12: Add the IWebEditable interface and the property

    //Modify the class interfaces by adding IWebEditable
    public classTut_WP_OSI : Microsoft.Sharepoint.WebPartsPages.WebPart,
      IParametersOutProvider//step 1

    //... like this
    public classTut_WP_OSI : Microsoft.Sharepoint.WebPartsPages.WebPart,
      IParametersOutProvider, IWebEditable//step 1, 12

    //Move the textData definition up and change it to
    private string textData = string.Empty;

    //add this to the 'using' definitions
    using System.ComponentModel;

    [WebDisplayName("Text Data"),Category("Configuration"),

    public string TextData
      get { return textData; }
      set { textData = value; }

    object IWebEditable.WebBrowsableObject
      get { return this; }


  • Step 13: Create the EditorPart based class

    class Tut_WP_OSI_Editor : EditorPart //step 13
      private TextBox textData;
      public Tut_WP_OSI_Editor()
        ID = "Tut_WP_OSI_Editor";

      protected override void CreateChildControls()
        Label label = new Label();
        label.Text = "Data";
        Controls.Add(new HtmlGenericControl("br"));
        textData = new TextBox();
        textData.TextMode = TextBoxMode.MultiLine;
        textData.Rows = 5;
        textData.Wrap = false;
        textData.Width = 200;
        Controls.Add(new HtmlGenericControl("br"));

    public override bool ApplyChanges()
        Tut_WP_OSI listProvider = (Tut_WP_OSI)WebPartToEdit;
        listProvider.TextData = textData.Text;
        return true;

      public override void SyncChanges()
        Tut_WP_OSI listProvider = (Tut_WP_OSI)WebPartToEdit;


  • Step 14: Create the CreateEditorParts() method 

    //add this to the 'using' definitions
    using System.Collections.Generic;

    //step 14
    EditorPartCollection IWebEditable.CreateEditorParts()
      List<EditorPart> editorPartList = new List<EditorPart>();
      editorPartList.Add(new Tut_WP_OSI_Editor());
      return new EditorPartCollection(editorPartList);

    This section returns the object needed when and while in edit mode.

  • Testing the WebPart's web editable property

  • That's it for adding a web editable property to a webpart. We can see it is still working at the webpart page.

  • How about testing it out, let's click on the arrow and then on Modify Shared Web Part

  • This should bring us to the properties pane, and we should be able to see and edit the Data section and I've put "CDT158", "SINUSOID" and "SINUSOIDU", one per line. And then clicked on Ok

  • We can see the Web Part working correctly.

  • We still need to test out if it can change the tag by clicking on the dropdown.

  • And now we have a Server Side connected WebPart!

  • Adding client side scripting

  • Add the JavaScript file to the project (note that you could have as easily added an existing item)
  • I will name this new item TagListProvider.cs and select it to be a JScript File
  • You can paste the following code, (just the yellow patches) you'll see some comments around them, mainly it is the client side code for the interfaces we have defined in the server side code.

    <SCRIPT LANGUAGE="JavaScript">
    // TagListProvider object is the client-side implementation of the ParametersOutProvider interface
    function TagListProvider(wpq, dropDownListClientID, title) {
        this.WPQ = wpq;
        this.DropDownListClientID = dropDownListClientID;
        this.Title = title;

        // ParametersOutProvider interface
        this.RaiseParametersOut = _raiseParametersOut;
        this.PartCommunicationInit = _partCommunicationInit;
        this.PartCommunicationMain = _partCommunicationMain;
        this.ParametersOutReady = _parametersOutReady;
        this.NoParametersOut = _noParametersOut;

        // prepare parameters to be passed through the connection(s)
        function _raiseParametersOut()
            var parametersOutReadyEventArgs = new Object();
            parametersOutReadyEventArgs.ParameterValues = new Array(1);
            var dropDownList = document.all(this.DropDownListClientID);
            var selectedValue = dropDownList.options[dropDownList.selectedIndex].value;
            parametersOutReadyEventArgs.ParameterValues[0] = selectedValue;

        // initialize connection information
        function _partCommunicationInit()
            var parametersOutProviderInitEventArgs = new Object();
            parametersOutProviderInitEventArgs.ParameterOutProperties = new Array(1);
            parametersOutProviderInitEventArgs.ParameterOutProperties[0] = new Object();
            parametersOutProviderInitEventArgs.ParameterOutProperties[0].Description =
              "List of tags";
            parametersOutProviderInitEventArgs.ParameterOutProperties[0].ParameterDisplayName =
              "Tag List";
            parametersOutProviderInitEventArgs.ParameterOutProperties[0].ParameterName =
            parametersOutProviderInitEventArgs.ParameterOutProperties[0].Required = false;
            WPSC.RaiseConnectionEvent("TagListProvider" + this.WPQ,
              "ParametersOutProviderInit", parametersOutProviderInitEventArgs);

        // entry point for connection
        function _partCommunicationMain()

        // signals the connection infrastructure that new values are ready
        function _parametersOutReady(parametersOutReadyEventArgs) {
            WPSC.RaiseConnectionEvent("TagListProvider" + this.WPQ, "ParametersOutReady",

        // special function for when there are no parameter values to pass
        function _noParametersOut()

    I know I wrote this before but: be sure, make really sure you use the same parameters here as in the previous section or those two objects (the client side and the server side) will not play well with each other.
    Someone commented that if you dare to remove the "title" variable from the JavaScript section it will break the ability to connect to other webparts. So please be aware of that if you try to compress or minimize the JavaScript code.
  • Change its compilation action to embedded resource
  • Step 15: Modify the "CanRunAt" function

    //Add this to our WebPart class
    public override ConnectionRunAt CanRunAt()
      if (this.BrowserDesignMode) return ConnectionRunAt.Server;
      return ConnectionRunAt.Client;

    We need to run in server mode while in edit but we want it to run in client mode while not in edit mode, so we use the 'if' for that.
  • Step 16: Modify the GetScript Method

    private string GetScript() {
      string JavaScript = string.Empty;

      // get a reference to the current assembly so we can get to the ResourceManager
      System.Reflection.Assembly thisAssembly =

      // the name of the resource is dependent on the project's folder structure
      Stream scriptStream =
          this.GetType(), "Tut_WP_OSI.TagListProvider.js");

      using (StreamReader scriptReader = newStreamReader(scriptStream))
        JavaScript = scriptReader.ReadToEnd();

      return JavaScript;

    This is the method Greg used to get the file from the assembly, and I liked that idea.
  • Step 17: Modify the OnPreRender Method

    //step 17
    protected override void OnPreRender(EventArgs e)
        if (CanRunAt() == ConnectionRunAt.Client)
          dropDownList.Attributes["onchange"] =
          if (!Page.ClientScript.IsClientScriptBlockRegistered("ListProvider"))
              typeof(string), "ListProvider", GetScript());
          string initScript =
            @"var TagListProvider_WPQ_ = new TagListProvider('_WPQ_', '{0}', '{1}');";
          initScript = string.Format(
            ReplaceTokens(initScript), dropDownList.ClientID, this.StorageKey);
            typeof(string), ReplaceTokens("TagListProvider_WPQ_"), initScript, true);

    We need to get the JavaScript client side script to the client, and this is the place to do it, it won't work on "WebPartPreRender" as it get's executed too late, so we need to override this section in order to inject the JavaScript code.
  • Testing client side scripting

  • This is best done on the webpage, so it is left as an excercise to our readers, do not forget to test it while the scroll position is down so you can see how the data changes without refresing the whole web page.
  • Try adding twice the webparts and verifying that they play well wich each other.


  • Web Parts is a mature product and a really complex service from Microsoft, it is as complex as it is capable. So, be aware of what you are getting into.
  • The OSIsoft Provided Web Parts work almost flawlessly. That should take a lot of load out of our backs. And they integrate quice nicely, so use them as much as you want.
  • Tips

  • When creating a custom Web Part please do not forget the Shampoo's motto: Code, Test, Repeat.
  • For JavaScript loading save yourself a lot of headaches and override the PreRender method and use it for this.
  • All the connection information typed in the aspx source file must be identical to its JavaScript source part or the Web Part won't work.
  • To Do

  • The configuration does not get read from the storage after it was originally modified.

Wordsize Trends

Posted by cescamilla Aug 10, 2009


Showing data from a PI Server in a trend fashion in PI ProcessBook or Excel (with PI DataLink) is really simple (even for most end-users or at least that is what we like to think, thanks to the great smart clients OSIsoft provides :). However there are some custom devices, graphic sizes and specific graphics that cannot be created with the amazing trend control included in PI ProcessBook - a control that is only available inside PI ProcessBook and (in a different flavor) inside Excel.


 There also is the issue of having an effective way to show information about some values in a Word document, or a printed piece of paper. And this is exactly what we will attempt here: say you want to know the values for tag CDT158cdt158.png and its trend for the last 24 hours in an easy to read and easy to understand way inside a text flow (like this one). We'll achieve this programmatically and you can use this code anywhere - from mobile devices to a Word add-in.


In order to show trend data into a custom size trend we may need to go back to high school and remember how to do a graphical representation of data - the hardest part being to fit and fill the X (horizontal) axis as it is mapped to the 'time' variable. We'll have another problem too: graphics have the 'origin' or 'zero' vector at the bottom-left corner of the image but the graphic devices have the 'origin' or 'zero' vector at the top-left corner, so this has to be taken in consideration before plotting the values (otherwise we will get upside-down graphics).

The Contents of a Graphic

They say an image is worth 1 Megapixel (hehehehe) and here we go, our first image:




  The blue boxes show the way the computer screen is addressed.
  The orange boxes show the way a trend is interpreted.
  The purple part is the trending area.


  We need to keep this in mind as we draw the trend step-by-step.

Connecting to the PI Server

For simplicity sake I'll be using the OSIsoft.PISDK.Controls.ServPickList control, which is part of OSIsoft.PISDK.Controls.PISDKCtrlDlg Assembly and has the SelectedServer.Open() Method and the SelectedServer.Connected Property which we will be calling. If you do not have the PI SDK Control Dialogs in your Visual Studio 2008 toolbox then:

  1. Choose items
  2. Select Browse
  3. Navigate to \PIPC\PISDK and select the file OSIsoft.PISDK.Controls.PISDKCtrlDlg.dll
  4. Now you should be able to add [ServPickList / OSIsoft.PISDK.Controls] to your toolbox

Let’s code!

  1. Go to File, New Project, Windows Form Application
  2. Add the following controls to the Main Form
    • ServPickList (srvPickList1)
    • Button (buttonConnect)
    • ComboBox (tagComboBox)
    • 2 DateTimePicker (dateTimePickerStart, dateTimePickerStop)
    • PictureBox (pictureBox1)
    • [optional] Timer (timer1)
      The beauty of using the PI SDK's Server Pick List control is that we do not need to do anything else: just 'drag and drop' it into the form and that's all we need to make it work.

  3. Now we want to assign/create the "click" action to the 'buttonConnect'
    • Check if the selected server is connected, open a connection if it is not and enable the tag's control
    • Clear the Tag's control
    • Populate the ComboBox control with all the tags available from the selected server.

private void buttonConnect_Click(object sender, EventArgs e)
    //Check if the SelectedServer is connected, if it is not, then
    //open a connection to the server and enable the tagComboBox control.

    if (!servPickList1.SelectedServer.Connected)
        //open selected server
        //enable tag combobox.
        tagComboBox.Enabled = true;

    //Clear the tagComboBox (in case it is not the first click we get)

    //Populate the ComboBox with all the tags available from the selected server
    foreach (PISDK.PIPoint tmpPoint in servPickList1.SelectedServer.PIPoints)
        //Add point name to tag combo box


The result should look like this:

  1. We need to set the date of the date pickers so let's set the start time to 24 hours ago (*-1d) and the stop time to now (*). Note that we could make it the "now" change dynamically but this is beyond the scope of this article.

    Add the following lines in the Form1's "Load" event:

//Set the start date to 24 hours ago
dateTimePickerStart.Value = DateTime.Now.AddDays(-1);
//Set the stop date to now
dateTimePickerStop.Value = DateTime.Now;

  1. Call In the ComboBox we will need to change the "SelectedIndexChanged" event and add a function called update_trend() which we will define in the following step, for now it is enough to prototype it like:

private void update_trend(){}


//Index changed should contain this:

  1. The DateTimePicker controls should show the exact selected date with the seconds... so, let's change their properties to do so:
    • Change the 'Format' property to 'Custom'
    • Then change the 'CustomFormat' property to something meaningful (I'll use the standard PI format: dd-MMM-yy HH:mm:ss)
      Make sure you do this for both date time pickers.

  2. Finally (and for the most fun) we need to create a PictureBox (to output the trend we will be creating...), I'll allow the PictureBox to occupy most of the space left on my form and I'll set the 'Anchor' property to all borders (Top, Bottom, Left, Right) this will make the image resize as the user resizes the window.
  3. Now we need some global variables:
    (integers) XX and YY (size of the bitmap in pixels - horizontally and vertically, respectively)
    (Bitmap) mybitmap - The image we will be using and setting to the pictureBox1
    (Grpahics) myGraphic - Encapsulator, I'll use this to expose extra methods (DrawLine, FillRectangle, Clear)

  int XX = 640; //Horizontal resolution
  int YY = 480; //Vertical resolution
  Bitmap myBitmap; //The bitmap image I'll be using to display the trend
  Graphics myGraphic; //The graphics device needed to expose
                      //some additional methods (Clear, FillRectangle and DrawLine)

  1. Let's now define the update_trend() method,
    Note that this method may be a bit complex if you have never made custom graphics.

    Essentially, the update method needs to:
    • Check if we are connected to the selected PI Server and if the user has selected a tag
    • Create variables for the maximum and minimum values
    • Get the 'zero' and 'span' attributes for this particular tag
    • Determine the dimensions of the trend given the values to graph
    • Create a Graphics object from the Bitmap and clear it with a white background.
    • Set 'zero' to be the Maximum value of zero or pointMin
    • Set 'span' to be the Minimum value of (zero + span) or pointMax
    • Fill a rectangle from (0, span - zero) to (Horizontal size of the bitmap, (Vertical size of the bitmap - zero) / range)
    • Get all necesary values from the tag in the pi server, pair them and draw lines between them if both are numeric values (of type "System.Single")
    • Find the last numerico value from the list and draw a red rectangle at the right end of the graphic with that value
    • Show the bitmap

  private void update_trend()
   //Check if we are connected to the selected server in the server picklist and if
   //the user has selected a tag in the ComboBox
   if (servPickList1.SelectedServer.Connected)
    //Get the tag name from the selected item in the ComboBox
    string tagname = tagComboBox.SelectedItem.ToString();
    //Get the point object
    PISDK.PIPoint point = servPickList1.SelectedServer.PIPoints[tagname];
    //Get values from the PI Server
    PISDK.PIValues myValues = point.Data.PlotValues(dateTimePickerStart.Value,

    //Variables to help define the height of the trend
    float? NpointMax = null, NpointMin = null;
    float pointMax = 0, pointMin = 0;

    //Get zero from PI Points Database
    float zero = (float)point.PointAttributes["zero"].Value;
    //Get span from PI Points Database
    float span = (float)point.PointAttributes["span"].Value;

    //Check all values to find out the maximum and minimum values
    foreach (PISDK.PIValue tmpValue in myValues)
     //If it has a numeric value
     if (tmpValue.Value.GetType().ToString() == "System.Single")
      //Ff vertical maximum has a value
      if (NpointMax.HasValue)
       //Then keep the highest one (NpointMax or the current item value)
       NpointMax = Math.Max((float)NpointMax, (float)tmpValue.Value);
       //Ff no value, set it to the tag's value
       NpointMax = (float)tmpValue.Value;

      //If vertical minimum has a value
      if (NpointMin.HasValue)
       //Then keep the lowest one (NpointMin or the current item value)
       NpointMin = Math.Min((float)NpointMin, (float)tmpValue.Value);
       //If no value, set it to the tag's value
       NpointMin = (float)tmpValue.Value;

    //if NpointMax is not null
    if (NpointMax.HasValue)
     //then set pointMax to NpointMax
     pointMax = (float)NpointMax;

    //if NpoinMin is not null
    if (NpointMin.HasValue)
     //then set pointMin to Npointmin
     pointMin = (float)NpointMin;

    //Calculate the range
    float range = pointMax - pointMin;
    //Add 3% of Span up
    pointMax += Convert.ToSingle(range * 0.03);
    //Add 3% of Span down
    pointMin -= Convert.ToSingle(range * 0.03);
    //Minimum range is 1, range is always positive
    range = Math.Max(1, pointMax - pointMin);
    //Should be constant used for the graphics part
    long ticks = dateTimePickerStop.Value.Ticks - dateTimePickerStart.Value.Ticks;

    //Encapsulate the Bitmap into a Graphics object so we can use the Draw* methods
    myGraphic = Graphics.FromImage(myBitmap);
    //Set the Graphics background to white (using the encapsulation as a mean)

    //Get zero value from PI Database's zero
    zero = Math.Max(zero, pointMin);
    //Get span value from PI Database's span
    span = Math.Min(span, pointMax);

    //Fill the rectangle area that is included in the painted area
    //From left, top
    0, YY - (span - pointMin) * YY / range,
    //To right, bottom
    XX, (YY - (zero - pointMin) * YY / range) - (YY - (span - pointMin) * YY / range) );

    //Loop through the values to display them
    for (int x = 1; x < myValues.Count; x++)
     //If this value and the next one are numerical
     if (myValues[x].Value.GetType().ToString() == "System.Single" &&
         myValues[x + 1].Value.GetType().ToString() == "System.Single")
      //Draw a line between two points
       //Current date ticks minus first date ticks times pixels divided by ticks
       (myValues[x].TimeStamp.LocalDate.Ticks - myValues[1].TimeStamp.LocalDate.Ticks) * XX / ticks,
       //Image size vertically minus minimum point minus current value
       //times Image size vertically divided by range

       YY - ((float)myValues[x].Value - pointMin) * YY / range,
       //current date ticks minus next date ticks times pixels divided by ticks
       (myValues[x + 1].TimeStamp.LocalDate.Ticks - myValues[1].TimeStamp.LocalDate.Ticks) * XX / ticks,
       //Image size vertically minus minimum point minus next value times Image
       //size vertically divided by range

       YY - ((float)myValues[x + 1].Value - pointMin) * YY / range);
       //The previous lines are necessary to flip, scale, move and translate the
       //graph to an usable region on the graphics device.


    //Find the last numerical value
    for (int  x = myValues.Count; x > 0; x--) {
     if (myValues[x].Value.GetType().ToString() == "System.Single")
      //draw a rectangle in that position
      myGraphic.FillRectangle(Brushes.Red, XX - 2,
                              YY - 2 - ((float)myValues[myValues.Count].Value - pointMin) * YY / range,
                              2, 3);

    //Set the image source of the picture box to the current bitmap.
    pictureBox1.Image = myBitmap;

  1. Now down to the final details: prepare the trend area when the form loads.

  private void Form1_Load(object sender, EventArgs e)
  //Set the width of the picture box
   XX = pictureBox1.Width;
   //Set the height of the picture box
   YY = pictureBox1.Height;
   //Create an empty bitmap
   myBitmap = newBitmap(XX, YY);

  1. That's it... now we can run it and see if it works!
  2. And... for the final act, you can polish by adding this to the resize function:

private void Form1_Resize(object sender, EventArgs e)
   //Set the width of the picture box
   XX = pictureBox1.Width;
   //Set the height of the picture box
   YY = pictureBox1.Height;
   //Create a new bitmap at least 1 in size
   myBitmap = newBitmap(Math.Max(1,XX), Math.Max(1,YY));
   //Redraw trend

  1. Optionally, add a (5 seconds?) timer and add this to its 'Tick' event:

private void timer1_Tick(object sender, EventArgs e) {
  //Update the "end" date time picker
  dateTimePickerStop.Value = DateTime.Now;


Thanks for reading and stay tuned for more!

Last bits

  • As you can see custom trending in your own application/device/console is not difficult.
  • There are a lot of resources for trending and shoThis code does not check for errors, server changes, sign up for updates nor does it handle digital states in tags.
  • This can be used as an image in a web service, or any other kind of application. (More to come about that).. (More to come about that).


Last time edits and corrections

  • Andreas suggested the use of PlotValues instead of Recorded values as PlotValues get the least number of values needed to draw a trend at a given horizontal-pixel resolution.
  • Steve suggested the use of Tagsearch instead of a dropdown list for large systems. ComboBox control remained but usability was changed
  • Error checks are not performed
  • Some objects changed their text value (like the button1) sorry about that, the name in code was not changed though.
  • Wait for PART 2 of this post where it is going to be shown how to use and catch digital state values.
  • Project file is attached in the post.



Live, from the UC 2009!

Posted by cescamilla Apr 1, 2009

You can feel the eagerness in the air of this fresh morning tainted by the smell of Transpara's coffee machine, distant noises make us aware of the day that is about to begin. This is a dawn for a new year, OSIsoft's new year, it will be marked with new developments, fresh ideas, new content, and more news than even before. In a constant evolving world and with Microsoft telling us that they do not have a plan for the next 6 months as there is no visibility into what the future will bring we realize that one full year has passed and now it is time for a wake up call.




It is OSIsoft's 20th User Conference, a place were OSIsoft's employees and Customers discuss all things alike, from games' scores to new jokes, technical questions, use cases, family updates, and the good old fashion catch-up game that societies and families like to play. And that is exactly what we are, a big family.


And it all begins with the founder and CEO of OSIsoft giving and interesting and insightful speech about technology, stupid grids and everything, it was amazing for the customers that didn't had the chance to attend a previous Users Conference and refreshing for the lucky ones that have had the chance to attend before.






I hope to see the community members at the vCampus Live!

How many of us have had a hard time staying up-to-date with all the things that come out each day? I bet all of us, imagine sending those trends on a day-to-day basis... oh, that should be hard.


So, how about that guy named Atlas? Yeah, the guy that was carrying the world wide web on his shoulders, wait... what??? what do you mean there was no WWW on his days? Ohhh, I see. I have found and now share with you a couple of links that will, hopefully, make your life easier, put you up to date, and make it for an overall better user experience using Ajax 2.0 with Microsoft .NET Framework 3.5


You can find the Official AJAX.NET Homepage where you can get the latest version of this framework (and believe me, you so want to get it).


Maybe you'll need a little more convincing... I'm up for that challenge, and I'm presenting you a little bit more of eye candy in this ASP.NET Ajax Control Toolkit feel free to play with it, configure it, and fall in love with it. I really intended to put some screenshoots of this here, but it does not have the same feeling, you need to go there and see for yourself! Trust me on this one.


You will see something like this. See the big list on the left side? all of those are Controls! Go go go!


And last, but not least important, if you call now you can get all this for the incredible web-offered price of... hehehe, too much infomercials for me. You can get it for free, it does integrate well with Visual Studio, and a Microsoft team (plus a lot of external collaborators) are working to make this available for everyone to use, so... what are you waiting for? get to the download page and start using the ASP.NET Ajax Control Toolkit with your OSIsoft's SDKs! (Marketing told me to put that here. hehe.)


I hope you enjoy this, I'll be playing with these controls this week.


Stay tuned!




P.S. Just for a little bit ofbackground AJAX is a Web2.0 technology that allows changes in a webpage with a seamless refresh, it is becoming comonly used and is an acronim for 'Asynchronous JavaScript And XML', Atlas was the codename for the AJAX Control Toolkit.


-- Links --


Getting data from a PI/OLEDB Provider is a no brainer for most of us, but assigning row numbers to each row according to some criteria is quite a challenge and it is something customers request frequently. I have learned the hard way that there is no "easy" way to do this and that in order to achieve this you need to make some sacrifices: some of them come in the shape of internal functions, external code that manages the visualisation layer or (in this case) having a linked server inside SQL Server 2005/2008. This may not be the best way to do this (performance-wise) but its simplicity is enough to give it a try.


The way this works is that the SQL Server will get the data from the PI Server through the PI OLEDB Provider and will then add the Row Numbers before returning this dataset to the final client. This final client can be anything that can connect to the SQL Data provider. Note that we need a client able to issue the row_number() function so the use of "SQL Server Native Client 10.0" is strongly recommended.

Adding a Linked Server in SQL Server 2008

First we need to get our PI Server listed into the SQL Server, and we need to start up the Microsoft SQL Server Management Studio (or the MSSMS, too many M's and S's if you ask me). Once in that program and after we connected to our desired SQL Server we...

  1. Expand 'Server Objects'.
  2. Right Click on 'Linked Servers'.
  3. Select 'New linked Server'. A dialog will pop up.
  1. Fill 'Linked Server' with a name of your choice. "LOCALHOST" was used in this example.
  2. Select in 'Provider:' dropdown "PI OLE DB Provider".
  3. Fill 'Product Name:' with a value of your choice. "piserver" was used in this example.
  4. Fill 'Data source:' with the name or ip of your pi server as it appears on the PI SDK Connections. "localhost" would work in this example.
  5. Click 'OK'.

Building a query

After we have added our linked server we will need to create a new query and write the query we want to execute.

  1. Click on the 'New Query' button. A new, empty, query will appear.
  2. Write the query you want to run in the new window:

WITH OrderedValues AS
FROM [LOCALHOST].[piarchive]..[picomp]
WHERE tag like'cdt158'
FROM OrderedValues

  1. Now click on 'Execute' and you should get a list of results that will not be exactly those displayed but will look pretty much alike.

Please note that you can change the tag name, select more than one tag, select a different number of rows (I used from rows 8829 up to 8838 in this example), you could even use parameters in this query. I specifically used these values to show the same range of data in all three windows so you could compare them.

Verifying the values

So far we got a linked server and some data, now we'll verify that we have the same values in both places, in the PI OLEDB Tool and in the SQL Linked Server, we will be using the infamous CDT158 random tag for this test, and the query we'll use is SELECT [tag],[time],[value],[status] FROM [picomp] WHERE tag like 'cdt158'
The query can be seen in the lower part of the screenshot from the program.



Using the values

Now we will be consuming the values connecting to the SQL Server. Note that we could do this step with any tool that supports acquiring data from SQL (such as Excel, SharePoint, ProcessBook or any other zillion applications). However, for ease of use and since the dialogs for the configuration are almost the same in all of them we'll be connecting with Excel.

  1. Create a blank workbook in Excel
  2. Select 'Data', From other sources, From the 'Data Connection Wizard'
  3. Select 'Other/Advanced' and click 'Next'
  4. Under 'Provider', Select the 'SQL Server Native Client 10.0' (I have SQL 2008, that's why it says 10, it may say 9 if you have SQL 2005)
  5. Under 'Connection', enter your SQL Server name (LOCALHOST\SQLEXPRESS in this example)
  6. Select the appropiate log on type ('Use Windows NT Integrated security' in this example)
  7. Select any database (PIAF_Database in this example), note that the linked server's databases do not show up, so any database will do for now.
  8. 'Test' your connection and if everything succeeds...
  9. Click 'OK'
  10. In the next dialog check 'Connect to specific' table
  11. Select any table (AFTableView in this example)
  12. Click 'Finish'
  13. It may complain about a file that already exists (even more so if you have done this step before), be sure to overwrite it.
  14. In the 'Import Dialog' I left everything as is, your preferences may change, it is ok to change this.
  15. Click 'Properties'
  16. In 'Connection name' any name will do (PIoledb on this example)
  17. Under 'Definition', for the 'Command Type' select 'SQL' from the dropdown list.
  18. In 'Command Text' paste the query you build before (ref.: Building query, Step 2 in this example)
  19. Click 'OK'
  20. Excel will complain about the connection having changed from the one in the configuration file and warn you about removing the link to the external file (wait, does that mean that we won't need the .odc file anymore? that's correct! can you believe it?) be sure to click 'OK'
  21. Back on the Import data dialog, click 'OK'
  22. And voila! we've got data numbered on our spreadsheet, thanks to the PI OLEDB Provider and SQL Server.

Last bits

  • You’ll notice that even getting this amount of data can be done in less than 6 seconds on a low-end computer running the PI Server and the SQL Server (which is not recommended)
  • There are some other interesting numbering functions available in SQL 2005/2008 like DENSE_RANK(), RANK() and NTILE()
  • You could try to do this with the SQL Server Native Client instead, but it will fail miserably when changing the query as that connector won't understand the new functions.
  • We added too much complexity for numbering these rows, that could be easily done with a programmatic interface after we have collected the values.
  • The index in this example will change from query to query, which is less than optimal. But then again, if it didn't change why would we need dynamic numbering anyway?
  • This can be used to do some pagination-like feature on some places, like RtWebparts, or a datagrid.

Filter Blog

By date: By tag: