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

PI Developers Club

4 Posts authored by: pmartin Employee

A topic that I've seen posted quite a few times is "How do I get a custom symbol to change other symbols on the display?"  Without the knowledge of what's available to you, it can seem like a tricky or impossible task.  Once you know how to do it, it actually becomes quite easy. To get you started, I've written a couple of fun/useless symbols to show you the basics.  I may expand/improve the list in the future because I'm constantly learning new things about PI Vision Extensibility.


The Big Red Button


What does it do?


It deletes everything on your display when clicked.  It should go without saying but please don't use this on any production displays.

How does it work?

It uses the displayProvider to select all of the symbols in the display and subsequently loops though and deletes all selected symbols.

The code


<div id="bigredbutton" ng-click="pressed()"> DELETE </div>


window.PIVisualization = window.PIVisualization || {};
(function (PV) {
     'use strict';

     function symbolVis() { }
     symbolVis.prototype.init = function (scope, elem, displayProvider, $rootScope)
          scope.pressed = function(){

     var def = {
          typeName: 'bigredbutton',
          datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Single,
          visObjectType: symbolVis,
          inject: [ 'displayProvider', '$rootScope'],
          getDefaultConfig: function(){
                    DataShape: 'Value',
                    Height: 150,
                    Width: 150


The Add DataStream Button

What does it do?


When the add button is hit, it searches the display for all symbols that support multiple data streams.  It then adds whatever tag or attribute is in the textbox as a datastream to each of the found symbols.

How does it work?

It uses the displayProvider to select all of the symbols in the display and subsequently loops though, checks if the DatasourceBehavior allows multiple data sources, pushes the new data source, and then deselects the symbol.

The code


<div id='add-data-source-container'>
     <input type='text' class='text-input' ng-model='newdatasource.text' placeholder='pi:\\servername\tagname'/>
     <button class='submit-button' ng-click="add()"> Add </button>


Note: Everything except the init function is the same as the previous symbol.  For brevity, just the init function is shown.

scope.newdatasource = {
     text: ""

scope.add = function(){
          //check if this symbol supports multiple datasources
          if(displayProvider.getRuntimeSymbolData(sym).def.datasourceBehavior == PV.Extensibility.Enums.DatasourceBehaviors.Multiple){
               //add the datasource from the textbox
          //deselect this symbol


The Chatting Symbols

What does it do?


The name of a receiver symbol (which must be of the same type) is entered into the top box along with a message in the bottom box.  Upon clicking "Send", that message is displayed in the destination's bottom box along with who sent it (in the top box).

How does it work?

This symbol is starting to show some real power.  We're modifying the receiver's scope to change its values in real time.

Before we get to the code

There is a behavior in Angular which makes reading another DOM element's scope impossible unless debugInfoEnabled is set to true.  In order for this symbol to work, you will need to edit a line in changing:




Be aware that this may cause a performance decrease.

The code


<div id='sayhi-source-container'>
     <input type='text' class='text-input receiver' ng-model='message.receiver' ng-click='receiverboxclicked()' placeholder='symbol name of receiver'/>
     <input type='text' class='text-input message' ng-model='message.text' placeholder='type message to receiver'/>
     <button class='submit-button' ng-click="sayhi()"> Send </button>


Note: Everything except the init function is the same as the the big red button symbol. For brevity, just the init function is shown.

var name =;

scope.message = {
     receiver: "",
     text: ""

scope.receiverboxclicked = function(){
     scope.message.receiver = scope.message.receiver.replace(' says:','');
     scope.message.text = '';

scope.sayhi = function(){
     var receiver = $('#'+scope.message.receiver);
     if(receiver.length > 0){
          var receiver_scope = receiver.scope();
               receiver_scope.message.receiver = name + " says:";
               receiver_scope.message.text = scope.message.text;
               alert(name + "'s receiver, " + scope.message.receiver + ", is not of type 'sayhi' ");
          alert(name + "'s receiver," +scope.message.receiver + ", does not exist");


The Disable Display Selection Switch (new)


What does it do?

It disables selection of symbols on the display (and also disables the right click menu)

How does it work?

We're modifying every symbol's behavior by modifying something common to all of the symbols - the display provider

The code

(styling for switch omitted)


<div style="color: white"> Selection </div>
<label class="switch">
     <input type="checkbox" ng-model="config.Enabled" ng-change="toggled()">
     <span class="slider round"></span>



symbolVis.prototype.init = function (scope, elem, displayProvider){
     var originalDisplayProvider = displayProvider['selectSymbol'];
     function allowClick(){
               displayProvider['selectSymbol'] = originalDisplayProvider;
               displayProvider['selectSymbol'] = function(){};

     setTimeout(function() { 
     }, 1000);

     scope.toggled = function(){
var def = {
     typeName: 'disableselection',
     datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Single,
     visObjectType: symbolVis,
     iconUrl: 'Scripts/app/editor/symbols/ext/Icons/toggleswitch.png',
     inject: ['displayProvider'],
     getDefaultConfig: function(){
               DataShape: 'Value',
               Enabled: true,
               Height: 70,
               Width: 70,
               BackgroundColor: 'rgb(0,0,0)',
               TextColor: 'rgb(0,255,0)',


GitHub link: GitHub - osipmartin/Interacting-Symbols


PI Vision offers a form of Element Relativity that is quite different from PI ProcessBook.  While we love the style of PI Vision ER, we missed some of the features from ProcessBook ER when using PI Vision. With this in mind, we decided to develop a PI Vision toolpane that has features similar to what was available in ProcessBook.



  • Quickly swap between elements with a single click
  • Elements of Interest persistent across multiple sessions
  • Elements of Interest unique to each display
  • Minimal set-up
  • Robust search to populate list of Elements of Interest




Set Up


  1. In Windows Explorer, navigate to the "%PIHOME%\PIVision\Scripts\app\editor\tools\ext" on your PI Vision web server; typically, it's located in "C:\Program Files\PIPC\PIVision\Scripts\app\editor\tools\ext". If this folder doesn't exist, create it.
  2. Copy contents of the repository into the above folder. You may exclude "Example.png" and "README.MD"


In order to utilize the Elements of Interest unique to each display, an AF database is created to store the configuration strings for each display.  Use of this feature requires that the user be able to read from the AF Server when loading pages and write to the AF Server when saving the list in order to update the configuration. The creation of the AF database, elements, and attributes is done automatically once the AF Server has been specified.

The AF Server where the configuration is stored must be specified in the setup.json file located in the %pihome%\PIVision\Scripts\app\editor\Tools\Ext\ folder. With JSON format, such as the below:

{ AFServer”:”<YourAFServerName>” }

Where <YourAFServerName> is replaced with whatever AF Server you’d like to store the configuration in.

Additionally, in order to write to the AF Database, you will need Kerberos Delegation enabled and the user making changes to the elements of interest pane will need to have write permissions on that Asset Server in order to create a new database, and element beneath it.

Lastly, in some cases it has been necessary to create a MIME type in IIS in order to read the JSON configuration file if one does not already exist. This can be accomplished by opening up IIS Manager > Select the site hosting PI Vision > Double click on "MIME Types" in the Features View > Right-click "add" and use the settings below



Adding Elements to the Elements of Interest Pane

  1. Select a server from the list
  2. Verify the server connection was successful

Good: Bad:

  1. Specify search parameters:

Database (required): Which database should we search for elements in.

Root Element: Search only for elements below a particular element.  Default value searches entire database.

Element Name: Search only for elements with a particular name.  Wildcards (*) supported.  Default value is “*”

Template Name: Search only for elements with a particular template.  Default value is all templates.

Category Name: Search only for elements in a particular category.  Default value is all categories.

Add to Existing Results: Should the results of this search be added to the existing elements in the Elements of Interest.


Removing Elements from the Elements of Interest Pane

  1. Click the trash icon next to the element you want to remove from the list.

Display attributes for Element of Interest

  1. Click anywhere on the row for the element you want to view





This tool is not an official OSIsoft solution.  As such, it will not be supported.  If you have issues, please contact myself, Anna Perry, or Robert Schmitz and we will try our best to help you.

I occasionally see people in the Developer's Club requesting information on how to get PI SDK logs (such as information from their PI Data Archive) using the PI AF SDK.  The AF SDK does not provide this functionality and often our go-to response is to use the PowerShell Tools for the PI System. It's a great product but I can understand not wanting to learn how to use Powershell to do something simple such as getting message logs.


To make things easier, I decided to write a quick C# wrapper so that you can leverage these tools without leaving the comfort of .NET .   This is by no means an official solution.  Just something I thought might be helpful.

You can download the project here: GitHub - osipmartin/PowershellLogWrapper: C# Wrapper for Powershell Get-PIMessage to get PI SDK logs.


There are two projects in the solution.

Powershell_Logging: This is the actual library.  When you build the project, it will produce a dll that you can use elsewhere.

ReadPSLog: This is a simple console application that shows how to use one of the functions from the library.


I did just a bare minimum of testing.  If you have issues please feel free to tell me or fork the repository and fix it.


Note: I included the switches that I use regularly when I am getting messages but left out a couple switches that I never use.  Feel free to fork and modify to your hearts content.


New Attributes Traits

Posted by pmartin Employee Nov 3, 2016

What are Traits

Traits are a new way of identifying attributes with AF 2016.  Traits can be attached to AF Attributes and are designed to generically identify common characteristics or behaviors. Specifically, a trait can be used to define and/or find related AF Attribute objects with well-known behaviors and relationships. Starting with the AF SDK 2016, users are able to create, edit, delete, view, and utilize their attribute traits

To get a better idea of what traits are, let’s take an example from an application that makes use of traits: PI Coresight.  Coresight can find limits automatically and trend them without the need to know the exact trait attribute name.  See the video here: OSIsoft: Configure Attribute Traits in AF (Asset Framework) to Set Limits [AF 2016].  In this video, attribute names were chosen to match the names of their associated traits. Please be aware that this is not necessary; traits are designed to be generic and independent from the attribute name.


Types of Traits

There are 3 types of traits in the AF 2016 release. Additional trait types such as longitude and latitude will be rolled out in later versions of AF Server.

Limit Attribute Traits

Identify expected range of process variables. Available Limit Attribute Traits are Minimum, LoLo, Lo, Target, Hi, HiHi, and Maximum.

Forecast Attribute Traits

Use predicted values to make comparisons to your current values.

Analysis Start-Trigger Attribute Traits

For Event Frame Templates, store the Analysis trigger name or expression that caused the Event Frame to start.


Why use them

Traits provide context for attributes. Attributes like minimum value, maximum value, or forecasted value are universal so it doesn’t matter whether you, as the developer, know the process or not.  Want to make a program that validates whether the values coming in to a system are normal? Easy! Just search for the minimum and maximum trait attributes and compare it to the current value of its parent attribute.



If you are planning on following along, a few things need to be configured before you run the code. I have attached an XML export of my Traits database that you will need to import. Please note that the code used in this project is hardcoded to look at the “Traits” database.  I suggest creating a new database with that name and importing the XML file there.  If you don’t want to create a new database, you will need to modify the line of code that has:

AFDatabase db = AF.Databases["Traits"];

so that it is looking at your database and not the “Traits” database.


Creating Traits

Let’s start creating some trait attributes.  First, we’ll find the template that was imported in the previous section.

AFElementTemplate temperatureTemplate = db.ElementTemplates["Temperature_Simple"];

This template has two attributes:  Humidity and Temperature.  These two attributes have values that are randomly generated.  See the attached source code file is you wish to change these values from their initial value.

In this next section of code, we find the Temperature attribute and check if the Hi Limit attribute trait has already been created.  If it hasn’t, we create it using the value of its Abbreviation property for its name.  We then repeat this process for the Lo attribute trait. In this implementation of the code, the Humidity attribute is left unused so that you can experiment with other limit traits if you so desire.


//If Hi Trait doesn't exist, create it
if(temperature.AttributeTemplates[AFAttributeTrait.LimitHi.Abbreviation] == null) {
       //create new attribute
       AFAttributeTemplate limitHi = temperature.AttributeTemplates.Add(AFAttributeTrait.LimitHi.Abbreviation); 
       //specify which trait this attribute is supposed to be
       limitHi.Trait = AFAttributeTrait.LimitHi;
       //set the value for the Hi Limit
       limitHi.SetValue(100, limitHi.DefaultUOM);

//If Lo Trait doesn't exist, create it
if (temperature.AttributeTemplates[AFAttributeTrait.LimitHi.Abbreviation] == null) {
       AFAttributeTemplate limitLo = temperature.AttributeTemplates.Add(AFAttributeTrait.LimitLo.Abbreviation);
    limitLo.Trait = AFAttributeTrait.LimitLo;
    limitLo.SetValue(0, limitLo.DefaultUOM);


Once this code has been run, our template is defined.  Next, we'll utilize a for loop to initialize 100 elements using this template with random values for Humidity and Temperature.


for (int i = 0; i < 100; i++) {
       AFElement e = db.Elements.Add("Location"+ i);
       e.Template = temperatureTemplate;
       e.Attributes["Humidity"].SetValue(new AFValue(r.Next(0,100)));
       e.Attributes["Temperature"].SetValue(new AFValue(r.Next(-20, 120)));


Using Traits

As I discussed earlier in this post, traits can be used to quickly find whether your values are out of range.  That’s exactly what we’re going to do in this section.  I’ve outlined two different approaches for doing the same task.  The first method searches by Attribute trait name. This approach can check attributes across multiple templates but will only work if your trait attributes are consistently named.  The second approach searches by parent attribute name.  This approach will only check the attributes with a particular name, but it will do so much faster than the first approach.  In addition, this approach allows us to grab attribute traits with a wide variety of names.


Search by Attribute Traits

First, we’ll find all the attributes corresponding to our Lo limit attribute.  We will utilize the FindElementAttributes method to find attributes that have the same name as the abbreviation for the Lo Limit.  If you recall from the previous section, we used the Trait abbreviations to name our Limit attributes.


AFAttributeList los = AFAttribute.FindElementAttributes(db, null, null, null, null, AFElementType.Any, AFAttributeTrait.LimitLo.Abbreviation, null, TypeCode.Double, true, AFSortField.Name, AFSortOrder.Descending, 101);


Next, we’ll preload the values for Lo so that the performance is better.  The style of preloading uses bulk calls.  See the New Features and Best Practices Webinar and the comparison between Serial vs Parallel vs Bulk calls KB article for more information.  The values for Lo will all be the same in this simple example but it’s a bad habit to assume that it will always be the case.  Limits can be configured on a per Element basis or can be configured to get the value from a PI Point, Table Lookup, as well as other sources.


AFValues lovals = los.GetValue();


Limits (and Forecasts) are children of the attribute they are to be compared to.  In this next step we will loop through every returned Lo attribute and compare it with the parent to determine whether the parent value is out of range.


for (int i = 0; i < los.Count; i++) {
       //if the lo value is greater than the parent (attribute) value, output to console
       if(lovals[i].ValueAsDouble() > los[i].Parent.GetValue().ValueAsDouble()) {
         Console.WriteLine(   "Value under Lo Limit for {0}  :  {1} < {2}", 


We then repeat the process for our Hi Limit to determine which values have a high temperature.


Our output looks something like this:

Value under Lo Limit for Location9  :  -8 < 0

Value under Lo Limit for Location0  :  -20 < 0

Value over Hi Limit for Location98   :  110 > 100

Value over Hi Limit for Location96   :  114 > 100


Search by Parent Attribute

First, we’ll have to find the list of parent attributes.  We will utilize the FindElementAttributes method to find attributes named Temperature that are under an element that uses the Temperature_Simple template.   We’ll preload load values from this attribute similar to how we did in the previous section.


//Get Temperature Template
AFElementTemplate temperatureTemplate = db.ElementTemplates["Temperature_Simple"];
//Find all attributes that belong to an element of this template
AFAttributeList al = AFAttribute.FindElementAttributes(db, null, null, null, temperatureTemplate, AFElementType.Any, "Temperature", null, TypeCode.Double, true, AFSortField.Name, AFSortOrder.Ascending, 200);
//load values
AFValues alval = al.GetValue();


Now that we have the values, we’ll loop through each associated attribute and get its traits by using the GetAttributeByTrait method.  The nice thing about this method is that even if we had created our attribute traits with the crazy name below, it would still find accurately find and identify the limit.


AFAttributeTemplate limitHi = temperature.AttributeTemplates.Add("Some crazy high limit name");


After verifying that we found an attribute trait by checking for null, we compare the two to see if the value is out of range.


foreach(AFValue v in alval) {
     //get the trait values
     AFAttribute lo_a = v.Attribute.GetAttributeByTrait(AFAttributeTrait.LimitLo);
     AFAttribute hi_a = v.Attribute.GetAttributeByTrait(AFAttributeTrait.LimitHi);

     //compare to trait values if the trait exists
     if(hi_a != null && v.ValueAsDouble() > hi_a.GetValue().ValueAsDouble()) {
          Console.WriteLine("Value over Hi Limit for {0}  :  {1} > {2}",
     else if(lo_a != null && v.ValueAsDouble() < lo_a.GetValue().ValueAsDouble()) {
              Console.WriteLine("Value under Lo Limit for {0}  :  {1} < {2}", 


Similar to the first method, the output results look like this:

Value over Hi Limit for Location98     : 110 > 100

Value under Lo Limit for Location85  :  -8 < 0

Value over Hi Limit for Location79     : 112 > 100

Value under Lo Limit for Location72  :  -10 < 0


The only difference is that Lo and Hi limits are interleaved.



I hope you can see the benefits of using traits even in this contrived example. Even without knowing how or where the temperature was measured, you could instantly tell whether the values you were seeing were expected. More trait types will be coming in the future so you should get on board now!

If you liked this post and want to learn more about traits, let us know.  In addition to some more tips/tricks, I have content on Forecasts and Analysis Start Condition traits that I would love to share.  If you want to learn more about them on your own, the examples of those are included in the source code in the “COMPLEX EXAMPLE” region.

I have attached my source code and an export of my database so that you experiment and modify as much as you’d like.  The code is also available on GitHub.

Filter Blog

By date: By tag: