Introduction

 

On one of my previous blog post Developing the Wikipedia Data Reference - Part 1, we have written the WikipediaWebService.cs class, which contains static methods to query wikipedia.

 

On this blog post, we will continue to work on this project in order to:

 

  • Create a custom data reference capable of querying wikipedia
  • Design an editor (Windows Form) in order to help the user set up the config string using PI System Explorer.
  • Automate the deployment process by setting up the pre-build and post-build events command line.
  • Test the data reference using PI System Explorer

 

Creating the Wikipedia Data Reference

 

We will continue working with the project WikipediaDR project available on part 1.  As there are a lot of examples about developing custom data references on PIDevClub, I will focus on the logic used in this project. You can download the source code package here.

 

First of all, let's define our goals:

 

  • The data reference should query wikipedia by providing an input string to the WikipediaWebServices.cs class.
  • The user should be able to choose if he wants to query the attribute element name, other attribute name from the same element, other attribute value or a custom text.
  • Due to performance issues, it is convenient to store the wikipedia description on memory so that the data reference does not need to make HTTP requests to Wikipedia whenever this data reference is accessed.
  • Wikipedia pages are dynamic. If you open the same page after one year, there is a high probability that the text would be changed. The data reference should query again wikipedia if the last query execution time was more than 6 months ago. The ConfigString will store the last query execution time in order to implement this logic.

 

The code snippet from the Wikipedia.cs is below:

 

using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using OSIsoft.AF.Asset;
using OSIsoft.AF.UnitsOfMeasure;
using OSIsoft.AF.Time;




namespace OSIsoft.AF.Asset.DataReference.WikipediaDR
{
    [Serializable]
    [Guid("B1D3CF85-D3CF-4033-A058-7554BE3EEEFF")]
    [Description("Wikipedia; Get description of a word from the Wikipedia.")]
    public class Wikipedia : AFDataReference
    {
        private Tuple<string, string, AFTime> _lastQueriedValue;


        private string _configString;


        public enum SourceOption
        {
            ElementName, AttributeName, AttributeValue, Custom
        }


        public Wikipedia()
            : base()
        {
        }


        public override Type EditorType
        {
            get { return typeof(ConfigStringEditor); }
        }


        public override AFDataReferenceContext SupportedContexts
        {
            get
            {
                AFDataReferenceContext supportedContexts = AFDataReferenceContext.All;
                return supportedContexts;
            }
        }


        public override AFDataReferenceMethod SupportedMethods
        {
            get
            {
                AFDataReferenceMethod supportedMethods = AFDataReferenceMethod.GetValue;
                return supportedMethods;
            }
        }


        public override AFValue GetValue(object context, object timeContext, AFAttributeList inputAttributes, AFValues inputValues)
        {
            if (string.IsNullOrEmpty(ConfigString))
                throw new Exception("ConfigString cannot be null or empty");


            if (Attribute == null || (Attribute.Type != typeof(string) && Attribute.Type != typeof(Object)))
                throw new Exception("The attribute type must be a String");


            AFTime timeStamp;
            if (timeContext is AFTime)
                timeStamp = (AFTime)timeContext;
            else if (timeContext is AFTimeRange)
                timeStamp = ((AFTimeRange)timeContext).EndTime;
            else
                timeStamp = AFTime.Now;


            var lastQueriedValue = _lastQueriedValue; // save queryString, result, query timestamp
            string wikipediaQuery = GetQueryString(context, timeContext);
            if (lastQueriedValue != null && lastQueriedValue.Item1 == wikipediaQuery && (AFTime.Now - lastQueriedValue.Item3).TotalDays < 180)
                return new AFValue(lastQueriedValue.Item2, timeStamp);


            string queryResult = WikipediaWebService.GetWikipediaDescription(wikipediaQuery).Trim().Replace(";", ",");
            if (queryResult.Contains("Error: "))
                return new AFValue(queryResult, timeStamp, null, AFValueStatus.Bad);


            lastQueriedValue = Tuple.Create(wikipediaQuery, queryResult, AFTime.Now); // keep track of timestamp when source was queried
            _lastQueriedValue = lastQueriedValue; // cache the last result


            return new AFValue(queryResult, timeStamp);
        }


        private string GetQueryString(object context, object timeContext)
        {
            string substitutedConfig = SubstituteParameters(_configString, this, context, timeContext).Trim();
            if (substitutedConfig.StartsWith("%@") && substitutedConfig.EndsWith("%"))
            {
                string attributeName = substitutedConfig.Substring(2, substitutedConfig.Length - 3);
                var referencedAttribute = base.GetAttribute(attributeName);
                if (!ReferenceEquals(referencedAttribute, null))
                {
                    var value = referencedAttribute.GetValue(context, timeContext, null);
                    if (value != null && value.Value != null)
                    {
                        return value.Value.ToString();
                    }
                }
            }


            return substitutedConfig;
        }


        private bool CheckIfAttributeExists(string attributeName)
        {
            if (this.Attribute.Element.Attributes[attributeName] == null)
                return false;
            else
                return true;
        }




        public void SaveConfigString()
        {
            base.SaveConfigChanges();
        }


        public override string ConfigString
        {
            get
            {
                return _configString;
            }


            set
            {
                if (ConfigString != value)
                {
                    _configString = value;
                    SaveConfigChanges();
                }
            }
        }
    }
}

 

Note that the AFDataReference.SubstituteParameters method is used which means that if the config string is %Element%, this method will convert it to the element name. If the config string is %@AttributeName%, it will search on wikipedia for the value of the attribute whose name is AttributeName.

Designing the Editor

 

An editor helps a lot when the user wants to set up the ConfigString using PI System Explorer. We've created the ConfigStringEditor.cs Windows Form class that looks like the window below:

 

 

 

Right click on the ConfigStringEditor.cs and click on "View Code...." and add the following code snippet:

 

using OSIsoft.AF.Asset.DataReference;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace OSIsoft.AF.Asset.DataReference.WikipediaDR
{
    public partial class ConfigStringEditor : Form
    {
        private Wikipedia dataReference = null;
        private List<string> attributeNames = new List<string>();
        public ConfigStringEditor(Wikipedia dataReference, bool bReadOnly)
            : base()
        {
            InitializeComponent();
            this.dataReference = dataReference;
            cbDrOptions.SelectedItem = "Element name";




            cbAttributes.Items.Clear();
            if (dataReference.Attribute != null)
            {
                attributeNames = GetAttributeNames((AFElement)this.dataReference.Attribute.Element);
                cbAttributes.Items.AddRange(attributeNames.ToArray());
                if (attributeNames.Count > 0)
                {
                    cbAttributes.SelectedItem = attributeNames[0];
                }
            }
            cbDrOptions_SelectedIndexChanged(null, null);
            LoadValuesFromConfigString();
        }


        private List<string> GetAttributeNames(AFElement parentElement)
        {
            List<string> attributeNames = new List<string>();


            foreach (AFAttribute attribute in parentElement.Attributes)
            {
                if (attribute.Name != this.dataReference.Attribute.Name)
                {
                    attributeNames.Add(attribute.Name);
                }
            }
            return attributeNames;


        }


        private void cbDrOptions_SelectedIndexChanged(object sender, EventArgs e)
        {


            if (cbDrOptions.SelectedItem.ToString() == "Element name")
            {
                lbCustom.Visible = false;
                lbCustom.Text = "";
                tbCustom.Visible = false;
                cbAttributes.Visible = false;
            }
            else if (cbDrOptions.SelectedItem.ToString() == "Attribute name")
            {
                lbCustom.Visible = true;
                lbCustom.Text = "Select attribute name option:";
                tbCustom.Visible = false;
                cbAttributes.Visible = true;
                if (attributeNames.Count > 0)
                {
                    cbAttributes.SelectedItem = attributeNames[0];
                }


            }
            else if (cbDrOptions.SelectedItem.ToString() == "Attribute value")
            {
                lbCustom.Visible = true;
                lbCustom.Text = "Select attribute name value:";
                tbCustom.Visible = false;
                cbAttributes.Visible = true;
                if (attributeNames.Count > 0)
                {
                    cbAttributes.SelectedItem = attributeNames[0];
                }
            }
            else if (cbDrOptions.SelectedItem.ToString() == "Custom")
            {
                lbCustom.Visible = true;
                lbCustom.Text = "Write custom words:";
                tbCustom.Visible = true;
                cbAttributes.Visible = false;


            }




        }




        private void btnOK_Click(object sender, EventArgs e)
        {
            try
            {
                if (cbDrOptions.SelectedItem.ToString() == "Element name")
                {
                    dataReference.ConfigString = "%Element%";
                }
                else if (cbDrOptions.SelectedItem.ToString() == "Attribute name")
                {
                    dataReference.ConfigString = cbAttributes.SelectedItem.ToString();
                }
                else if (cbDrOptions.SelectedItem.ToString() == "Attribute value")
                {
                    dataReference.ConfigString = "%@" + cbAttributes.SelectedItem.ToString() + "%";
                }
                else if (cbDrOptions.SelectedItem.ToString() == "Custom")
                {
                    dataReference.ConfigString = tbCustom.Text;
                    cbAttributes.SelectedItem = "";
                }
                else
                {
                    throw new ArgumentException();
                }
                dataReference.SaveConfigString();
                this.Close();
            }
            catch (Exception ex)
            {
                DialogResult = DialogResult.None;
                MessageBox.Show("There was an error saving the ConfigString values. Error: " + ex.Message);
            }
            finally
            {


            }
        }


        private string GetAttributeNameFromConfigString()
        {
            foreach (var item in cbAttributes.Items)
            {
                if ((string.IsNullOrEmpty(dataReference.ConfigString) == false) && (dataReference.ConfigString.Contains(item.ToString())))
                {
                    return item.ToString();
                }
            }
            return null;
        }


        private void LoadValuesFromConfigString()
        {
            string attributeName = GetAttributeNameFromConfigString();
            try
            {


                if (dataReference.ConfigString == "%Element%")
                {
                    cbDrOptions.SelectedItem = "Element name";
                }
                else if (string.IsNullOrEmpty(attributeName) == false)
                {
                    if (dataReference.ConfigString.Contains("@") == true)
                    {
                        cbDrOptions.SelectedItem = "Attribute value";
                        cbAttributes.SelectedItem = attributeName;
                    }
                    else
                    {
                        cbDrOptions.SelectedItem = "Attribute name";
                        cbAttributes.SelectedItem = attributeName;
                    }
                }
                else
                {
                    cbDrOptions.SelectedItem = "Custom";
                    tbCustom.Text = dataReference.ConfigString;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("There was an error loading the values from Config String. Error: " + ex.Message);
            }
        }




        private void btnCancel_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

 

 

 

Here is a summary of what the code snippet above does:

 

  • Open and display the form.
  • Load the other attributes from the same element.
  • Update the UI according to the selected combo box option.
  • Set up the data reference ConfigString when the user press the "OK" button.

 

Automating the deployment process

 

Whenever you are developing a custom data reference, there are a lot of steps that you need to follow in order to update and debug the new version of the custom assembly by attaching to process. Here they are:

 

  • Close PI System Explorer.
  • Delete the .dll and .pdb files from the C:\ProgramData in case you are using the same version on the assembly information but with an updated code.
  • Register/Update the new assembly using regplugin.
  • Open PI System Explorer.
  • Copy the .pdb file to the proper C:\ProgramData subfolder in order to be able to attach Visual Studio Debugger to the AFExplorer.exe process. The .dll file will be copied by PI System Explorer automatically.

 

You can automate this process by setting up pre-build and post-build events command line on your project settings in Visual Studio.

 

Pre-build event command line:

 

tasklist /fi "imagename eq afexplorer.exe" |find ":" > nul
if errorlevel 1 taskkill /IM afexplorer.exe
"%pihome%\AF\regplugin" -u '$(TargetFileName)'
SET dllFile="C:\ProgramData\OSIsoft\AF\PlugIns\1.1.0.0\4.0\$(TargetFileName)"
SET pdbFile="C:\ProgramData\OSIsoft\AF\PlugIns\1.1.0.0\4.0\$(TargetName).pdb"
IF EXIST %dllFile% del /F %dllFile%
IF EXIST %pdbFile% del /F %pdbFile%

 

Post-build event command line:

 

"%pihome%\AF\regplugin" "$(TargetPath)" "$(TargetDir)HtmlAgilityPack.dll" "$(TargetDir)Newtonsoft.Json.dll" /own:WikipediaDR.dll
copy "$(TargetDir)$(TargetName).pdb" "C:\ProgramData\OSIsoft\AF\PlugIns\1.1.0.0\4.0"
"%pihome%\AF\afexplorer.exe"

 

Please refer to the screenshot below if you don't know exactly where you need to set up those settings on your project properties.

 

 

Some comments of this automated deployment process:

 

  • Tasklist is used to close PI System Explorer in case it is opened.
  • The lines above where written on Windows 7 - 32 bits. You might want to change %PIHOME% to %PIHOME64% according to your development system. The same might be needed for regplugin as you might prefer using regplugin64.
  • The other libraries (HtmlAgilityPack.dll and Newtonsoft.Json.dll) need to be referenced when the WikipediaDR library is registered as without them, WikipediaDR will not work properly.
  • We have taken advantage of using the macros that Visual Studio provides. Note that under the hood, "$(TargetName) is changed to "WikipediaDR". Visual Studio provides a great window to help you write the command lines as shown below.

 

 

Testing the assembly

 

Now that we have finished developing and deploying the assembly, let's test it:

 

  • Open PI System Explorer and create a new database WikiDb
  • Create an element called Brazil and create two attributes: "Uruguay" and "Wikipedia Description" on the Brazil element.
  • The value of the attribute Uruguay should be Argentina.

 

The screenshot below shows how your AF hierarchy should looks like.

 

 

Select the Wikipedia Description attribute and make sure you are able to get the description from Brazil, Uruguay and Argentina by changing the ConfigString through the editor. Finally, get the description from "OSIsoft" by changing the source option to "Custom" under the Wikipedia ConfigString Editor. The result is shown below:

Conclusion

 

There are many use cases where the Wikipedia Data Reference would be a good fit. We have developed a sample application that shows weather information including forecast data from worldwide cities. The wikipedia data reference was used to show the description from each city. Make sure you comply with Wikipedia terms of service when using this data reference in your applications.

 

I hope you have found this blog post interesting and stay tuned for the upcoming ones!