cescamilla

How to create Custom WebParts that connects with OSIsoft's RtWebParts

Blog Post created by cescamilla on Dec 5, 2009

Note

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

Introduction

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.

Overview

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.

 

Web-Parts-Example.png 

Reference

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.

Pre-requisites

  • 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.
    000-_2D00_-Splash.jpg

  • I created a new project using the new project menu as illustrated in the following picture
    001-_2D00_-New-Project.png

  • Navigate to Visual C#, locate SharePoint, and select WebPart
    002-_2D00_-New-Project-10262009-124332-PM.bmp.jpg

  • 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)
    003-_2D00_-Select-Trust-Level-10262009-124343-PM.bmp.jpg

  • You will be greeted with a default WebPart already created, your screen should look a lot like this:
    004-_2D00_-First-Screen.png
  • 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)
    005-_2D00_-Rename-Web-Part-10262009-11250-PM.bmp.jpg

  • 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";
    this.Controls.Add(label);

    010-_2D00_-Build-01.png

  • 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
    011-_2D00_-Document-center.png

  • Under Site Actions select Create as illustrated in the next picture
    012-_2D00_-Create-page-00.png

  • Under Web Pages select Web Part Page
    013-_2D00_-Web-Part-Page-00.png

  • I named the WebPage tut_webpart_conn.aspx and selected Header, Footer, 3 Columns then clicked create
    014-_2D00_-tut_5F00_webpart_5F00_conn.png

  • I wanted to add the recently compiled webpart to the Left Column
    015-_2D00_-Add-webpart-01.png

  • I found the WebPart under Miscellaneous as expected.
    016-_2D00_-Fullscreen-capture-10262009-13125-PM.bmp.jpg

  • 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.
    017-_2D00_-WebPart-working.png

  • I was kindly greeted by the 'Hello world' comment, that means that everything is working in run mode too.
    018-_2D00_-Exit-edit-mode-00.png

  • 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).
    019-_2D00_-Step-1-n-2.png

  • 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
    [Obsolete]
    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.
    020-_2D00_-Step-3.png

  • Step 4: Override the CanRunAt Method

    //Add this to our WebPart class
    [Obsolete]
    public override ConnectionRunAt CanRunAt()
    {
      return ConnectionRunAt.Server;
    }

    021-_2D00_-Step-4.png

  • Step 5: Override the PartCommunicationConnect method

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

    [Obsolete]
    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)
    022-_2D00_-Step-5.png

  • Step 6: Override the PartCommunicationInit method

    //Add this to our WebPart class
    [Obsolete]
    public override void PartCommunicationInit()
    {
      ParametersOutProviderInitEventArgs parametersOutProviderInitEventArgs =
        newParametersOutProviderInitEventArgs();
      parametersOutProviderInitEventArgs.ParameterOutProperties = newParameterOutProperty[1];
      parametersOutProviderInitEventArgs.ParameterOutProperties[0] = newParameterOutProperty();
      parametersOutProviderInitEventArgs.ParameterOutProperties[0].ParameterName =
        "TagName";
      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.
    023-_2D00_-Step-6.png

  • Step 7:  Override the PartCommunicationMain method

    //step 7
    private DropDownList dropDownList;

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

    024-_2D00_-Step-7.png

  • 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)
    {
      EnsureChildControls();
      if (paramsOutConnected)
      {
        dropDownList.Attributes["onchange"] =
          "javascript:__doPostBack('" + this.UniqueID + "', '');";
      }
      base.RenderWebPart(output);
    }

    We need to tell the webpage to do a full page refresh.
    025-_2D00_-Step-8-n-9-n-10.png

  • Step 11: Implement supporting methods

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

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

    protected override void CreateChildControls()
    {
        base.CreateChildControls();

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

        dropDownList = new DropDownList();
        dropDownList.Items.Clear();

        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 ]));
        }
        tc.Controls.Add(dropDownList);
        Controls.Add(htmlTable);
    }

    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.
    026-_2D00_-Step-11.png

  • 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.
    027-_2D00_-Working-02.png

  • 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
    028-_2D00_-Edit-02.png

  • Click on the center webpart zone that reads Add a webpart
    029-_2D00_-Add-WebPart-02.png

  • Look for and select RtTrend then click the button Add
    030-_2D00_-Fullscreen-capture-10262009-25418-PM.bmp.jpg

  • 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
    031-_2D00_-Connections-01.png

  • 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
    032-_2D00_-Modify-02.png

  • Look for the Selected Data section in the RtTrend Property pane and click on the ligthning bolt
    033-_2D00_-Connections-02.png

  • 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
    034-_2D00_-Fullscreen-capture-10262009-25726-PM.bmp.jpg

  • Close the property pane by pressing the OK button
    035-_2D00_-Ok-02.png

  • It is working, at least for the first parameter!
    036-_2D00_-Exit-edit-mode-02.png

  • Clicking on the second one shows the second tag, after that I clicked on the dropdown again just to show it off.
    037-_2D00_-Running-OK-01.png

  • 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"),
    WebBrowsable(false),WebPartStorage(Storage.Personal)]

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

    object IWebEditable.WebBrowsableObject
    {
      get { return this; }
    }

    038-_2D00_-Step-12.png

  • 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()
      {
        base.CreateChildControls();
        Label label = new Label();
        label.Text = "Data";
        Controls.Add(label);
        Controls.Add(new HtmlGenericControl("br"));
        textData = new TextBox();
        textData.TextMode = TextBoxMode.MultiLine;
        textData.Rows = 5;
        textData.Wrap = false;
        textData.Width = 200;
        Controls.Add(textData);
        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()
      {
        EnsureChildControls();
        Tut_WP_OSI listProvider = (Tut_WP_OSI)WebPartToEdit;
      }
    }

    039-_2D00_-Step-13.png

  • 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.
    040-_2D00_-Step-14.png

  • 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.
    041-_2D00_-Testing-custom-property.png

  • How about testing it out, let's click on the arrow and then on Modify Shared Web Part
    042-_2D00_-Modifying-shared-webpart.png

  • 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
    043-_2D00_-Editing-the-tag-list-for-the-dropdown.png

  • We can see the Web Part working correctly.
    044-_2D00_-Verifying-the-functionality.png

  • We still need to test out if it can change the tag by clicking on the dropdown.
    045-_2D00_-Checking-drowdown.png

  • And now we have a Server Side connected WebPart!
    046-_2D00_-Done-with-server-side.png

  • Adding client side scripting

  • Add the JavaScript file to the project (note that you could have as easily added an existing item)
    047-_2D00_-Add-new-item.png
  • I will name this new item TagListProvider.cs and select it to be a JScript File
    048-_2D00_-TagListProvider.png
  • 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;
            this.ParametersOutReady(parametersOutReadyEventArgs);
        }

        // 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 =
              "TagName";
            parametersOutProviderInitEventArgs.ParameterOutProperties[0].Required = false;
            WPSC.RaiseConnectionEvent("TagListProvider" + this.WPQ,
              "ParametersOutProviderInit", parametersOutProviderInitEventArgs);
        }

        // entry point for connection
        function _partCommunicationMain()
        {
            this.RaiseParametersOut();
        }

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

        // special function for when there are no parameter values to pass
        function _noParametersOut()
        {
        }
    }
    -->
    </script>

    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.
    049-_2D00_-JavaScript-source.png
  • Change its compilation action to embedded resource
    050-_2D00_-Embedded-Resource.png
  • Step 15: Modify the "CanRunAt" function

    //Add this to our WebPart class
    [Obsolete]
    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.
    051-_2D00_-Step-15.png
  • 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 =
        System.Reflection.Assembly.GetExecutingAssembly();

      // the name of the resource is dependent on the project's folder structure
      Stream scriptStream =
        thisAssembly.GetManifestResourceStream(
          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.
    052-_2D00_-Step-16.png
  • Step 17: Modify the OnPreRender Method

    //step 17
    protected override void OnPreRender(EventArgs e)
    {
      base.OnPreRender(e);
      if(paramsOutConnected)
      {
        if (CanRunAt() == ConnectionRunAt.Client)
        {
          dropDownList.Attributes["onchange"] =
            string.Format(
              "javascript:TagListProvider{0}.RaiseParametersOut();",
              ReplaceTokens("_WPQ_"));
          if (!Page.ClientScript.IsClientScriptBlockRegistered("ListProvider"))
            Page.ClientScript.RegisterClientScriptBlock(
              typeof(string), "ListProvider", GetScript());
          string initScript =
            @"var TagListProvider_WPQ_ = new TagListProvider('_WPQ_', '{0}', '{1}');";
          initScript = string.Format(
            ReplaceTokens(initScript), dropDownList.ClientID, this.StorageKey);
          Page.ClientScript.RegisterClientScriptBlock(
            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.
    053-_2D00_-Step-17.png
  • 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.

    Conclusions

  • 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.

Outcomes