AF SDK 2.9 Getting Started Guide EN

User Manual:

Open the PDF directly: View PDF PDF.
Page Count: 54

DownloadAF SDK 2.9 Getting Started Guide AF-SDK-2.9-Getting-Started-Guide-EN
Open PDF In BrowserView PDF
AF SDK 2.9
Getting Started Guide

OSIsoft, LLC
1600 Alvarado Street
San Leandro, CA 94577 USA
Tel: (01) 510-297-5800
Fax: (01) 510-357-8136
Web: http://www.osisoft.com
AF SDK 2.9 Getting Started Guide
© 2017 by OSIsoft, LLC. All rights reserved.
No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or
by any means, mechanical, photocopying, recording, or otherwise, without the prior written permission
of OSIsoft, LLC.
OSIsoft, the OSIsoft logo and logotype, Managed PI, OSIsoft Advanced Services, OSIsoft Cloud Services,
OSIsoft Connected Services, PI ACE, PI Advanced Computing Engine, PI AF SDK, PI API,
PI Asset Framework, PI Audit Viewer, PI Builder, PI Cloud Connect, PI Connectors, PI Data Archive,
PI DataLink, PI DataLink Server, PI Developer’s Club, PI Integrator for Business Analytics, PI Interfaces,
PI JDBC driver, PI Manual Logger, PI Notifications, PI ODBC, PI OLEDB Enterprise, PI OLEDB Provider,
PI OPC HDA Server, PI ProcessBook, PI SDK, PI Server, PI Square, PI System, PI System Access, PI Vision,
PI Visualization Suite, PI Web API, PI WebParts, PI Web Services, RLINK, and RtReports are all trademarks
of OSIsoft, LLC. All other trademarks or trade names used herein are the property of their respective
owners.
U.S. GOVERNMENT RIGHTS
Use, duplication or disclosure by the U.S. Government is subject to restrictions set forth in the OSIsoft, LLC
license agreement and as provided in DFARS 227.7202, DFARS 252.227-7013, FAR 12.212, FAR 52.227, as
applicable. OSIsoft, LLC.
Version: 2.9
Published: 03 August 2017

Contents
Introduction.............................................................................................................. 1
About OSIsoft developer technologies............................................................................................................ 2
AF SDK versus PI SDK......................................................................................................................................3
Where to find help........................................................................................................................................... 3

Configuration............................................................................................................ 5

Create the database for your PI system........................................................................................................... 5
Create an application structure....................................................................................................................... 6
Set the target framework................................................................................................................................ 6
Add AF SDK references to your project............................................................................................................7
Specify namespaces........................................................................................................................................ 7

Programming exercises.............................................................................................. 9
Lesson 1: Connect to PI Data Archive or PI AF database.................................................................................. 9
AF SDK basics............................................................................................................................................ 10
Connect to a PI Server or AF Database.......................................................................................................12
Lesson one exercises..................................................................................................................................13
Lesson 1 hints and tips............................................................................................................................... 15
Lesson 1 exercise solutions........................................................................................................................ 15
Lesson 2: Searching for assets........................................................................................................................17
Lesson 2 exercises..................................................................................................................................... 19
Lesson 2 hints and tips...............................................................................................................................20
Lesson 2 exercise solutions........................................................................................................................ 20
Lesson 3: Reading and writing data................................................................................................................22
Lesson 3 exercises..................................................................................................................................... 24
Lesson 3 hints and tips...............................................................................................................................28
Lesson 3 exercise solutions........................................................................................................................ 28
Lesson 4: Building an AF hierarchy.................................................................................................................31
Lesson 4 exercises......................................................................................................................................33
Lesson 4 hints and tips............................................................................................................................... 35
Lesson 4 exercise solutions........................................................................................................................ 36
Lesson 5: Working with AF event frames....................................................................................................... 41
Lesson 5 exercises......................................................................................................................................43
Lesson 5 hints and tips...............................................................................................................................44
Lesson 5 exercise solutions........................................................................................................................ 45

Conclusion............................................................................................................... 47
Technical support and other resources....................................................................... 49

AF SDK 2.9 Getting Started Guide

iii

Contents

iv

AF SDK 2.9 Getting Started Guide

Introduction
AF SDK (Asset Framework software development kit) is a .NET-based library that provides
access to data stored in the PI System. Using AF SDK you can access:
• Hierarchically structured assets and their properties and relationships
• Time-series data from PI Data Archive and other sources
• Event frames that record and contextualize process events
• PI Data Archive, PI points, and point data
The information in this Getting Started Guide covers basic usage of AF SDK. AF SDK is one of
the available developer technologies that can help you maximize the utility of your PI System.
It is recommended that you have a background in .NET application development using C# and
a familiarity with the PI System; however, familiarity with AF SDK is not required.
PI developers should also consider becoming familiar with PI SQL Framework, PI Web API, or
PI Builder before contemplating development with AF SDK. While AF SDK is powerful and is
required in many cases, using it effectively in a complex application is easier if you already have
an understanding of PI SQL or PI Web API.
When developing applications for the PI System, you have several options from which to
choose, including any of the family of products that make up PI SQL Framework, PI Web API, or
AF SDK. However, for best performance from your application, AF SDK has the greatest
potential of all of the products in the PI System Access suite.
AF SDK provides a comprehensive, Windows-based programmatic interface to the PI System. It
provides an object-oriented approach to interacting with AF structures and data, as well as the
PI Data Archive directly.
There are many reasons you might choose to develop software using AF SDK:
• You can quickly build out complex PI AF hierarchies with AF SDK. Many OSIsoft partners
prefer to build AF hierarchies this way, rather than use the PI AF Builder plugin to Microsoft
Excel.
• AF SDK is well suited to developing middleware applications which are situated between
the PI System and other systems. AF SDK gives you flexibility about how to integrate other
systems with the PI System, and allows you to do so in the most resource efficient way
possible. It is also possible to automate element generation and synchronization between PI
AF and other systems.
• You have the ability to create rich and customized client applications with AF SDK. Most
client applications that make up the PI Visualization Suite were developed using AF SDK and
its predecessors.
AF SDK is designed for use with Microsoft .NET languages such as Visual Basic .NET, C#, and
Managed C++. The computer you use for software development and the computer that will
eventually run applications developed with AF SDK must have Microsoft .NET Framework 4.0
installed. Note that Microsoft .NET is installed automatically by the PI System Explorer
installation program if it is not already installed on your computer.

AF SDK 2.9 Getting Started Guide

1

Introduction

