Marcos Vainer Loeff

Using PI AF SDK with ASP.NET Signal R

Blog Post created by Marcos Vainer Loeff Employee on Oct 1, 2015

Introduction

 

On this blog post, I will show you how to create an ASP.NET Web Application that communicates with the PI System using PI AF SDK and the Microsoft ASP.NET library for real time data ASP.NET Signal R. Let's first understand what ASP.NET Signal R is.

 

What is ASP.NET Signal R?

 

In order to improve the user experience, real-time data functionality is becoming a key feature to achieve this goal. RESTful web services are not appropriate for real time data as clients might need to request data from the server.

 

ASP.NET SignalR is a library for ASP.NET developers to add real-time web functionality to their applications. Using this library, the web server is able to push content to the connected clients as it happens.

 

Signal R takes advantage of four transports options. It will automatically select the best available transport given the client's browser. A good example is WebSocket, which is an HTML5 API that enables bi-directional communication between the browser and server. Signal R will use WebSockets on new browsers. In case the browser does not support this feature, it will fall back to other techniques. Everything is done under the hood so the developer does not need to add any line of code.

 

Developing a sample application

 

Imagine the power of creating an application using ASP.NET Signal R using the AF DataPipe from PI AF SDK. The great benefit of this integration would be that the server will be able to provide a value to the client as fast as PI AF SDK detects a new value.  This is what we will develop on this blog post.

 

You can download the source code package with the Visual Studio Solution by clicking here.

 

First, open Visual Studio 2013 and create a new ASP.NET Web Application project.

 

 

 

Select the Empty template but make sure to add folders and core references for MVC and Web API.

 

 

Some NuGet libraries needs to be added. Please use the following commands on the Package Console Manager:

 

Install-package Microsoft.AspNet.SignalR
Install-package bootstrap

 

It is always good to work with the most updated libraries. Please run the command below in order to update all of your packages.

 

Update-package

 

Add the Home controller (HomeController.cs) to the Controller folder with the following content:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;


namespace UsingPIAFSDKandSignalR.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }
    }
}

 

Create a new root folder called Hubs and add a new class PIHub.cs with the following code snippet:

 

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;


namespace UsingPIAFSDKandSignalR.Hubs
{
    [HubName("piHub")]
    public class PIHub : Hub
    {
        public void BroadcastMessage(string text)
        {
            Clients.All.displayText(text);
        }
    }
}

 

Under the Views\Home folder, create a new View called Index.cshtml with the following content:

 

@{
    Layout = null;
}


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Using PI AF SDK with ASP.NET Signar R Demo</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div class="container body-content">
        <div>
            <input type="text" id="msg" />
            <input type="button" id="broadcast" value="Broadcast message" />        
            <ul id="messages"></ul>
        </div>
    </div>


    <script src="~/Scripts/jquery-2.1.4.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
    <script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script>
    <script src="/signalr/hubs" type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            var broadcaster = $.connection.piHub;
            broadcaster.client.displayText = function (text) {
                $('#messages').append('<li>' + text + '</li>');
            };
            $.connection.hub.start().done(function () {
                $("#broadcast").click(function () {
                    broadcaster.server.broadcastMessage($('#msg').val());
                });              
            });
        });
    </script>
</body>
</html>

 

 

Finally, create a new class on the root folder called Startup.cs, which will start the ASP.NET Signal R libraries which include the hubs.

 

using Microsoft.AspNet.SignalR;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;


namespace UsingPIAFSDKandSignalR
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HubConfiguration configuration = new HubConfiguration();
            configuration.EnableDetailedErrors = true;
            app.MapHubs(configuration);
        }
    }
}

 

Although we have not integrated this project with PI AF SDK yet, we can start this application and test it. Please open two tabs of the main page. If you type any message on the web page and press "Broadcast message", this message will appear on all clients as it is shown on the following screenshot. The reason that the message "test" does not appear on the second tab is because it was sent before the second tab was opened.

 

 

 

 

Adding PI AF SDK to the Sample Application

 

Let's continue to develop our sample application. Right-click on the project and select "Add references...". Add the library "OSIsoft. AF SDK" using the Reference Manager.

 

 

Let's add two public methods on the PIHub.cs class.

 

        public Task Join(string groupName)
        {
            Clients.Caller.displayText("Adding connectionId " + Context.ConnectionId + " to the group " + groupName.ToLower());
            return Groups.Add(Context.ConnectionId, groupName.ToLower());
        }
        public Task Leave(string groupName)
        {
            Clients.Caller.displayText("Adding connectionId " + Context.ConnectionId + " to the group " + groupName.ToLower());
            return Groups.Remove(Context.ConnectionId, groupName.ToLower());
        }

 

 

Adding the WebBackgrounder package

 

According to the WebBackgrounder GitHub respository web site, WebBackgrounder is a proof-of-concept of a web-farm friendly background task manager meant to just work with a vanilla ASP.NET web application.

 

We need to create a background thread running AFDataPIpe in order to receive live events from the PISystem. Nevertheless, if ASP.NET does not detect and manage this thread properly, it could end up tearing down the app doin in the middle of the work, leaving AFDataPipe in a potentially invalid state.

 

One way to solve this issue is to notify ASP.NET that work is in progress through the WebBackgrounder.

 

We are also going to use the WebActivator, which is a NuGet package that allows other packages to easily bring in Startup and Shutdown code into a web application. This gives a much cleaner solution than having to modify global.asax with the startup logic from many packages.

 

Please  use the following command on Package Manager Console to add the package to the project:

 

Install-package WebBackgrounder
Install-package WebActivatorEx

 

Writing the EventPipesJob

 

In order to get data from the PI System using AFDataPipe, we will write the EventPipesJob.cs class which derives from Job class. EventPipesJob also implements the IDisposable interface. When the method Execute() is called, it returns a task that subscribes for updates in two attributes which points to the "sinusoid" and "cdt158" tags. This background task keeps receiving values while it is running. Once a value is received, the PIObserver.OnNext method is called as it is using the observer pattern. When the Dispose() method is called, it closes the connection opened by the AFEventPipe and finish the task by using a cancellationToken.

 

using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using WebBackgrounder;


namespace UsingPIAFSDKandSignalR
{
    public class EventPipesJob : Job, IDisposable
    {
        private CancellationTokenSource cancellationSource = null;
        private AFDataPipe myDataPipe = null;
        public EventPipesJob(TimeSpan interval)
            : base("Event Pipes Job", interval)
        {
            cancellationSource = new CancellationTokenSource();
            myDataPipe = new AFDataPipe();
            AFElement myElement = AFObject.FindObject(@"\\MARC-PI2014\TestDb\Cities") as AFElement;


            myDataPipe.EventHorizonMode = AFEventHorizonMode.TimeOffset;
            myDataPipe.EventHorizonOffset = new TimeSpan(0, 5, 0);
            myDataPipe.AddSignups(myElement.Attributes);


            IObserver<AFDataPipeEvent> observer = new PIObserver();
            myDataPipe.Subscribe(observer);
        }


        public override Task Execute()
        {     
            // create a cancellation source to terminate the update thread when the user is done  
            return Task.Run(() =>
            {
                // keep polling while the user hasn't requested cancellation  
                while (!cancellationSource.IsCancellationRequested)
                {
                    // Get updates from pipe and process them  
                    AFErrors<AFAttribute> myErrors = myDataPipe.GetObserverEvents();


                    // wait for 1 second using the handle provided by the cancellation source  
                    cancellationSource.Token.WaitHandle.WaitOne(1000);
                }


            }, cancellationSource.Token);
        }


        public void Dispose()
        {
            myDataPipe.Dispose();
            cancellationSource.Cancel(); // when this is called the update loop will terminate 
        }
    }
}

 

 

PIObserver implements the observer pattern. When a value is received, the OnNext method is called. On this method, we need to get the SignalR context in order to send values to the selected clients using the clients.displayText method.

 

using Microsoft.AspNet.SignalR;
using OSIsoft.AF.Data;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using UsingPIAFSDKandSignalR.Hubs;


namespace UsingPIAFSDKandSignalR
{
    public class PIObserver : IObserver<AFDataPipeEvent>
    {
        public void OnCompleted()
        {
            Debug.WriteLine("Completed");
        }


        public void OnError(Exception error)
        {
            Debug.WriteLine("Error");
        }