About OSIsoft developer technologies
OSIsoft developer technologies PI Developer Technologies (also called PI System Access) is a
family of products designed to support the implementation of custom applications that work
with the PI System. PI Developer Technologies also helps you integrate PI System data with
other applications and business systems such as Microsoft Office or SQL Server, enterprise
resource planning (ERP) systems, reporting and analytics platforms, web portals, geospatial,
and maintenance systems, to name just a few examples.
The PI System Access suite covers a wide range of use cases in various environments,
programming languages, operating systems, and infrastructures. The different technologies in
the PI System Access family are:
• AF SDK
Provides comprehensive, high-performance, Windows-based .NET programmatic access to
the PI System. AF SDK:
◦ Has the best performance of all PI development technologies.
◦ Has more available methods and options than any other technology.
◦ Is limited to Windows operating systems running .NET Framework.
• PI Web API
Enables operating system and device-independent programmatic access to the PI System
through a REST API, and is the recommended cross-platform offering for any operating
system that is able to use a modern web browser such as Google Chrome, Mozilla Firefox, or
Microsoft Edge. PI Web API:
◦ Is not dependent on a specific operating system or programming language.
◦ Has limited methods and options available compared with AF SDK.
• PI SQL Framework
Makes the data on PI Server available for querying as if it was on a relational database. PI
SQL Framework:
◦ Is compatible with other systems that use Structured Query Language (SQL).
◦ Has limited methods and options available compared with AF SDK (for example, setting
security on assets and creating units of measure).
◦ Is limited to read-only PI AF access (but can create and write to tags on your PI System).
There are more ways to access the PI System programmatically; however, the above three
methods describe the vast majority of use cases.
You might consider using the tools for Microsoft PowerShell. See the OSIsoft Tech Support page
PI Powershell (https://techsupport.osisoft.com/Documentation/PI-Powershell/title.html).
PowerShell cmdlets help with administration of PI Data Archive and PI Asset Framework
servers, and can be used in a variety of ways to create scripts for commonly needed
functionality or for bulk system management operations.
Documentation for these products (and others) can be found in: Live Library (https://
livelibrary.osisoft.com/).

2

AF SDK 2.9 Getting Started Guide

Introduction

AF SDK versus PI SDK
OSIsoft recommends using AF SDK for all new software development projects. If you have
experience coding with the older PI SDK, you should be aware of the following differences:
• AF SDK cannot be used with ModuleDB, PIModules, and so on.
• AF SDK cannot be used with PI message logs (for this, you should use PowerShell instead) .
Applications that do not reference any of the items in the above list will execute faster if they
are written using AF SDK.
In addition, AF SDK uses managed .NET code, whereas PI SDK uses un-managed code that
relies upon Microsoft COM (Component Object Module), which might be awkward to use for
beginning developers.
Another difference is that AF SDK uses the AFValue object while PI SDK uses a PIValue
object. While a PIValue is composed of a Value and Timestamp property, an AFValue includes
a UOM (unit of measure) property.
Virtually everything you could do with PI SDK (with only limited exceptions), you can do with
AF SDK.

Where to find help
You can obtain AF SDK help online at the OSIsoft Tech Support page AF SDK RFeference
(https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/1a02af4c-1bec-4804a9ef-3c7300f5e2fc.htm).
You can also view a help file that is installed when you install AF SDK. Navigate to the %PIHOME
%\help directory and double-click the AFSDK.chm file.
You can also display help directly from PI System Explorer by selecting Help > AF SDK
Reference.
PI Developers Club within PI Square is also available for obtaining help online. PI Developers
Club is a one-stop shop for all PI System development needs. See PI Developers Club (https://
pisquare.osisoft.com/community/developers-club).

AF SDK 2.9 Getting Started Guide

3

Introduction

4

AF SDK 2.9 Getting Started Guide

Configuration
The information in this topic and the following topics describes how to configure your
development environment to create applications using AF SDK.
Note:
The rest of the topics in this section provide information about how to configure your
system so you can run the programming exercises in this guide. These steps are required
only if you are configuring your own environment and are not necessary if you are using
the Virtual Learning Environment (VLE).
There are two ways to configure an environment for developing applications. The easiest way
is to simply sign up with OSIsoft Learning or PI Developers Club space within PI Square and
create a Virtual Learning Environment (VLE). A VLE is a virtual computer that is configured
with the appropriate versions of PI Data Archive, PI AF, Visual Studio, .NET, and AF SDK. Other
than creating the VLE, no additional configuration is required. A VLE is available for this
Getting Started Guide at a nominal cost. See the OSIsoft Learning page Virtual Learning
Environment Overview (https://learning.osisoft.com/Labs/LabInformation) for details.
The other way to configure an environment for developing applications is to use an existing PI
Data Archive and PI AF database at your company or organization. To run the examples in this
Getting Started Guide you must have the following:
• An operational PI Data Archive and PI AF system, preferably a system used for testing or QA
work. Because this Getting Started Guide creates and edits tags and asset elements, you
should not use a production system.
• AF SDK version 2.9.1. You can download the latest version from OSIsoft Tech Support
(https://techsupport.osisoft.com/Downloads/All-Downloads/Developer-Technologies/AllProducts).
• .NET version 4.5. See the OSIsoft Tech Support page .NET 4.5 differences (https://
techsupport.osisoft.com/Documentation/PI-AF-SDK/html/cc26e52eb3fc-4b0b-9ff5-0526a8b4c8cc.htm) for more information.
• Microsoft Visual Studio 2015 or Microsoft Visual Studio Community edition (available free).
Visual Studio Express 2017 is also available free from Microsoft.

Create the database for your PI system
The steps in this section create the database and sample data you need for the exercises in this
guide.
The steps in this section are necessary only if you are creating your own environment and are
not required if you are using a Virtual Learning Environment.

Procedure
1. Clone a local copy of the GitHub repository.
The code for the exercises is located in GitHub:
https://github.com/osisoft/AF-SDK-Getting-Started-Guide

2. Start PI System Explorer and click File > Database

AF SDK 2.9 Getting Started Guide

5

Configuration
The Select Database window displays.
3. Click New Database.
The Database Properties window displays.
4. In the Name field, enter the name of the database to create; in this case, Green Power
Company.
Also provide a brief description, such as: Intro to AF SDK exercise database.
5. Click OK when finished, then click Close.
6. Click File > Import from File
7. In the File text box, browse to and select AF Database - Green Power Company.xml.
AF Database - Green Power Company.xml

8. Click OK when finished.
The data for the exercises is created and stored in the Green Power Company database.

Create an application structure
Microsoft Visual Studio creates the outline of a program when you create a new project. Follow
the steps in this section to create a program outline. Subsequent topics show how to customize
the application and augment it with AF SDK information.

Procedure
1. If necessary, start Visual Studio.
2. Click File > New > Project
The New Project window displays.
3. Select Console Application, then enter a name for the project in the Name field.
For this example, enter PI_Connect as the project name. Visual Studio creates the project
and presents a program outline that includes the Main function. You augment this outline
with your own code.

Set the target framework
The steps in this section set or verify that the target framework is set correctly in Visual Studio.

Procedure
1. In Solution Explorer window, right click the program name. A tab opens with properties for
your application.
2. If necessary, click Application.
3. In the Assembly name list, select .NET Framework 4.5.2.
4. Close the tab.

6

AF SDK 2.9 Getting Started Guide

Configuration

Add AF SDK references to your project
To make Visual Studio compile your code using AF SDK references, you use Reference Manager.
The extensions section of Reference Manager lists assemblies that OSIsoft has developed to
extend the .NET Framework.

Procedure
1. In Visual Studio, select Project > Add reference.
The Reference Manager window displays.
2. Click the Extensions node.
A list of custom assemblies displays.
3. Scroll the list until you see items from OSIsoft.
You can also enter a search term such as OSI in the Search Assemblies box.
4. Select the check box next to OSIsoft AFSDK version 4.0.0.0.
Be sure to select version OSIsoft AFSDK and not OSIsoft.PISDK. Also be sure to select
version 4 and not version 2.
5. Click OK.
The Reference Manager window closes and saves your changes.
The references you added using Reference Manager apply to the entire project, which
means that the referenced dynamic link libraries (DLLs or library files) are available to the
project. However, the referenced DLLs are quite large and contain lots of functionality that
your program might not use.

Specify namespaces
You must specify which namespaces you want to work with for a given DLL. For a given
application, you must add the appropriate using statements at the top of the program to
indicate the namespaces that the application will be using frequently.
Note:
When you run an application created with AF SDK, the application dynamically calls AF
SDK dynamic link libraries (DLLs); however, the application also requires some of the
services that are installed with AF SDK in addition to the DLLs. Therefore, you must
install PI System Explorer on any computer on which an AF SDK application is run.

Procedure
1. Using the Visual Studio editor, insert the following lines at the beginning of the program:
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.UnitsOfMeasure;

For later class files or projects that you work on, some other popular "usings" are:
using OSIsoft.AF.Time;
using OSIsoft.AF.Data;
using OSIsoft.AF.EventFrame;

AF SDK 2.9 Getting Started Guide

7

Configuration
The using statements you add depend on which of the many namespaces in a given DLL you
want to have available to the program you are working on.

8

AF SDK 2.9 Getting Started Guide

Programming exercises
In each of the lessons that follow, you will be tasked with creating methods that are
representative of the types of work you perform using AF SDK. Each lesson introduces the
tasks to be performed, followed by hints and tips about the lesson. In addition, working code is
provided for each of the lesson exercises.
Lessons 1 through 3 can be completed even if you have only a basic understanding of PI system
and AF, while lessons 4 and 5 require more in-depth knowledge of PI and how to perform
certain operations. Complete source code is included for every lesson and programing exercise,
and explanatory text contains code snippets you can use to augment your understanding of the
material.

AF SDK 2.9 Getting Started Guide

9

Programming exercises

Lesson 1: Connect to PI Data Archive or PI AF database
After completing this lesson you will be able to:
• Describe the structure of a PI AF database
• Create a simple console application
• Use AF SDK to create an application to connect to a PI AF server or PI Data Archive
• Use the PISystems class to find a list of known PI AF servers, and the PIServers class to
find the list of PI Data Archive servers
• Describe the AF SDK internal cache
• Connect to PI AF server and PI Data Archive collectives

Help for methods and data types used in this lesson
The following links contain information that describes important objects used in this lesson.
Refer to these pages on the OSIsoft Support site when writing code for the exercises:
• AFDatabase Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_AFDatabase.htm)
• AFElementTemplate Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/
Html/T_OSIsoft_AF_Asset_AFElementTemplate.htm)
• AFDatabase.ElementTemplates.FilterBy() Method (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/M_OSIsoft_AF_AFSDKExtension_FilterBy.htm)
• AFNamedCollectionList Class (https://techsupport.osisoft.com/Documentation/PI-AFSDK/html/T_OSIsoft_AF_AFNamedCollectionList_1.htm)
• AFAttributeTemplate Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/
html/T_OSIsoft_AF_Asset_AFAttributeTemplate.htm)
• UOMClass Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_UnitsOfMeasure_UOMClass.htm)
• AFEnumerationSets Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/
html/T_OSIsoft_AF_Asset_AFEnumerationSets.htm)
• AFCategories Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_AFCategories.htm)

Introduction
For this lesson, you will access a PI AF database named Green Power Company, which has
already been configured on your local PI AF server. You will be asked to print to the console the
names and properties of various PI AF objects. You should familiarize yourself with the Green
Power Company PI AF database using PI System Explorer before starting the exercises.

AF SDK basics
AF SDK presents a hierarchical model of objects and collections that represent underlying PI
System features and concepts. Using AF SDK, you use the PISystem object as an abstraction for
your PI System.

10

AF SDK 2.9 Getting Started Guide

Programming exercises
The PISystem object is the top-level object used to represent a logical PI AF server. Each
PISystem can have one or more separate AFDatabase objects. Each AFDatabase object may
contain any number of child AFElement objects, which are sometimes referred to as assets.
Each AFElement object may have child AFElements or child AFAttributes. AFAttributes
of an AFElement may represent values from a data stream, table, or descriptive values (either
static or dynamically changing). The PISystem object is ideal for organizing assets that may be
grouped in hierarchies and classified using templates created to describe similar objects across
the database.
Note that the UOM (unit of measure) database is attached to a PISystem object. In addition,
only one UOM database exists in a PI System.

Objects and collections
Many PI AF objects have associated collections. For example, each AFElement may contain any
number of child elements. The group of child elements is represented as an AFElements
object. The reference between an object and its child collection is used to build the hierarchical
structure of PI AF. Note that AF SDK employs a consistent naming convention for collections;
for example, a collection of AFElement objects is named AFElements.
The programmatic object hierarchy is very similar to the visual hierarchy displayed in PI
System Explorer. Therefore, understanding how objects are related within PI System Explorer
greatly facilitates learning AF SDK.

Namespaces
AF SDK is organized into several namespaces. The most common namespaces and their basic
functions are listed here. A block diagram of the PISystem Hierarchy (https://
techsupport.osisoft.com/Documentation/PI-AF-SDK/html/EB961F37-282A-43D2-8F8CF19CE07D9FA8.htm) is available in help.
• OSIsoft.AF
The AF namespace contains the main classes used by all the other namespaces within AF
SDK. It provides base class implementations and methods for connecting to PI AF servers.
The primary classes are PISystem and AFDatabase. Each PI System is composed of any
number of distinct databases. For more information see the OSIsoft Tech Support page
PISystem hierarchy (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
EB961F37-282A-43D2-8F8C-F19CE07D9FA8.htm).
• OSIsoft.AF.Asset
The Asset namespace provides a set of classes for representing assets within an
organization. It allows the creation of hierarchies of assets and their attributes. Additionally,
it provides features for dealing with common requirements such as remote data access,
unit-of-measure conversion, and defining and enforcing asset definitions. Templates for
assets ensure consistency among attributes. In the AF namespace, you will find AFElement,
AFAttribute, AFElementTemplates, and so on, along with their associated collections
(such as AFElements). In addition, AFValue, used to represent a time-series event, is
located in the AF namespace. For more information see the OSIsoft Tech Support page Asset
namespace (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
N_OSIsoft_AF_Asset.htm).

AF SDK 2.9 Getting Started Guide

11