        public void OnNext(AFDataPipeEvent value)
        {
            var context = GlobalHost.ConnectionManager.GetHubContext<PIHub>();
            var clients = context.Clients.Group(value.Value.Attribute.Name.ToLower());
            clients.displayText(value.Value.Attribute.Name.ToLower() + " - " + value.Value.ToString());
            Debug.WriteLine("\n{0} NEW VALUE from PI Point: {1}\n => Value: {2} and Timestamp is {3}.", DateTime.Now.ToString(), value.Value.PIPoint.Name, value.Value.Value, value.Value.Timestamp.LocalTime);
        }
    }
}

 

Setting up WebBackgrounder

 

Create the WebBackgrounderSetup.cs file under the App_Start folder with the following content:

 

using System;
using WebBackgrounder;




[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(UsingPIAFSDKandSignalR.App_Start.WebBackgrounderSetup), "Start")]
[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(UsingPIAFSDKandSignalR.App_Start.WebBackgrounderSetup), "Shutdown")]




namespace UsingPIAFSDKandSignalR.App_Start
{
    public static class WebBackgrounderSetup
    {
        private static bool started = false;
        static readonly JobManager _jobManager = CreateJobWorkersManager();


        public static void Start()
        {
            _jobManager.Start();
            started = true;
        }


        public static void Shutdown()
        {
            if (started == true)
            {
                _jobManager.Dispose();
            }
        }


        private static JobManager CreateJobWorkersManager()
        {
            var jobs = new IJob[]
            {
                new EventPipesJob(TimeSpan.FromSeconds(5))
            };


            var coordinator = new SingleServerJobCoordinator();
            var manager = new JobManager(jobs, coordinator);
            return manager;
        }
    }
}

 

 

The method CreateJobWorkersManager() creates an array with only EventPipesJob. As the application runs only on a single server, the SingleServerJobCoordinator() can be used.

 

Writing the View

 

Finally, we need to add two buttons on the Index.cshtml view and its respective methods.

 

@{
    Layout = null;
}


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Using PI AF SDK with ASP.NET Signar R Demo</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div class="container body-content">
        <div>
            <input type="text" id="msg" />
            <input type="button" id="broadcast" value="Broadcast message" />
            <input type="button" id="joinsinu" value="Broadcast Sinusoid values" />
            <input type="button" id="joincdt" value="Bradcast Cdt158 values" />
            <ul id="messages"></ul>
        </div>
    </div>


    <script src="~/Scripts/jquery-2.1.4.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
    <script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script>
    <script src="/signalr/hubs" type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            var broadcaster = $.connection.piHub;
            broadcaster.client.displayText = function (text) {
                $('#messages').append('<li>' + text + '</li>');
            };
            $.connection.hub.start().done(function () {
                $("#broadcast").click(function () {
                    broadcaster.server.broadcastMessage($('#msg').val());
                });
                $("#joinsinu").click(function () {
                    broadcaster.server.join('sinusoid');
                });


                $("#joincdt").click(function () {
                    broadcaster.server.join('cdt158');
                });
            });
        });
    </script>
</body>
</html>

 

Note that for each button, the client will execute a method on the server-side. For instance, when the user clicks on the button that broadcasts sinusoid messages, the join server method will be called with the "sinusoid" as an input. This will add the user connection to the group that will be notified of the live updates from the sinusoid PI Point. A similar process happens with cdt158. You can add some breakpoints in order to understand better what is happening under the hood.

 

 

Our web app is ready! Let's start it and open two tabs again. After they are opened, click on "Broadcast Sinusoid Values" on the first tab and "Broadcast Cdt158 Values" on the second tab. We can see on the screenshot below that the first tab receives values from sinusoid and the second tab from cdt158 as expected. This proves that ASP.NET Signal R is able to select which groups should broadcast messages.

 

 

 

 

Conclusion

 

This sample application shows how to use PI AF SDK with ASP.NET Signal R adding the PI System real time functionality (AFDataPipe) to web applications. Nevertheless, as this integration is new, a lot of testing needs to be done before using this feature in production.

 

Upcoming releases of PI Web API will use a similar feature. Therefore, this will be another alternative to achieve similar goal.

 

I hope that you found this blog post interesting and I would like to thank for reading it!

Outcomes