Programming exercises
• OSIsoft.AF.Data
The Data namespace provides classes for obtaining rich data access (RDA) from assets
within an organization. Both historical and real-time data access are provided. Data access
across asset types allow for simpler architecture as well as helpful functionality such as
unit-of-measure conversions. For more information see the OSIsoft Tech Support page Data
namespace (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
N_OSIsoft_AF_Data.htm).
• OSIsoft.AF.PI
The PI namespace provides classes that are used to manage connections and access
information about PI Data Archive. It provides direct access to PI Data Archive and can be
used to find or edit PI points and read or write data directly to PI Data Archive. See the AF
SDK online reference for more information. For more information see the OSIsoft Tech
Support page AF.PI namespace (https://techsupport.osisoft.com/Documentation/PI-AFSDK/html/N_OSIsoft_AF_PI.htm).
• OSIsoft.AF.Time
The AF.Time namespace provides classes for performing time operations with the PI
System. It provides methods for converting time strings including abbreviations in the PI
time syntax (such as "*-5m") to an AFTime and converting .NET DateTime structures to and
from AF time structures. Time, time range, and time interval classes also facilitate accurate
representation across time zones and across daylight transitions. For more information see
the OSIsoft Tech Support page AF.Time namespace (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/N_OSIsoft_AF_Time.htm).
• OSIsoft.AF.EventFrame
The EventFrame namespace provides classes for reading and writing PI AF Event Frames.
These objects can be tied to PI AF elements and represent events that occur over a time
period, such as equipment downtime or abnormal behavior. For more information see the
OSIsoft Tech Support page EventFrame namespace (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/N_OSIsoft_AF_EventFrame.htm).
Namespaces are brought into scope in your application by adding the appropriate using
directives at the top of the class file. For example:
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Data;

Connect to a PI Server or AF Database
You can create a list of known AF servers by first initializing an instance of the PISystems
collection. The list of all PI Data Archive servers that are known to the client is represented as
PIServers. The code below shows how to obtain an object called My AF Server that represents
the PI AF server and an object called My PI Data Archive that represents PI Data Archive.
PISystems piSystems = new PISystems();
PISystem assetServer = piSystems["My AF Server"];
PIServers piServers = new PIServers();
PIServer piServer = piServers["My PI Data Archive"];

After you obtain a reference to the PISystem, you can refer to its collection of AFDatabase
objects by using the Databases property of piSystem, as shown in the following code:
12

AF SDK 2.9 Getting Started Guide

Programming exercises
AFDatabase database = assetServer.Databases["Meters"];

To return a specific AFDatabase, you pass a name to the AFDatabases indexer.
You can obtain additional references from the AFDatabase to other collections such as root
elements, categories, templates, and tables. To obtain a specific object from a collection, use the
collection's indexer and pass in a name, as shown in the following code.
AFElements rootElements = database.Elements;
AFElement element1 = rootElements["Meter1"];
AFCategories attributeCategories = database.AttributeCategories;
AFElementTemplates elementTemplates = database.ElementTemplates;
AFTables tables = database.Tables;

Notice that the object hierarchy corresponds very closely to the hierarchy you see in PI System
Explorer. Remember to use PI System Explorer as a guide for illustrating object relationships
within AF SDK.
Note that there is no explicit call to a Connect() method. AF SDK connects to the PI AF
database automatically as required. This automatic connection is called an implicit connection.
To make an explicit connection, you can call Connect() on a PISystem instance. Implicit and
explicit connections are discussed in more detail in the AF SDK Online Reference (https://
techsupport.osisoft.com/Documentation/PI-AF-SDK/html/a1616e71fb2e-40b1-9f14-299b2d9b269c.htm).

Example
static void PrintRootElements(AFDatabase database)
{
Console.WriteLine("Print Root Elements: {0}", database.Elements.Count);
foreach (AFElement element in database.Elements)
{
Console.WriteLine(" {0}", element.Name);
}
}

Console.WriteLine();

Lesson one exercises
For each of the following exercises, add logic to each method to accomplish the given task. Add
the methods to your console application and test each one by writing to the console.
• Exercise 1
Create a method with the definition and arguments shown below. Incorporate the method
into your console application and print the name of each AFElementTemplate. For each
element template, also print the name of each AFCategory in its Categories collection.
void PrintElementTemplates( AFDatabase database )

When the method is run, it should produce the following output:
Print
Name:
Name:
Name:

Element Templates
City; Categories:
MeterAdvanced; Categories: Shows Status;
MeterBasic; Categories: Measures Energy;

AF SDK 2.9 Getting Started Guide

13

Programming exercises
• Exercise 2
Create a method with the definition and arguments shown below. Incorporate the method
into your console application and print the name of each AFAttributeTemplate for the
passed-in element template. For each attribute template, also print the name of its data
reference plug-in.
void PrintAttributeTemplates(AFDatabase database, string elemTempName)

When the method is run, it should produce the following output:
Print Attribute Templates for Element Template: MeterAdvanced
Name: Status, DRPlugin: PI Point

• Exercise 3
Create a method with the definition and arguments shown below. Incorporate the method
into your console application and print the name of each unit of measure (UOM) and its
abbreviation under the UOMClass Energy.
void PrintEnergyUOMs( PISystem system )

When the method is run, it should produce the following output:
Print Energy UOMs
UOM: gigawatt hour, Abbreviation: GWh
UOM: megawatt hour, Abbreviation: MWh
UOM: watt hour, Abbreviation: Wh
UOM: joule, Abbreviation: J
UOM: British thermal unit, Abbreviation: Btu
UOM: calorie, Abbreviation: cal
UOM: gigajoule, Abbreviation: GJ
UOM: kilojoule, Abbreviation: kJ
UOM: kilowatt hour, Abbreviation: kWh
UOM: megajoule, Abbreviation: MJ
UOM: watt second, Abbreviation: Ws
UOM: kilocalorie, Abbreviation: kcal
UOM: million calorie, Abbreviation: MMcal
UOM: million British thermal unit, Abbreviation: MM Btu

• Exercise 4
Create a method with the definition and arguments shown below. Incorporate the method
into your console application and print the name of each AFEnumerationSet object. For
each set, print its list of states.
void PrintEnumerationSets( AFDatabase database )

When the method is run, it should produce the following output:
Print Enumeration Sets
Building Type
0 - A
1 - B
Meter Status
0 - Good
1 - Bad

• Exercise 5
Create a method with the definition and arguments shown below. Incorporate the method
into your console application and print the name of each element category and attribute
category.

14

AF SDK 2.9 Getting Started Guide

Programming exercises
void PrintCategories( AFDatabase database )

When the method is run, it should produce the following output:
Print Categories
Element Categories
Measures Energy
Shows Status
Attribute Categories
Building Info
Location
Time-Series Data

Lesson 1 hints and tips
AF SDK maintains an internal cache for each PI AF identity. For example, if two users have
exactly the same access to the PI AF database, then they would share the same cache. The cache
is initialized when the first connection to the database is made. Internally, AF SDK allows
connections to the PI AF server and PI Data Archive to drop after periods of inactivity and
automatically reestablishes connection when activity is resumed. The user cache is preserved
between periods of inactivity and can be reused after reconnecting. Calling Disconnect()
explicitly, however, clears the user cache.
When using a PISystem or PIServer instance, OSIsoft does not recommend that you
manually incorporate your own reconnect logic using Disconnect() and Connect(); doing
so forces the cache to re-initialize upon reconnecting. In most of your projects with AF SDK,
OSIsoft recommends using implicit connections and having AF SDK handle reconnecting.

Lesson 1 exercise solutions
This section contains sample code that solves each of the exercises in this lesson.
• Exercise 1
static void PrintElementTemplates(AFDatabase database)
{
Console.WriteLine("Print Element Templates");
AFNamedCollectionList elemTemplates =
database.ElementTemplates.FilterBy(typeof(AFElement));
foreach (AFElementTemplate elemTemp in elemTemplates)
{
Console.WriteLine("Name: {0}; Categories: {1}", elemTemp.Name,
elemTemp.CategoriesString);
}
}

Console.WriteLine();

• Exercise 2
static void PrintAttributeTemplates(AFDatabase database, string elemTempName)
{
Console.WriteLine("Print Attribute Templates for Element Template:
{0}", elemTempName);
AFElementTemplate elemTemp = database.ElementTemplates[elemTempName];
foreach (AFAttributeTemplate attrTemp in elemTemp.AttributeTemplates)
{
AF SDK 2.9 Getting Started Guide

15

Programming exercises
string drName = attrTemp.DataReferencePlugIn ==
null ? "None" : attrTemp.DataReferencePlugIn.Name;
Console.WriteLine("Name: {0}, DRPlugin: {1}", attrTemp.Name, drName);

}

}
Console.WriteLine();

• Exercise 3
static void PrintEnergyUOMs(PISystem system)
{
Console.WriteLine("Print Energy UOMs");
UOMClass uomClass = system.UOMDatabase.UOMClasses["Energy"];
foreach (UOM uom in uomClass.UOMs)
{
Console.WriteLine("UOM: {0}, Abbreviation: {1}", uom.Name,
uom.Abbreviation);
}
Console.WriteLine();
}

Note that the UOMDatabase belongs to PISystem and is not exclusive to an AFDatabase.
• Exercise 4
static void PrintEnumerationSets(AFDatabase database)
{
Console.WriteLine("Print Enumeration Sets");
AFEnumerationSets enumSets = database.EnumerationSets;
foreach (AFEnumerationSet enumSet in enumSets)
{
Console.WriteLine(enumSet.Name);
foreach (AFEnumerationValue state in enumSet)
{
int stateValue = state.Value;
string stateName = state.Name;
Console.WriteLine("{0} - {1}", stateValue, stateName);
}

}

}

Console.WriteLine();

• Exercise 5
static void PrintCategories(AFDatabase database)
{
Console.WriteLine("Print Categories");
AFCategories elemCategories = database.ElementCategories;
AFCategories attrCategories = database.AttributeCategories;
Console.WriteLine("Element Categories");
foreach (AFCategory category in elemCategories)
{
Console.WriteLine(category.Name);
}
Console.WriteLine();
Console.WriteLine("Attribute Categories");
foreach (AFCategory category in attrCategories)
{
Console.WriteLine(category.Name);
}

16

AF SDK 2.9 Getting Started Guide

Programming exercises
}

Console.WriteLine();

AF SDK 2.9 Getting Started Guide

17

Programming exercises

Lesson 2: Searching for assets
After completing this lesson you will be able to:
• Use AF SDK to search for assets (elements and their attributes).
• Experiment with the different search criteria that can be used.
• Evaluate how to handle search results.

Help for methods and data types used in this lesson
• AFElement Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_Asset_AFElement.htm)
• AFElementSearch Constructor (https://techsupport.osisoft.com/Documentation/PI-AFSDK/html/M_OSIsoft_AF_Search_AFElementSearch__ctor_1.htm)
• AFElementSearch.FindElements() Method (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/
M_OSIsoft_AF_Search_AFElementSearch_FindElements.htm)
• AFAttribute.Categories Property (https://techsupport.osisoft.com/Documentation/PI-AFSDK/html/P_OSIsoft_AF_Asset_AFAttribute_Categories.htm)

Introduction
OSIsoft recommends that you reduce the search results to a minimum on the server, not on the
client. In other words, you should call AF SDK methods that allow the server to process the
search results instead of writing custom search logic in the client code.
Starting with PI AF 2016 (2.8), search query methods are available that process query strings
instead of methods for each type of search. For the exercises in this section, use the new
AFSearch methods such as the AFElementSearch class.
For this lesson, imagine you are a software developer for a power company. Your assignment is
to develop a client application that allows users to search for electric meters (PI AF Elements)
and attributes. When the application is complete, users should be able to search for meters by
name, element template, element category, and attribute values. The user should also be able to
search for meter attributes by name, element template, and attribute category.
Use the PI AF database named Green Power Company, which is located on your PI AF server.
You will be asked to write several methods that search for elements and attributes and print
the search results to the console. Before starting, it might help to familiarize yourself with the
Green Power Company database using PI System Explorer.

Example
The first example problem is shown here along with its solution. For this first problem, you
should implement the following methods inside Program2.cs and run the application with
the arguments provided for each of the following methods in Main().
static void FindMetersByName(AFDatabase database, string elementNameFilter)
{
Console.WriteLine("Find Meters by Name: {0}", elementNameFilter);
// Default search is as an element name string mask.
string querystring = string.Format("{0}", elementNameFilter);
AFElementSearch elementquery = new AFElementSearch(database,

18

AF SDK 2.9 Getting Started Guide

Programming exercises
"ElementSearch", querystring);
foreach (AFElement element in elementquery.FindElements())
{
Console.WriteLine("Element: {0}, Template: {1}, Categories: {2}",
element.Name,
element.Template.Name,
element.CategoriesString);
}
Console.WriteLine();

Lesson 2 exercises
For each of the following exercises, your task is to add logic to each method. Add the methods
to your console application and test each one by writing output to the console.
• Exercise 1
Find all meters that were created from the provided element template and derived
templates. Print the names of each found meter in a list and their element template names.
Count the number of derived templates that were found.
void FindMetersByTemplate( AFDatabase database, string templateName)

When the method is run, it should product the following output:
Find Meters by Name: Meter00*
Element: Meter001, Template: MeterBasic, Categories: Measures Energy;
Element: Meter002, Template: MeterBasic, Categories: Measures Energy;
Element: Meter003, Template: MeterBasic, Categories: Measures Energy;
Element: Meter004, Template: MeterBasic, Categories: Measures Energy;
Element: Meter005, Template: MeterBasic, Categories: Measures Energy;
Element: Meter006, Template: MeterBasic, Categories: Measures Energy;
Element: Meter007, Template: MeterBasic, Categories: Measures Energy;
Element: Meter008, Template: MeterBasic, Categories: Measures Energy;
Element: Meter009, Template: MeterAdvanced, Categories: Measures Energy;Shows
Status;

• Exercise 2
Find all meters supplied by the provided substation. Print the names of each of the meters
that were found.
void FindMetersBySubstation( AFDatabase database, string substationLocation)

When the method is run, it should product the following output:
Find Meters by Template: MeterBasic
Element: Meter001, Template: MeterBasic
Element: Meter002, Template: MeterBasic
Element: Meter003, Template: MeterBasic
Element: Meter004, Template: MeterBasic
Element: Meter005, Template: MeterBasic
Element: Meter006, Template: MeterBasic
Element: Meter007, Template: MeterBasic
Element: Meter008, Template: MeterBasic
Element: Meter009, Template: MeterAdvanced
Element: Meter010, Template: MeterAdvanced
Element: Meter011, Template: MeterAdvanced
Element: Meter012, Template: MeterAdvanced
Found 4 derived templates

AF SDK 2.9 Getting Started Guide

19

Programming exercises
• Exercise 3
Find all meters with energy usage greater than the supplied limit. Print the names of each
meter that was found.
void FindMetersAboveUsage( AFDatabase database, double limit)

When the method is run, it should product the following output:
Find Meters above Usage: 300
Meter003, Meter005, Meter006, Meter008, Meter009, Meter012

• Exercise 4
Find all attributes belonging to the Building Info attribute category that are part of the
provided element template. Print the number of attributes found.
void FindBuildingInfo( AFDatabase database, string templateName)

When the method is run, it should product the following output:
Find Building Info: MeterAdvanced
Found 8 attributes.

Lesson 2 hints and tips
A general pattern for searching is shown here:
using (AFElementSearch elementQuery = new AFElementSearch(database,
"nameOfSearch", queryString))
{
elementQuery.CacheTimeout = TimeSpan.FromMinutes(5);
foreach (AFElement element in elementQuery.FindElements())
{
ProcessElement(element);
}
}

Lesson 2 exercise solutions
• Exercise 1: Find meters by template
static void FindMetersByTemplate(AFDatabase database, string templateName)
{
Console.WriteLine("Find Meters by Template: {0}", templateName);
using (AFElementSearch elementQuery =
new AFElementSearch(database, "TemplateSearch",
string.Format("template:\"{0}\"", templateName)))
{
elementQuery.CacheTimeout = TimeSpan.FromMinutes(5);
int countDerived = 0;
foreach (AFElement element in elementQuery.FindElements())
{
Console.WriteLine("Element: {0}, Template: {1}", element.Name,
element.Template.Name);
if (element.Template.Name != templateName)
countDerived++;
}
Console.WriteLine("
Console.WriteLine();

20

AF SDK 2.9 Getting Started Guide

Found {0} derived templates", countDerived);

Programming exercises
}

}

• Exercise 2: Find meters by substation
static void FindMetersBySubstation(AFDatabase database, string
substationLocation)
{
Console.WriteLine("Find Meters by Substation: {0}", substationLocation);
string templateName = "MeterBasic";
string attributeName = "Substation";
using (AFElementSearch elementQuery =
new AFElementSearch(database, "AttributeValueEQSearch",
string.Format("template:\"{0}\" \"|{1}\":\"{2}\"",
templateName, attributeName, substationLocation)))
{

}

}

elementQuery.CacheTimeout = TimeSpan.FromMinutes(5);
int countNames = 0;
foreach (AFElement element in elementQuery.FindElements())
{
Console.Write("{0}{1}", countNames++ == 0 ? string.Empty : ", ",
element.Name);
}
Console.WriteLine();

• Exercise 3: Find meters with above-average usage:
static void FindMetersAboveUsage(AFDatabase database, double val)
{
Console.WriteLine("Find Meters above Usage: {0}", val);
string templateName = "MeterBasic";
string attributeName = "Energy Usage";
AFElementSearch elementquery = new AFElementSearch(database,
"AttributeValueGTSearch",
string.Format("template:\"{0}\" \"|{1}\":>{2}", templateName,
attributeName, val));
int countNames = 0;
foreach (AFElement element in elementquery.FindElements())
{
Console.Write("{0}{1}", countNames++ ==
0 ? string.Empty : ", ", element.Name);
}
}

Console.WriteLine();

• Exercise 4: Find building information
static void FindBuildingInfo(AFDatabase database, string templateName)
{
Console.WriteLine("Find Building Info: {0}", templateName);
AFElementTemplate elemTemp = database.ElementTemplates[templateName];
AFCategory buildingInfoCat = database.AttributeCategories["Building Info"];
AFSearchToken token = new AFSearchToken(AFSearchFilter.Template,
AFSearchOperator.Equal, elemTemp.GetPath());

AF SDK 2.9 Getting Started Guide

21

Programming exercises
AFElementSearch search =
new AFElementSearch(database, "search", new[] { token });
IEnumerable foundElements = search.FindElements();
AFNamedCollectionList foundAttributes =
new AFNamedCollectionList();
foreach (AFElement foundElem in foundElements)
{
foreach (AFAttribute attr in foundElem.Attributes)
{
if (attr.Categories.Contains(buildingInfoCat))
{
foundAttributes.Add(attr);
}
}
}

}

22

Console.WriteLine("Found {0} attributes.", foundAttributes.Count);
Console.WriteLine();

AF SDK 2.9 Getting Started Guide

Programming exercises

Lesson 3: Reading and writing data
After completing this lesson you will be able to:
• Use AF SDK to read data from PI AF
• Use AF SDK to write data to PI AF
• Use bulk calls to retrieve data within a specified time range for many attributes
• Use the AFValue object to store time-series information

Help for methods and data types used in this lesson
• AFValue Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_Asset_AFValue.htm)
• PIPagingConfiguration() Constructor (https://techsupport.osisoft.com/Documentation/PIAF-SDK/html/M_OSIsoft_AF_PI_PIPagingConfiguration__ctor.htm)
• AFAttribute ListClass (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_Asset_AFAttributeList.htm)

Bulk calls
To retrieve data within a specified time range for many attributes, it is time-consuming to issue
a data call for each attribute, because many round trips over the network to the PI Data Archive
are required. In networked systems, latency delays can be costly.
You can reduce network latency (the number of network round trips) by using list calls in AF
SDK. First, you create a list of attributes to retrieve and store them in an AFAttributeList.
You then access the Data property on the AFAttributeList, which exposes data retrieval
methods for the entire attribute list. Now, you can make a single call to the server to retrieve
data for all of the attributes, as shown in the following code:
AFAttributeList attrList = AFAttribute.FindElementAttributes(
// argument list omitted for brevity
);
// Bulk call to get value at specified time for all found attributes
IList vals = attrList.Data.RecordedValue(time);

You might be wondering why the above example used AFAttributeList instead of
AFAttributes. The reason in that AFAttributes returns a list of attributes for an element,
whereas AFAttributeList returns a list of unrelated AFAttributes. For more information,
see AFAttributes (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_Asset_AFAttributes.htm) and AFAttributeList (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/T_OSIsoft_AF_Asset_AFAttributeList.htm).
The AFValue object contains two important properties:
• AFValue.Timestamp: An AFTime object that represents the event's timestamp
• AFValue.Value: A .NET object representing the event's value that can be cast to the
appropriate type
AFTime is similar to a .NET DateTime object; however, AFTime objects can be constructed
using PI Time syntax (for example: "t" or "*-10m").

AF SDK 2.9 Getting Started Guide

23

Programming exercises

Example
To read time-series data, you first obtain a reference to the AFAttribute, and then access its
Data property. The data property is an AFData object that exposes methods to retrieve timeseries data. In the example here, all of the archived values of an attribute for the last 10
minutes are retrieved.
AFTime startTime = new AFTime("*-10m");
AFTime endTime = new AFTime("*");
AFTimeRange timeRange = new AFTimeRange(startTime, endTime);
AFValues vals = attr.Data.RecordedValues(
timeRange: timeRange,
boundaryType: AFBoundaryType.Inside,
desiredUOM: null,
filterExpression: null,
includeFilteredValues: false);

Lesson 3 exercises
Your assignment is to develop a client application that allows users to read data from and write
data to the PI Data Archive.
You will use the PI AF database Green Power Company located on your local PI AF server. You
will be asked to write several methods to retrieve historical data from the PI Data Archive and
print the results to the console. You will also implement methods to write data. Before starting
the exercises, use PI System Explorer to familiarize yourself with the PI AF database.
For this lesson, implement the following methods inside Program3.cs and run the application
with the arguments provided for each of the following methods in Main().
• Exercise 1
Create a method to retrieve interpolated values for the Energy Usage attribute for the
specified meter element. The startTime and endTime should be in PI Time syntax. Print
the following information for each returned AFValue: Timestamp (Local time), Value in
kilowatt hours.
void PrintInterpolated(AFDatabase database, string meterName,
string startTime, string endTime, TimeSpan timeSpan)

When the method is run, it should product the following output:
Print Interpolated
Timestamp (Local):
Timestamp (Local):
Timestamp (Local):
Timestamp (Local):

Values 7/6/2017
7/6/2017
7/6/2017
7/6/2017

Meter: Meter001, Start: *-30s, End: *
4:14:29 PM, Value: 271.21 kWh
4:14:39 PM, Value: 270.92 kWh
4:14:49 PM, Value: 270.92 kWh
4:14:59 PM, Value: 270.92 kWh

• Exercise 2
Create a method to print the hourly time-weighted average for the Energy Usage attribute
for the specified meter element. The startTime and endTime should be in PI Time syntax
with time range larger than one hour. Print the following information for each returned
AFValue: Timestamp (Local time), Value in kilowatt hours.
void PrintHourlyAverage(AFDatabase database, string meterName,
string startTime, string endTime)

When the method is run, it should product the following output:

24

AF SDK 2.9 Getting Started Guide

Programming exercises
Print Hourly Average - Meter:
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05
Timestamp (Local): 2017-07-05

Meter001, Start: y, End: t
00h, Value: 219.71 kWh
01h, Value: 269.12 kWh
02h, Value: 287.00 kWh
03h, Value: 321.93 kWh
04h, Value: 304.84 kWh
05h, Value: 319.51 kWh
06h, Value: 312.74 kWh
07h, Value: 307.56 kWh
08h, Value: 326.19 kWh
09h, Value: 283.09 kWh
10h, Value: 325.61 kWh
11h, Value: 319.55 kWh
12h, Value: 308.70 kWh
13h, Value: 241.24 kWh
14h, Value: 291.32 kWh
15h, Value: 300.86 kWh
16h, Value: 314.04 kWh
17h, Value: 297.21 kWh
18h, Value: 343.67 kWh
19h, Value: 281.79 kWh
20h, Value: 273.63 kWh
21h, Value: 289.00 kWh
22h, Value: 282.71 kWh
23h, Value: 310.37 kWh

• Exercise 3
Create a method to retrieve the archived event at the given timestamp for the Energy Usage
attributes for all meters. The method should use the AFAttributeList object. Print the
following information for each meter: Meter name, Timestamp (Local time), Value in
kilowatt hour.
void PrintEnergyUsageAtTime(AFDatabase database, string timeStamp)

When the method is run, it should product the following output:
Print Energy Usage at Time: t+10h
Meter: Meter001, Timestamp (Local):
Meter: Meter002, Timestamp (Local):
Meter: Meter003, Timestamp (Local):
Meter: Meter004, Timestamp (Local):
Meter: Meter005, Timestamp (Local):
Meter: Meter006, Timestamp (Local):
Meter: Meter007, Timestamp (Local):
Meter: Meter008, Timestamp (Local):
Meter: Meter009, Timestamp (Local):
Meter: Meter010, Timestamp (Local):
Meter: Meter011, Timestamp (Local):
Meter: Meter012, Timestamp (Local):

2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06
2017-07-06

10h,
10h,
10h,
10h,
10h,
10h,
10h,
10h,
10h,
10h,
10h,
10h,

Value:
Value:
Value:
Value:
Value:
Value:
Value:
Value:
Value:
Value:
Value:
Value:

240.79
320.99
146.76
247.14
300.47
343.78
300.69
368.51
255.79
387.83
254.49
391.02

kWh
kWh
kWh
kWh
kWh
kWh
kWh
kWh
kWh
kWh
kWh
kWh

• Exercise 4
Create a method that retrieves the daily averages of the Energy Usage attribute for all
meters. The startTime and endTime should be in PI Time syntax. The method should use
the AFAttributeList object. Print the following information for each meter: Meter name,
Timestamp (Local time), Avg. Value in kilowatt hours.
void PrintDailyAverageEnergyUsage(AFDatabase database, string startTime,
string endTime)

When the method is run, it should product the following output:

AF SDK 2.9 Getting Started Guide

25

Programming exercises
Print Daily Energy Usage - Start: t-7d, End: t
Averages for Meter: Meter001
Timestamp (Local): 2017-06-29, Avg. Value: 292.41
Timestamp (Local): 2017-06-30, Avg. Value: 296.29
Timestamp (Local): 2017-07-01, Avg. Value: 288.61
Timestamp (Local): 2017-07-02, Avg. Value: 289.56
Timestamp (Local): 2017-07-03, Avg. Value: 305.59
Timestamp (Local): 2017-07-04, Avg. Value: 300.72
Timestamp (Local): 2017-07-05, Avg. Value: 297.14

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter002
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

290.59
301.80
313.02
296.27
296.36
294.80
290.91

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter003
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

306.56
296.34
297.13
303.96
296.18
296.94
294.30

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter004
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

290.80
303.45
300.44
295.50
300.97
302.34
290.79

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter005
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

295.32
296.99
306.82
305.22
304.02
301.04
306.42

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter006
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

304.98
300.31
314.78
300.96
314.68
304.36
292.20

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter007
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

299.20
300.51
296.11
311.83
307.76
300.82
303.01

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter008

26

AF SDK 2.9 Getting Started Guide

Programming exercises
Timestamp
Timestamp
Timestamp
Timestamp
Timestamp
Timestamp
Timestamp

(Local):
(Local):
(Local):
(Local):
(Local):
(Local):
(Local):

2017-06-29,
2017-06-30,
2017-07-01,
2017-07-02,
2017-07-03,
2017-07-04,
2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

298.25
304.60
294.96
308.05
299.51
295.44
304.02

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter009
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

291.10
300.96
293.08
297.69
297.84
298.78
309.21

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter010
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

311.84
295.10
296.29
298.10
298.69
298.91
299.91

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter011
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

297.88
303.83
299.66
303.63
297.02
299.82
306.44

kWh
kWh
kWh
kWh
kWh
kWh
kWh

Averages for Meter: Meter012
Timestamp (Local): 2017-06-29,
Timestamp (Local): 2017-06-30,
Timestamp (Local): 2017-07-01,
Timestamp (Local): 2017-07-02,
Timestamp (Local): 2017-07-03,
Timestamp (Local): 2017-07-04,
Timestamp (Local): 2017-07-05,

Avg.
Avg.
Avg.
Avg.
Avg.
Avg.
Avg.

Value:
Value:
Value:
Value:
Value:
Value:
Value:

291.31
301.20
299.30
295.43
299.08
306.22
300.89

kWh
kWh
kWh
kWh
kWh
kWh
kWh

• Exercise 5
Occasionally, the Energy Usage meter data is incorrect. The values for one meter actually
belong to another meter and vice versa. Therefore, implement the method from Exercise 4
which takes the name of two meters and start and end time in PI Time syntax. The method
should swap the Energy Usage attribute values between the two meters within the given
time range. Try to implement this with only two UpdateValues calls. What are some
tradeoffs when limiting the number of remote calls in this case? How would you ensure
there is no data loss?
void SwapValues(AFDatabase database, string meter1, string meter2,
string startTime, string endTime)

When the method is run, it should product the following output:
Swap values for meters: Meter001, Meter002 between y and y+1h

AF SDK 2.9 Getting Started Guide

27

Programming exercises

Lesson 3 hints and tips
For exercises 4 and 5, use the GetAttributes() helper function to obtain an
AFAttributeList, then use AFAttributeList.Data to make the bulk calls.
For exercise 4, do not use RecordedValues() and then compute the average in your code.
Instead, use the Summaries() method to allow PI Data Archive to compute the average. Then,
return only the average to the client.
For exercise 5, the existing archived values must be removed before inserting new values. Use
the AFListData.UpdateValues in conjunction with AFUpdateOption.Remove.

Lesson 3 exercise solutions
• Exercise 1, print interpolated values
static void PrintInterpolated(AFDatabase database, string meterName,
string startTime, string endTime, TimeSpan timeSpan)
{
Console.WriteLine(string.Format("Print Interpolated Values - Meter: {0},
Start: {1}, End: {2}", meterName, startTime, endTime));
AFAttribute attr =
AFAttribute.FindAttribute(@"\Meters\" + meterName + @"|Energy Usage",
database);
AFTime start = new AFTime(startTime);
AFTime end = new AFTime(endTime);
AFTimeRange timeRange = new AFTimeRange(start, end);
AFTimeSpan interval = new AFTimeSpan(timeSpan);
AFValues vals = attr.Data.InterpolatedValues(
timeRange: timeRange,
interval: interval,
desiredUOM: null,
filterExpression: null,
includeFilteredValues: false);

}

foreach (AFValue val in vals)
{
Console.WriteLine("Timestamp (Local): {0},
Value: {1:0.00} {2}", val.Timestamp.LocalTime, val.Value,
val?.UOM.Abbreviation);
}
Console.WriteLine();

• Exercise 2, print hourly average
static void PrintHourlyAverage(AFDatabase database, string meterName,
string startTime, string endTime)
{
Console.WriteLine(string.Format
("Print Hourly Average - Meter: {0}, Start: {1}, End: {2}", meterName,
startTime, endTime));
AFAttribute attr =
AFAttribute.FindAttribute(@"\Meters\" + meterName + @"|Energy Usage",
database);

28

AF SDK 2.9 Getting Started Guide

Programming exercises
AFTime start = new AFTime(startTime);
AFTime end = new AFTime(endTime);
AFTimeRange timeRange = new AFTimeRange(start, end);
IDictionary vals = attr.Data.Summaries(
timeRange: timeRange,
summaryDuration: new AFTimeSpan(TimeSpan.FromHours(1)),
summaryType: AFSummaryTypes.Average,
calcBasis: AFCalculationBasis.TimeWeighted,
timeType: AFTimestampCalculation.EarliestTime);
foreach (AFValue val in vals[AFSummaryTypes.Average])
{
Console.WriteLine("Timestamp (Local):
{0:yyyy-MM-dd HH\\h}, Value: {1:0.00} {2}", val.Timestamp.LocalTime,
val.Value,val?.UOM.Abbreviation);
}
}

Console.WriteLine();

• Exercise 3, print energy usage
static void PrintEnergyUsageAtTime(AFDatabase database, string timeStamp)
{
Console.WriteLine("Print Energy Usage at Time: {0}", timeStamp);
AFAttributeList attrList = GetAttributes(database, "MeterBasic",
"Energy Usage");
AFTime time = new AFTime(timeStamp);

}

IList vals = attrList.Data.RecordedValue(time);
foreach (AFValue val in vals)
{
Console.WriteLine("Meter: {0},
Timestamp (Local): {1:yyyy-MM-dd HH\\h}, Value: {2:0.00} {3}",
val.Attribute.Element.Name,val.Timestamp.LocalTime,
val.Value,val?.UOM.Abbreviation);
}
Console.WriteLine();

• Exercise 4, print daily average energy usage
static void PrintDailyAverageEnergyUsage(AFDatabase database,
string startTime, string endTime)
{
Console.WriteLine(string.Format("Print Daily Energy Usage - Start: {0},
End: {1}", startTime, endTime));
AFAttributeList attrList = GetAttributes(database,
"MeterBasic", "Energy Usage");
AFTime start = new AFTime(startTime);
AFTime end = new AFTime(endTime);
AFTimeRange timeRange = new AFTimeRange(start, end);
// Ask for 100 PI Points at a time
PIPagingConfiguration pagingConfig =
new PIPagingConfiguration(PIPageType.TagCount, 100);
IEnumerable> summaries =
attrList.Data.Summaries(
timeRange: timeRange,
AF SDK 2.9 Getting Started Guide

29

Programming exercises
summaryDuration: new AFTimeSpan(TimeSpan.FromDays(1)),
summaryTypes: AFSummaryTypes.Average,
calculationBasis: AFCalculationBasis.TimeWeighted,
timeType: AFTimestampCalculation.EarliestTime,
pagingConfig: pagingConfig);
// Loop through attributes
foreach (IDictionary dict in summaries)
{
AFValues values = dict[AFSummaryTypes.Average];
Console.WriteLine("Averages for Meter: {0}",
values.Attribute.Element.Name);
// Loop through values per attribute
foreach (AFValue val in dict[AFSummaryTypes.Average])
{
Console.WriteLine("Timestamp (Local): {0:yyyy-MM-dd},
Avg. Value: {1:0.00} {2}",
val.Timestamp.LocalTime,val.Value,val?.UOM.Abbreviation);
}
Console.WriteLine();

}

}
Console.WriteLine();

• Exercise 5, swap values
static void SwapValues(AFDatabase database, string meter1, string meter2,
string startTime, string endTime)
{
Console.WriteLine
(string.Format("Swap values for meters: {0}, {1} between {2} and {3}",
meter1, meter2, startTime, endTime));
// NOTE: This method does not ensure that there is no data loss
// if there is failure.
// Persist the data first in case you need to rollback.
AFAttribute attr1 = AFAttribute.FindAttribute(@"\Meters\" +
meter1 + @"|Energy Usage", database);
AFAttribute attr2 = AFAttribute.FindAttribute(@"\Meters\" +
meter2 + @"|Energy Usage", database);
AFTime start = new AFTime(startTime);
AFTime end = new AFTime(endTime);
AFTimeRange timeRange = new AFTimeRange(start, end);
// Get values to delete for meter1
AFValues valsToRemove1 = attr1.Data.RecordedValues(
timeRange: timeRange,
boundaryType: AFBoundaryType.Inside,
desiredUOM: null,
filterExpression: null,
includeFilteredValues: false);
// Get values to delete for meter2
AFValues valsToRemove2 = attr2.Data.RecordedValues(
timeRange: timeRange,
boundaryType: AFBoundaryType.Inside,
desiredUOM: null,
filterExpression: null,
includeFilteredValues: false);
List valsToRemove = valsToRemove1.ToList();
valsToRemove.AddRange(valsToRemove2.ToList());

30

AF SDK 2.9 Getting Started Guide

Programming exercises
// Remove the values
AFListData.UpdateValues(valsToRemove, AFUpdateOption.Remove);
// Create new AFValues from the other meter and assign them to this meter
List valsToAdd1 = valsToRemove2.Select(v => new AFValue(attr1,
v.Value, v.Timestamp)).ToList();
List valsToAdd2 = valsToRemove1.Select(v => new AFValue(attr2,
v.Value, v.Timestamp)).ToList();
List valsCombined = valsToAdd1;
valsCombined.AddRange(valsToAdd2);

}

AFListData.UpdateValues(valsCombined, AFUpdateOption.Insert);
Console.WriteLine();

AF SDK 2.9 Getting Started Guide

31

Programming exercises

Lesson 4: Building an AF hierarchy
After completing this lesson you will be able to:
• Use AF SDK to build a PI AF hierarchy.
• Create a PI AF database.
• Create element and attribute templates.
• Create an asset hierarchy, and link to PI Points from the attribute configuration.
• Use recommended AF SDK patterns such as checking in changes in bulk.

Help for methods and data types used in this lesson
• AFDatabase.ElementTemplate.Add() Method (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/M_OSIsoft_AF_Asset_AFElementTemplates_Add.htm)
• AFDatabase.Elements.Add() Method (https://techsupport.osisoft.com/Documentation/PIAF-SDK/Html/M_OSIsoft_AF_Asset_AFElements_Add_3.htm)
• AFDatabase.CheckIn() Method (https://techsupport.osisoft.com/Documentation/PI-AFSDK/html/M_OSIsoft_AF_AFDatabase_CheckIn.htm)
• AFReferenceType Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/
html/T_OSIsoft_AF_Asset_AFReferenceType.htm)

Introduction
When attempting to add an element to a database, you should always check to see if an element
with the same name already exists in the database before adding it. Attempting to add an
element that already exists in the database will generate an exception.
The following code shows one way to add an element named Meters to the database:
// Attempts to retrieve the element "Meters", and adds the element after
checking
// to see if it already exists
AFElement meters = database.Elements["Meters"]
if (meters == null)
meters = database.Elements.Add("Meters");

Alternatively, the previous operation could be written using a single line by using the C#
coalesce operator: ??, as shown here:
AFElement meters = database.Elements["Meters"] ?? database.Elements.Add("Meters")

Shown below is a simple example that shows how to create a new PI AF database, PI AF
element, and PI AF attribute, and then check in the changes. Refer to the Example 4 exercises
section for more detailed examples.
PISystem assetServer = new PISystems().DefaultPISystem;
AFDatabase database = assetServer.Databases.Add("MyDb");
AFElement newEl = database.Elements[“MyElement”] ??
database.Elements.Add(“MyElement”);
AFAttribute newAttr = newEl.Attributes["MyAttribute"] ??
newE1.Attributes.Add(MyAttribute");

32

AF SDK 2.9 Getting Started Guide

Programming exercises

database.CheckIn();

Example
The following example shows how to create an element template called FeederTemplate. The
example creates one attribute template called District which contains no data reference,
then creates another attribute template called Power. The default UOM is set to watt and the
Type to Single. The example also sets the data reference to PI Point.
See the following links for more information:
• Configuration of data references (https://livelibrary.osisoft.com/LiveLibrary/content/en/
server-v9/GUID-FBC4ED44-9448-4C17-95FE-BA55AA5CABF1)
• Units of measure in PI AF (https://livelibrary.osisoft.com/LiveLibrary/content/en/serverv9/GUID-B6ABBFA8-22EE-42E3-84F3-BBAA638EFE58)
• PI point data reference (https://livelibrary.osisoft.com/LiveLibrary/content/en/server-v9/
GUID-9FA38FDC-4325-4222-BBBF-926DC1183685)
static void CreateElementTemplate(AFDatabase database)
{
string templateName = "FeederTemplate";
AFElementTemplate feederTemplate;
if (database.ElementTemplates.Contains(templateName))
return;
else
feederTemplate = database.ElementTemplates.Add(templateName);
AFAttributeTemplate cityAttributeTemplate =
feederTemplate.AttributeTemplates.Add("City");
cityattributeTemplate.Type = typeof(string);
AFAttributeTemplate power = feederTemplate.AttributeTemplates.Add("Power");
power.Type = typeof(Single);
power.DefaultUOM = database.assetServer.UOMDatabase.UOMs["watt"];
power.DataReferencePlugIn =
database.PISystem.DataReferencePlugIns["PI Point"];
}

database.CheckIn();

AF SDK 2.9 Getting Started Guide

33

Programming exercises

Lesson 4 exercises
• Exercise 1
Create an element called Feeders directly under the database root.
• Exercise 2
Create an element called Feeder001 under Feeders based off of the FeederTemplate
element template. Set the value of the City attribute to London. Set the ConfigString
property of the Power attribute such that the attribute's data source is the SINUSOID PI
Point.
• Exercise 3
Create a weak reference (https://livelibrary.osisoft.com/LiveLibrary/content/en/serverv9/GUID-4318CCE5-2EBC-4239-BA04-8F0B1808BD53) between Feeder001 and the
Feeder001 element. The result should resemble the following structure:
Elements
- Configuration
- Feeders
- Feeder001
- Geographical Locations
- London
- Feeder001
- Meter001
- Meter002
- Meter003
- Meter004
+ Montreal
+ San Francisco
+ Meters

Bonus exercises
Create a replica of the AF Database Green Power Company.
1. Create an AF Database named Ethical Power Company.
AFDatabase CreateDatabase( string servername, string databasename )

2. Create the element categories and attribute categories.
void CreateCategories( AFDatabase database )

3. Create the enumeration sets and their values.
void CreateEnumerationSets( AFDatabase database )

4. Create the PI AF Element Templates and their attribute templates. Set the properties and
categories based on the reference database.
void CreateTemplates( AFDatabase database )

5. Create a root element called Meters. Create individual meter elements from the appropriate
templates. For Meter001 through Meter008 use MeterBasic. For Meter009 through
Meter012 use MeterAdvanced.
void CreateElements( AFDatabase database )

6. Set the attribute values of the meter as appropriate based on the reference database. Only
do this for the first meter.
void SetAttributeValues( AFDatabase database )

34

AF SDK 2.9 Getting Started Guide

Programming exercises
7. Create the root element called Geographical Locations. Then, create three child
elements called London, Montreal, and San Francisco, based on the District template.
void CreateDistrictElements( AFDatabase database )

8. Add meter elements as weak references under the District elements, following the
reference database.
void CreateWeakReferences( AFDatabase database )

Lesson 4 hints and tips
• Create an AFEnumerationSet
AFEnumerationSet bTypeEnum = database.EnumerationSets.Add("Building Type");
bTypeEnum.Add("Residential", 0);
bTypeEnum.Add("Business", 1);

• Set AFAttributeTemplate properties
AFAttributeTemplate energyUsageAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("Energy Usage");
energyUsageAttrTemp.Type = typeof(Single);
energyUsageAttrTemp.Categories.Add(tsDataA);
energyUsageAttrTemp.DefaultUOM = uom;
energyUsageAttrTemp.DataReferencePlugIn =
database.PISystem.DataReferencePlugIns["PI Point"];
energyUsageAttrTemp.ConfigString =
@"\\%@\Configuration|PIDataArchiveName%\%Element%.%Attribute%;UOM=kWh";

• Set AFAttributeTemplate type to be an enumeration set
AFAttributeTemplate attrTemp = elemTemp.AttributeTemplates["Building Type"];
AFEnumerationSet enumSet = database.EnumerationSets["Building Type"];
attrTemp.TypeQualifier = enumSet;

• Check in the changes
if (database.IsDirty)
database.CheckIn();

• Commit changes using bulk CheckIn
In the exercise solution code, you perform CheckIn operations in bulk to improve
performance. This is accomplished by calling CheckIn less frequently. For most situations,
avoid calling CheckIn on every object change because each causes a call to the SQL server.
When making multiple modifications, a general rule of thumb is to check In 200 objects at a
time. If more control is required over the list of objects to be checked in, use the CheckIn()
method on a PISystem instance. For more information, see PISystem.Checkin Method
(https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
M_OSISOFT_AF_PISYSTEM_CHECKIN_2.htm).
• Add Overloads
When adding AFElement objects to an AFElements collection, several Add methods are
available. The methods can be roughly divided into two groups:

AF SDK 2.9 Getting Started Guide

35

Programming exercises
◦ Methods that accept a string denoting the new AFElement name and return a reference
to the newly created AFElement.
AFElement meters = database.Elements.Add("Meters");

◦ Methods that accept a reference to an existing AFElement object and return void
A common use case for the second method is when adding an existing element as a weak
reference to a parent element as in the following:
AFReferenceType weakRefType = database.ReferenceTypes["Weak Reference"];
AFElement meters = database.Elements["Meters"];
AFElement buildingA = database.Elements["Meter Company"].Elements["Building
A"];
buildingA.Elements.Add(meters.Elements["Meter001"], weakRefType);

Lesson 4 exercise solutions
• Exercise 1: Create an element named Feeders
static void CreateFeedersRootElement(AFDatabase database)
{
if (database.Elements.Contains("Feeders"))
return;

}

database.Elements.Add("Feeders");
database.CheckIn();

• Exercise 2: Create an element from template
static void CreateFeederElements(AFDatabase database)
{
AFElementTemplate template = database.ElementTemplates["FeederTemplate"];
AFElement feeders = database.Elements["Feeders"];
if (template == null || feeders == null) return;
if (feeders.Elements.Contains("Feeder001")) return;
AFElement feeder001 = feeders.Elements.Add("Feeder001", template);
AFAttribute city = feeder001.Attributes["City"];
if (city != null) city.SetValue(new AFValue("London"));
AFAttribute power = feeder001.Attributes["Power"];
power.ConfigString = @"%@\Configuration|PIDataArchiveName%\SINUSOID";

}

if (database.IsDirty)
database.CheckIn();

• Exercise 3: Create a weak reference
static void CreateWeakReference(AFDatabase database)
{
AFReferenceType weakRefType = database.ReferenceTypes["Weak Reference"];
AFElement london =
database.Elements["Geographical Locations"].Elements["London"];
AFElement feeder0001 = database.Elements["Feeders"].Elements["Feeder001"];

36

AF SDK 2.9 Getting Started Guide

Programming exercises
if (london == null || feeder0001 == null) return;
if (!london.Elements.Contains(feeder0001))
london.Elements.Add(feeder0001, weakRefType);
}

database.CheckIn();

Answers to bonus exercises
1. Create an AF Database named Ethical Power Company.
private static AFDatabase GetOrCreateDatabase(string servername, string
databasename)
{
PISystem assetServer = new PISystems()[servername];
if (assetServer == null)
return null;
AFDatabase database = assetServer.Databases[databasename];
if (database == null)
database = assetServer.Databases.Add(databasename);
return database;
}

2. Create the element categories and attribute categories.
private static void CreateCategories(AFDatabase database)
{
if (database == null) return;
var items = new List
{
"Measures Energy",
"Shows Status",
"Building Info",
"Location",
"Time - Series Data"
};
foreach (var item in items)
{
if (!database.ElementCategories.Contains(item))
database.ElementCategories.Add(item);
}
if (database.IsDirty)
database.CheckIn();
}

3. Create the enumeration sets and their values.
private static void CreateEnumerationSets(AFDatabase database)
{
if (database == null) return;
if (!database.EnumerationSets.Contains("Building Type"))
{
AFEnumerationSet bTypeEnum =
database.EnumerationSets.Add("Building Type");
bTypeEnum.Add("Residential", 0);
bTypeEnum.Add("Business", 1);
}
if (!database.EnumerationSets.Contains("Meter Status"))
{
AFEnumerationSet mStatusEnum =
database.EnumerationSets.Add("Meter Status");
mStatusEnum.Add("Good", 0);
mStatusEnum.Add("Bad", 1);
}

AF SDK 2.9 Getting Started Guide

37

Programming exercises

}

if (database.IsDirty)
database.CheckIn();

4. Create the PI AF Element Templates and their attribute templates. Set the properties and
categories based on the reference database.
In order to modularize the code and to limit each method to performing only one task, the
following solution was divided into four methods:
private static void CreateTemplates(AFDatabase database)
{
if (database == null) return;
AFElementTemplate meterBasicTemplate = CreateMeterBasicTemplate(database);
CreateMeterAdvancedTemplate(meterBasicTemplate);
CreateCityTemplate(database);

}

if (database.IsDirty)
database.CheckIn();

private static AFElementTemplate CreateMeterBasicTemplate(AFDatabase database)
{
AFElementTemplate meterBasicTemplate =
database.ElementTemplates["MeterBasic"];
if (meterBasicTemplate != null)
return meterBasicTemplate;
UOM uom = database.PISystem.UOMDatabase.UOMs["kilowatt hour"];
AFCategory mEnergyE = database.ElementCategories["Measures Energy"];
AFCategory bInfoA = database.AttributeCategories["Building Info"];
AFCategory locationA = database.AttributeCategories["Location"];
AFCategory tsDataA = database.AttributeCategories["Time-Series Data"];
AFEnumerationSet bTypeNum = database.EnumerationSets["Building Type"];
meterBasicTemplate = database.ElementTemplates.Add("MeterBasic");
meterBasicTemplate.Categories.Add(mEnergyE);
AFAttributeTemplate substationAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("Substation");
substationAttrTemp.Type = typeof(string);
AFAttributeTemplate usageLimitAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("Usage Limit");
usageLimitAttrTemp.Type = typeof(string);
usageLimitAttrTemp.DefaultUOM = uom;
AFAttributeTemplate buildingAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("Building");
buildingAttrTemp.Type = typeof(string);
buildingAttrTemp.Categories.Add(bInfoA);
AFAttributeTemplate bTypeAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("Building Type");
bTypeAttrTemp.TypeQualifier = bTypeNum;
bTypeAttrTemp.Categories.Add(bInfoA);
AFAttributeTemplate cityAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("City");
cityAttrTemp.Type = typeof(string);
cityAttrTemp.Categories.Add(locationA);
AFAttributeTemplate energyUsageAttrTemp =
meterBasicTemplate.AttributeTemplates.Add("Energy Usage");
energyUsageAttrTemp.Type = typeof(Single);

38

AF SDK 2.9 Getting Started Guide

Programming exercises
energyUsageAttrTemp.Categories.Add(tsDataA);
energyUsageAttrTemp.DefaultUOM = uom;
energyUsageAttrTemp.DataReferencePlugIn =
database.PISystem.DataReferencePlugIns["PI Point"];
energyUsageAttrTemp.ConfigString =
@"\\%@\Configuration|PIDataArchiveName%\%Element%.%Attribute%;UOM=kWh";
}

return meterBasicTemplate;

private static void CreateMeterAdvancedTemplate(AFElementTemplate
meterBasicTemplate)
{
AFDatabase database = meterBasicTemplate.Database;
AFElementTemplate meterAdvancedTemplate =
database.ElementTemplates["MeterAdvanced"];
if (meterAdvancedTemplate == null)
database.ElementTemplates.Add("MeterAdvanced");
AFCategory tsDataA =
database.AttributeCategories["Time-Series Data"];
AFEnumerationSet mStatusEnum =
database.EnumerationSets["Meter Status"];
meterAdvancedTemplate.BaseTemplate = meterBasicTemplate;

}

AFAttributeTemplate statusAttrTemp =
meterAdvancedTemplate.AttributeTemplates["Status"];
if (statusAttrTemp == null)
meterAdvancedTemplate.AttributeTemplates.Add("Status");
statusAttrTemp.TypeQualifier = mStatusEnum;
if (!statusAttrTemp.Categories.Contains(tsDataA))
statusAttrTemp.Categories.Add(tsDataA);
statusAttrTemp.DataReferencePlugIn =
database.PISystem.DataReferencePlugIns["PI Point"];
statusAttrTemp.ConfigString =
@"\\%@\Configuration|PIDataArchiveName%\%Element%.%Attribute%";

private static void CreateCityTemplate(AFDatabase database)
{
AFElementTemplate cityTemplate = database.ElementTemplates["City"];
if (cityTemplate != null)
return;
AFAttributeTemplate cityEnergyUsageAttrTemp =
cityTemplate.AttributeTemplates.Add("Energy Usage");
cityEnergyUsageAttrTemp.Type = typeof(Single);
cityEnergyUsageAttrTemp.DefaultUOM =
database.PISystem.UOMDatabase.UOMs["kilowatt hour"];
cityEnergyUsageAttrTemp.DataReferencePlugIn =
database.PISystem.DataReferencePlugIns["PI Point"];
cityEnergyUsageAttrTemp.ConfigString =
@"\\%@\Configuration|PIDataArchiveName%\%Element%.%Attribute%";
}

5. Create a root element called "Meters". Create individual meter elements from the
appropriate templates. Meter001-008 use MeterBasic. The rest use MeterAdvanced.
private static void CreateElements(AFDatabase database)
{
if (database == null) return;
// here we create the configuration element
// we do a small exception creating an attribute in this method.
AFElement configuration;

AF SDK 2.9 Getting Started Guide

39

Programming exercises
if (!database.Elements.Contains("Configuration"))
{
configuration = database.Elements.Add("Configuration");
AFAttribute name= configuration.Attributes.Add("PIDataArchiveName");
name.SetValue(new AFValue("PISRV01"));
}
AFElement meters = database.Elements["Meters"];
if (meters == null)
meters = database.Elements.Add("Meters");
AFElementTemplate basic = database.ElementTemplates["MeterBasic"];
AFElementTemplate advanced = database.ElementTemplates["MeterAdvanced"];
foreach (int i in Enumerable.Range(1, 12))
{
string name = "Meter" + i.ToString("D3");
if (!meters.Elements.Contains(name))
{
AFElementTemplate eTemp = i <= 8 ? basic : advanced;
AFElement e = meters.Elements.Add(name, eTemp);
}
}

}

if (database.IsDirty)
database.CheckIn();

6. Set the attribute values of the meter as appropriate based on the reference database. Only
do this for the first meter.
private static void SetAttributeValues(AFDatabase database)
{
if (database == null) return;
AFElement meter001 = database.Elements["Meters"].Elements["Meter001"];
meter001.Attributes["Substation"].SetValue(new AFValue("SSA-01"));
meter001.Attributes["Usage Limit"].SetValue(new AFValue(350));
meter001.Attributes["Building"].SetValue(new AFValue("The Shard"));

}

AFEnumerationValue bTypeValue =
database.EnumerationSets["Building Type"]["Residential"];
meter001.Attributes["Building Type"].SetValue(new AFValue(bTypeValue));
meter001.Attributes["City"].SetValue(new AFValue("London"));

7. Create the root element called Geographical Locations. Then, create three child
elements called London, Montreal, and San Francisco, based on the District template.
private static void CreateCityElements(AFDatabase database)
{
if (database == null) return;
if (!database.Elements.Contains("Geographical Locations"))
{
AFElement geoLocations = database.Elements.Add("Geographical Locations");
AFElementTemplate cityTemplate = database.ElementTemplates["City"];
geoLocations.Elements.Add("London", cityTemplate);
geoLocations.Elements.Add("Montreal", cityTemplate);
geoLocations.Elements.Add("San Francisco", cityTemplate);
}
if (database.IsDirty)

40

AF SDK 2.9 Getting Started Guide

Programming exercises

}

database.CheckIn();

8. Add meter elements as weak references under the District elements, following the reference
database.
private static void CreateWeakReferences(AFDatabase database)
{
if (database == null) return;
AFReferenceType weakRefType = database.ReferenceTypes["Weak Reference"];
AFElement meters = database.Elements["Meters"];
AFElement locations = database.Elements["Geographical Locations"];
AFElementTemplate cityTemplate = database.ElementTemplates["City"];
foreach (AFElement meter in meters.Elements)
{
string cityName = meter.Attributes["City"].GetValue().ToString();
if (string.IsNullOrEmpty(cityName))
continue;
AFElement city = locations.Elements[cityName];
if (city == null)
locations.Elements.Add(cityName, cityTemplate);
if (!city.Elements.Contains(meter.Name))
city.Elements.Add(meter, weakRefType);
}

}

if (database.IsDirty)
database.CheckIn();

AF SDK 2.9 Getting Started Guide

41

Programming exercises

Lesson 5: Working with AF event frames
After completing this lesson you will be able to:
• Use AF SDK to manage event frames
• Create an event frame template
• Create and work with event frames

Help for methods and data types used in this lesson
• AFEventFrameSearch.FindEventFrames() Method (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/
M_OSIsoft_AF_Search_AFEventFrameSearch_FindEventFrames.htm)
• AFEventFrame.CaptureValues() Method (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/
M_OSIsoft_AF_EventFrame_AFEventFrame_CaptureValues.htm)
• AFEveventFrame Class (https://techsupport.osisoft.com/Documentation/PI-AF-SDK/html/
T_OSIsoft_AF_EventFrame_AFEventFrame.htm)

Introduction
An Event Frame represents a time period defined by a start time and end time. Each Event
Frame typically references one or more PI AF elements to associate the event with an asset. An
Event Frame can also have a collection of child Event Frames, which represent child events that
make up the larger event.
When creating AFEventFrame objects, the following properties should usually be set:
StartTime, EndTime, and PrimaryReferencedElement.
AFEventFrame ef = new AFEventFrame(database, "*", eventFrameTemplate);
ef.SetStartTime(startTime);
ef.SetEndTime(endTime);
ef.PrimaryReferencedElement = meter;

When creating Event Frame templates in code, you might be surprised to discover there is no
such object as AFEventFrameTemplate. Instead, an Event Frame template is a simply an
AFElementTemplate with its InstanceType property set to typeof(AFEventFrame).
AFElementTemplate eventFrameTemplate =
database.ElementTemplates["Name of Template"];
if (eventFrameTemplate == null)
AFElementTemplate eventFrameTemplate =
database.ElementTemplates.Add("Name of Template");
eventFrameTemplate.InstanceType = typeof(AFEventFrame);

To find existing PI AF Event Frames, use the AFEventFrameSearch.FindEventFrames()
method. You can search by name, start time, end time, duration, template, category, and a
variety of other fields. Note that unlike elements, event frames cannot be accessed directly as a
property under the AFDatabase object. For more information, see the online AF SDK reference
AFEventFrameSearch.FindEventFrames() (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/
Overload_OSIsoft_AF_EventFrame_AFEventFrame_FindEventFrames.htm).
Note that AFEventFrameSearch is analogous to AFElementSearch from Lesson 2.

42

AF SDK 2.9 Getting Started Guide

Programming exercises
AFEventFrameSearch eventFrameSearch =
new AFEventFrameSearch(database, "EventFrame Captures",
AFEventFrameSearchMode.ForwardFromStartTime,
startTime, queryString);

Because Event Frames typically represent historical time ranges, attribute values of event
frames typically do not change over time and can be cached to improve performance. In AF
SDK, you can call the CaptureValues() method on an event frame instance to capture and
save attribute values to the PI AF server for faster subsequent retrieval. Updating the start or
end time of the event frame will automatically recapture the values. For more information, see
the online AF SDK reference CaptureValues() (https://techsupport.osisoft.com/
Documentation/PI-AF-SDK/html/
M_OSIsoft_AF_EventFrame_AFEventFrame_CaptureValues.htm).
foreach (AFEventFrame item in eventFrameSearch.FindEventFrams())
{
item.CaptureValues();
}

Lesson 5 exercises
Your next task is to develop an energy reporting system for the PI AF database Green Power
Company. You should familiarize yourself with the PI AF database using PI System Explorer
before starting the exercise.
The Green Power Company database contains 12 electric meters deployed to different
buildings. Your task is to create PI AF Event Frames to track average energy usage per day. Each
PI AF Event Frame is created using a PI AF Event Frame Template. The template should have
one attribute configured to report the average of the Energy Usage attribute of a meter over the
event frame time range.
Please implement the following methods inside Program5.cs and run the application with the
arguments provided for each of the following methods in Main().
For each of the following exercises, your task is to add the logic to each method. Add the
methods to your console application and test each one by writing output to the console.
• Exercise 1
Create an AF Event Frame template called Daily Usage. Event frame names created from
this template should include the event frame template name, referenced element name, and
start date. For example: Usage Tracker-Meter002-2016-02-01 00:00:00.
Create a PI AF Attribute Template under the event frame template called Average Energy
Usage. Configure the AFAttributeTemplate properties such that the event frame
attribute reports the average value of the referenced Energy Usage attribute over the event
frame time range. See Lesson 5 hints and tips for an example configuration string to use.
AFElementTemplate CreateEventFrameTemplate(AFDatabase database)

• Exercise 2
For each meter and for each day within February 1 - 5, 2016, create an AFEventFrame based
off of the "Daily Usage" template. Set the PrimaryReferencedElement to the appropriate
meter.

AF SDK 2.9 Getting Started Guide

43

Programming exercises
void CreateEventFrames(AFDatabase database, AFElementTemplate
eventFrameTemplate)

• Exercise 3
Save the value of Average Energy Usage attribute to the PI AF server for each event frame
using CaptureValues() on the event frame instance. You will first need to find all event
frames using the AFEventFrameSearch.FindEventFrames() method.
void CaptureValues(AFDatabase database, AFElementTemplate eventFrameTemplate)

• Exercise 4
Find all of the event frames referencing Meter003. Use the
AFEventFrameSearch.FindEventFrames() method. Then print to the console the event
frame name, primary referenced element name, and value of the Average Energy Usage
attribute.
void PrintReport(AFDatabase database, AFElementTemplate eventFrameTemplate)

When the method is run, it should product the following output:
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily
Daily

Usage-Meter003-2017-06-29-EF, Meter003, 306.564053292511
Usage-Meter003-2017-06-29-EF1, Meter003, 306.564053292511
Usage-Meter003-2017-06-30-EF, Meter003, 296.339212218409
Usage-Meter003-2017-06-30-EF1, Meter003, 296.339212218409
Usage-Meter003-2017-07-01-EF, Meter003, 297.125675030912
Usage-Meter003-2017-07-01-EF1, Meter003, 297.125675030912
Usage-Meter003-2017-07-02-EF, Meter003, 303.961577200124
Usage-Meter003-2017-07-02-EF1, Meter003, 303.961577200124
Usage-Meter003-2017-07-03-EF, Meter003, 296.181190443142
Usage-Meter003-2017-07-03-EF1, Meter003, 296.181190443142
Usage-Meter003-2017-07-04-EF, Meter003, 296.944001749179
Usage-Meter003-2017-07-04-EF1, Meter003, 296.944001749179
Usage-Meter003-2017-07-05-EF, Meter003, 294.295850664074
Usage-Meter003-2017-07-05-EF1, Meter003, 294.295850664074

Lesson 5 hints and tips
CaptureValues() is used only on closed event frames; that is, an Event Frame with an
EndTime that occurs before AFTime.MaxValue. Event frames that are not closed have values
that can still change. If the CaptureValues() method is called and the EndTime has not been
set, then an exception is thrown.
Use the template NamingPattern property to define the event frame names. To obtain an
understanding of how the template works, try setting it in PI System Explorer first before using
it in AF SDK. For example, try the template shown here:
TEMPLATE%-%ELEMENT%-%STARTTIME:yyyy-MM-dd%-EF*

To configure the event frame attribute to report an average, set the data reference to a PI Point
DR. Use the following ConfigString:
.\Elements[.]|Energy Usage;TimeRangeMethod=Average

The ConfigString shown above configures the Average Energy Usage attribute to report the
average of the primary referenced element's Energy Usage over the event frame's time range.

44

AF SDK 2.9 Getting Started Guide

Programming exercises

Lesson 5 exercise solutions
• Exercise 1: Create Event Frame Template
static AFElementTemplate CreateEventFrameTemplate(AFDatabase database)
{
AFElementTemplate eventFrameTemplate =
database.ElementTemplates["Daily Usage"];
if (eventFrameTemplate != null)
return eventFrameTemplate;
eventFrameTemplate = database.ElementTemplates.Add("Daily Usage");
eventFrameTemplate.InstanceType = typeof(AFEventFrame);
eventFrameTemplate.NamingPattern =
@"%TEMPLATE%-%ELEMENT%-%STARTTIME:yyyy-MM-dd%-EF*";
AFAttributeTemplate usage =
eventFrameTemplate.AttributeTemplates.Add("Average Energy Usage");
usage.Type = typeof(Single);
usage.DataReferencePlugIn = AFDataReference.GetPIPointDataReference();
usage.ConfigString = @".\Elements[.]|Energy Usage;TimeRangeMethod=Average";
usage.DefaultUOM = database.PISystem.UOMDatabase.UOMs["kilowatt hour"];
if (database.IsDirty)
database.CheckIn();
}

return eventFrameTemplate;

• Exercise 2: Create Event Frames
static void CreateEventFrames(AFDatabase database, AFElementTemplate
eventFrameTemplate)
{
string queryString = "Template:MeterBasic";
{
// This method returns the collection of AFBaseElement objects that
// were created with this template.
using (AFElementSearch elementQuery =
new AFElementSearch(database, "Meters", queryString))
{
DateTime timeReference = DateTime.Today.AddDays(-7);
int count = 0;
foreach (AFElement meter in elementQuery.FindElements())
{
foreach (int day in Enumerable.Range(1, 7))
{
AFTime startTime = new AFTime(timeReference.AddDays(day - 1));
AFTime endTime = new AFTime(startTime.LocalTime.AddDays(1));
AFEventFrame ef = new AFEventFrame(database, "*",
eventFrameTemplate);
ef.SetStartTime(startTime);
ef.SetEndTime(endTime);
ef.PrimaryReferencedElement = meter;
// It is good practice to periodically
// check in the database
if (++count % 500 == 0)
database.CheckIn();
}
}
}
}
if (database.IsDirty)
AF SDK 2.9 Getting Started Guide

45

Programming exercises
}

database.CheckIn();

• Exercise 3: Capture Values
static public void CaptureValues(AFDatabase database, AFElementTemplate
eventFrameTemplate)
{
// Formulate search constraints on time and template
AFTime startTime = DateTime.Today.AddDays(-7);
string queryString = $"template:\"{eventFrameTemplate.Name}\"";
using (AFEventFrameSearch eventFrameSearch =
new AFEventFrameSearch(database, "EventFrame Captures",
AFEventFrameSearchMode.ForwardFromStartTime, startTime, queryString))
{
eventFrameSearch.CacheTimeout = TimeSpan.FromMinutes(5);
int count = 0;
foreach (AFEventFrame item in eventFrameSearch.FindEventFrames())
{
item.CaptureValues();
if ((count++ % 500) == 0)
database.CheckIn();
}

}

}

if (database.IsDirty)
database.CheckIn();

• Exercise 4: Print Report
static void PrintReport(AFDatabase database, AFElementTemplate
eventFrameTemplate)
{
AFTime startTime = DateTime.Today.AddDays(-7);
AFTime endTime =
startTime.LocalTime.AddDays(+8); // Or DateTime.Today.AddDays(1);
string queryString =
$"template:'{eventFrameTemplate.Name}' ElementName:Meter003";
using (AFEventFrameSearch eventFrameSearch =
new AFEventFrameSearch(database, "EventFrame Captures",
AFSearchMode.StartInclusive, startTime, endTime, queryString))
{
eventFrameSearch.CacheTimeout = TimeSpan.FromMinutes(5);
foreach (AFEventFrame ef in eventFrameSearch.FindEventFrames())
{
Console.WriteLine("{0}, {1}, {2}",
ef.Name,
ef.PrimaryReferencedElement.Name,
ef.Attributes["Average Energy Usage"].GetValue().Value);
}
}
}

46

AF SDK 2.9 Getting Started Guide

Conclusion
Congratulations! By now you should have a good, basic understanding of how to use AF SDK.
Before you go, please be sure to visit the PI Developers Club space within PI Square: PI Square
(https://pisquare.osisoft.com). You can find much more information about the PI system,
software development, and practical knowledge from experts.
If you are viewing this guide online, be sure to notice and use the link at the bottom of each
page, which asks: Was this information helpful? OSIsoft values your input and will make every
effort to improve and enhance this information based on your feedback.

AF SDK 2.9 Getting Started Guide

47

Conclusion

48

AF SDK 2.9 Getting Started Guide

Technical support and other resources
For technical assistance, contact OSIsoft Technical Support at +1 510-297-5828 or through the
OSIsoft Tech Support Contact Us page (https://techsupport.osisoft.com/Contact-Us/). The
website offers additional contact options for customers outside of the United States.
When you contact OSIsoft Technical Support, be prepared to provide this information:
• Product name, version, and build numbers
• Details about your computer platform (CPU type, operating system, and version number)
• Time that the difficulty started
• Log files at that time
• Details of any environment changes prior to the start of the issue
• Summary of the issue, including any relevant log files during the time the issue occurred
To ask questions of others who use OSIsoft software, join the OSIsoft user community,
PI Square (https://pisquare.osisoft.com). Members of the community can request advice and
share ideas about the PI System. The PI Developers Club space within PI Square offers
resources to help you with the programming and integration of OSIsoft products.

AF SDK 2.9 Getting Started Guide

49

Technical support and other resources

50

AF SDK 2.9 Getting Started Guide



Source Exif Data:
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.6
Linearized                      : Yes
Encryption                      : Standard V4.4 (128-bit)
User Access                     : Print, Copy, Annotate, Fill forms, Extract, Print high-res
Author                          : OSIsoft, LLC.
Create Date                     : 2017:08:03 15:29:14-08:00
Modify Date                     : 2018:07:11 17:07:47-07:00
Subject                         : AF SDK 2.9
XMP Toolkit                     : Adobe XMP Core 5.6-c015 84.159810, 2016/09/10-02:41:30
Format                          : application/pdf
Creator                         : OSIsoft, LLC.
Description                     : AF SDK 2.9
Title                           :  AF SDK 2.9 Getting Started Guide
Creator Tool                    : AH XSL Formatter V6.4 R1 for Windows (x64) : 6.4.2.26942 (2016/12/07 15:30JST)
Metadata Date                   : 2018:07:11 17:07:47-07:00
Producer                        : Antenna House PDF Output Library 6.4.928 (Windows (x64))
Trapped                         : False
Document ID                     : uuid:9942bb1a-756b-45e0-ae86-9ba4f57b14f2
Instance ID                     : uuid:70ebd2ee-1872-4137-950f-3db17218589e
Page Layout                     : OneColumn
Page Mode                       : UseOutlines
Page Count                      : 54
EXIF Metadata provided by EXIF.tools

Navigation menu