HMI DataGrid View

Data Grid Sample

A data grid is a table. Data sets can be displayed in this table that has been defined via Engineering or dynamically via an array by JavaScript. The data grid can be configured as follows to display a one-dimensional array. Link the one-dimensional array with the attribute “SrcData” of the category “Data”. getSrcData and setSrcData can be used to set the data dynamically. We shall display the following data in the grid view. This data can come from an extension module or from the PLC directly. Here we shall fetch the data from the C# extension ( the data is actually located in the SQLite database). If we want to show a list of the following class then we build a JSON string and set the SrcData of the Grid view. All items will be updated automatically. On the other hand, we can update the grid view partially, for example, if we want to update only Column 3 then we need to supply the corresponding parameters for Column1, Column2, Column 4, and Column 5. If we don’t supply then the other fields are set to null (I think).

    public class Data
    {
        public virtual string Test1 { get; set; }
        public virtual int Test2 { get; set; }
        public virtual bool Test3 { get; set; }
        public virtual string CurrentTime { get; set; }
        public virtual int ComboBox { get; set; }
    }

Download the sample DataGrid_Hemelix.zip

Code Snippet 1: These data are displayed as JSON string in the grid view

Figure 1: Grid view when the sample is running

There is 2 Grid view in this sample. If we press the Delete button it will fetch the currently selected items and delete those data. When we press the Add button then it will fetch items from the 2  text boxes, checkbox, and Combobox, and add the items to both Grid views.  Following are the Data set that is used in the second grid view. Though we are displaying only displaying 3 data in the column of 2nd grid view. For 2nd Grid view, we have 2 sets of default data that is set at the startup in static way.

 

[
  {
    "CurrentTime": "3/6/2022 12:22:26 AM",
    "Test1": "Example Data1",
    "Test2": false,
    "Test3": true,
    "Test4": "Editable Data"
  },
  {
    "CurrentTime": "3/6/2022 12:22:26 AM",
    "Test1": "Example Data2",
    "Test2": true,
    "Test3": false,
    "Test4": "Editable Data"
  }
]

Code Snippet 2: These data are displayed as JSON strings in the 2nd grid view (below the Main Grid view)

Figure 2: Grid view  configuration and design

We have five fields in the data, so we need 5 columns in Grid View 1. A label is what we see in the user interface. The name field should match with the field.

Test1 => Test1 field in the class Data, a string type,

Test2 => Test2 field in the class Data, an integer type,

Test3 => Test3 field in the class Data, a Boolean type (if Checkbox is selected it is true otherwise it is false.)

CurrentTime => Generated time by C# code as DateTime.Now.ToString()

ComboBox => Combobox selection

 

2nd grid view design

We have three fields in the 2nd grid view, so we need 3 columns in Grid View 2.

Test1 => data in Column 1, a string type,

Test2 => data in column 2, a Boolean  type,

Test3 => data in column 3, a Boolean type (we made it as inverted of Test2)

CurrentTime => This data is available but not used anywhere

Test4 => another string type but not used in grid view 2

We have 2 sets of static data  as shown in code snippet 2

 

Figure 3: Grid view  configuration for 2nd grid view

How the extension can be developed is described at https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-server-extension/

Figure 4: Overall architecture of the sample, HMI Client (our HMI communicates via C# extension to SQLite database)

When the grid view 1 is attached then we call a method from the extension that will initiate and create some default data and stores those data in the SQLite database.

case "WriteValueInit":
  {
    lock(myLock) {
      if (!initDone) {
        string s = AssemblyDirectory;
        string path1 = System.Reflection.Assembly.GetAssembly(typeof(ServerExtensionCSharpEmpty1)).Location;
        string directory = Path.GetDirectoryName(path1);
        var ss = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        if (!log4net.LogManager.GetRepository().Configured) {
          s = "C:\\TwinCAT\\Functions\\TE2000-HMI-Engineering\\Infrastructure\\TcHmiServer\\Latest\\win-x86\\log4net.config";
          var configFile = new FileInfo(s);
          if (!configFile.Exists) {
            throw new FileLoadException(String.Format("The configuration file {0} does not exist", configFile));
          }
          log4net.Config.XmlConfigurator.Configure(configFile);
        }
        sqlConnector = new SQLConnecter();
        sqlConnector.Do();
        initDone = true;
        log.Info("path1 =" + path1);
        log.Info("s =" + s);
        log.Info("ss =" + ss);
        log.InfoFormat("Initializing WriteValueInit {0}", command.WriteValue);
      }
    }
  }
  command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
  command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
  break;   

Log file  for the real server is generated in the folder  C:\TwinCAT\Functions\TF2000-HMI-Server\API\1.3.0.0\net48

and log4net.config should be stored in C:\TwinCAT\Functions\TF2000-HMI-Server  for actual server where we don’t have engineering version.

 

Code Snippet 3: Code snippet for initializing the extension which creates some test data

The Do() method creates a thread and that thread creates the following data for the HMI, So when we start the HMI it generates the data and stores those data in the database.

SQLConnecter class:

This version is different than what we have published in the zip file. The HMI in the zip file always creates 12 items when the HMI is created. In this version, we have adjusted it so that when the HMI starts then it checks if there are any items in the DB file. If it finds something then it uses those items. Otherwise, it will create 3 data items.

Also if the HMI finds the DB file then it does not create a new schema.

    private static ISessionFactory sessionFactory;
    private readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    private string data = "";
    private static ISessionFactory SessionFactory {
      get {
        if (sessionFactory == null) CreateSessionFactory();
        return sessionFactory;
      }
    }
    public string MyData {
      get {
        return data;
      }
      set {
        if (data != value) {
          data = value;
        }
      }
    }
    public SQLConnecter() {
      sessionFactory = CreateSessionFactory();
    }
    public void Do() {
      Thread t = new Thread(DoInBackground);
      t.Start();
    }
    public void DoInBackground() {
      try {
        MainFN();
      }
      catch(Exception ex) {}
    }
    private static ISessionFactory CreateSessionFactory() {
      return Fluently.Configure().Database(SQLiteConfiguration.Standard.UsingFile("projectData.db")).Mappings(m =>m.FluentMappings.AddFromAssemblyOf < Data > ()).ExposeConfiguration(BuildSchema).BuildSessionFactory();
    }
    private static void BuildSchema(NHibernate.Cfg.Configuration config) {
      //new SchemaExport(config).Create(true, false);
      if (!File.Exists("projectData.db")) {
        new SchemaExport(config).Create(false, false);
        //new SchemaExport(config).Execute(false, false, false);
      }
    }
    private void MainFN() {
      using(var session = sessionFactory.OpenSession()) {
        using(var transaction = session.BeginTransaction()) {
          List < Data > myDataInstance = (List < Data > ) session.QueryOver < Data > ().List < Data > ();
          if (myDataInstance.Count <= 0) {
            for (int i = 0; i < 3; i++) {
              myDataInstance[i] = new Data();
              myDataInstance[i].Test1 = "Example Data " + (i + 1).ToString();
              myDataInstance[i].Test2 = i + 11;
              myDataInstance[i].Test3 = (i % 2 == 0);
              myDataInstance[i].CurrentTime = DateTime.Now.ToString();
              myDataInstance[i].ComboBox = i % 4;
              myDataInstance.Add(myDataInstance[i]);
              session.SaveOrUpdate(myDataInstance[i]);
            }
          }
          MyData = JsonConvert.SerializeObject(myDataInstance, Formatting.Indented, new JsonSerializerSettings {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
          });
          transaction.Commit();
        }
      }
    }
    public void DeleteLine(int index) {
      using(var session = sessionFactory.OpenSession()) {
        using(var transaction = session.BeginTransaction()) {
          List < Data > storesAll = (List < Data > ) session.QueryOver < Data > ().List < Data > ();
          if (storesAll.Count > 0) {
            var sorefromlist = storesAll[index];
            var store = session.Get < Data > (sorefromlist.Id);
            session.Delete(store);
          }
          session.Flush();
          transaction.Commit();
        }
      }
      ReadData();
    }
    public void ReadData() {
      try {
        using(var session = sessionFactory.OpenSession()) {
          using(var transaction = session.BeginTransaction()) {
            var list = session.QueryOver < Data > ().List();
            MyData = JsonConvert.SerializeObject(list, Formatting.Indented, new JsonSerializerSettings {
              ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            });
          }
        }
      }
      catch(Exception) {}
    }
    public void AddItem(string test1, int test2, bool truefalse, int comb) {
      try {
        using(var session = sessionFactory.OpenSession()) {
          using(var transaction = session.BeginTransaction()) {
            List < Data > list = (List < Data > ) session.QueryOver < Data > ().List();
            Data d = new Data();
            d.Test1 = test1;
            d.Test2 = test2;
            d.Test3 = truefalse;
            d.CurrentTime = DateTime.Now.ToString();
            d.ComboBox = comb;
            list.Add(d);
            session.SaveOrUpdate(d);
            session.Flush();
            transaction.Commit();
          }
        }
      }
      catch(Exception) {}
      ReadData();
    }
 

 

 

 

Code Snippet 4: creating default data for HMI

For the combo box (on the Desktop. view) we are the static source data by using the following JavaScript code. 

console.log('combobox attached');
var combobox = TcHmi.Controls.get("TcHmiCombobox");
if(combobox != undefined) {     let comboString =  ["TopOne", "TopSecond", "TopThree", "TopFour"];     combobox.setSrcData(comboString);     combobox.setSelectedIndex(0);     }

Code Snippet 5: Inserting data to combobox

Figure 5: Attaching data to combo box by setSrcData(JSONString) method

Figure 6: Actions behind the Delete button, it calls a method from the extension with selected index

When we press the Delete button, we get a reference to the selected index, the selected index is written by the framework to command.WriteValue field, then it is deleted from the SQLite.

//When we write something then always IsSet is true and WriteValue is not null
case "WriteValueDelete": {
  if (command.IsSet == true && command.WriteValue != null) {
    int index = Int32.Parse(command.WriteValue);
    sqlConnector.DeleteLine(index);
  }
}
command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
break;

 

Figure 6:  How data is deleted from the gridview

Figure 7: Actions behind the Add button, it calls a method from the extension with selected index

When we press the Add button, we get data from the edit box, check box, and combo box and convert the data to JSON string and write the data to the extension by the following code. The item is DeserializeObject as Data and passes to SqlConector object for add. For the 2nd grid view, we get the existing data as array (vals[]), we have an object name person (name does not match to the context!) and push it to the array- Finally, the setSrcData is called to set it.

 

JavaScript code for adding new items:

console.log('Edit source code 2nd ');
var newText = TcHmi.Controls.get("TcHmiDatagrid_1");
if (newText != undefined) {
  let dataText = newText.getSrcData();
  console.log(dataText.length);
  console.log(dataText.toString());
  var vals = [];
  for (var i = 0; i < dataText.length; i++) {
    vals.push(dataText[i]);
  }
  var test1 = TcHmi.Controls.get("TcHmiTextbox_Test1");
  var test2 = TcHmi.Controls.get("TcHmiTextbox_Test2");
  var checkbox = TcHmi.Controls.get("TcHmiCheckbox");
  var combobox = TcHmi.Controls.get("TcHmiCombobox");
  var checkBoxStateChecked = false;
  let checkboxState = checkbox.getToggleState();
  if (checkboxState == 'Active') {
    checkBoxStateChecked = true;
  }
  else {
    checkBoxStateChecked = false;
  }
  const person = {
    CurrentTime: "sdfsd",
    Test1: test1.getText(),
    Test2: checkBoxStateChecked,
    Test3: !checkBoxStateChecked,
    Test4: "fdss"
  };
  vals.push(person);
  console.log(vals.length);
  newText.setSrcData(vals);
}

//Add items via the extension to database

case "WriteValueAdd":
if (command.IsSet == true && command.WriteValue != null) {
  Data routes_list = JsonConvert.DeserializeObject < Data > (command.WriteValue);
  sqlConnector.AddItem(routes_list.Test1, routes_list.Test2, routes_list.Test3, routes_list.ComboBox);
}

Figure 8:  How the data is set to SQL from the HMI

Figure 8: Actions behind the Add button, it calls a method from the extension with selected index

1 => The main grid view

2 => SrcData of the grid view has been configured with the return value of the GetDataValue parameter of the extension

3 => The source code of the extension which creates or supplies the JSON data for the grid view 1, When GetDataValue is used then we make sure some valid data is returned. If the initialized has not been called then ReadValue is set to “”,  command.ReadValue = “”; (note that MyData  is not static anymore)

 

case "GetDataValue":
  if (sqlConnector != null) {
    command.ReadValue = sqlConnector.MyData;
  }
  else {
    command.ReadValue = "";
  }
  command.ExtensionResult = ServerExtensionCSharpEmpty1ErrorValue.ServerExtensionCSharpEmpty1Success;
  command.ResultString = "Unknown command '" + command.Mapping + "' not handled.";
  break;

 

 

Discussion:

0 => GridView SrcData has schema of Array

//SrcData has schema Schema: tchmi:general#/definitions/Array

This causes an error from somewhere, if you know you can comment at group https://groups.google.com/g/hemelix

1 => When we use the log4net with FluentNHibernate then logfile with Debug mode is generated for FluentNHibernate. We need to add some info to the logger to indicate that we are not interested to get debug information from the NHibernate.

<log4net>
  <root>
    <level value="ALL" />
    <appender-ref ref="file" />
  </root>
  //Add the following
 <logger name="NHibernate">
   <level value="ERROR" />
 </logger>
 <logger name="NHibernate.SQL">
   <level value="ERROR" />
 </logger>
 //Add upto this
  <appender name="file" type="log4net.Appender.RollingFileAppender">
    <file value="logfile.log" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="5" />
    <maximumFileSize value="10MB" />
    <staticLogFileName value="true" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date [%thread] %level %logger - %message%newline" />
    </layout>
  </appender>
</log4net>

 

2 => Read data from PLC and make a backup to the SQLite database

When we read data from PLC, we can save it to the SQLite database. If the data from the PLC is not available then we can load the same version from the SQLite database.  We have lots of synchronous functions and asynchronous functions. We can call asynchronous JavaScript call as synchronous call by modifying the provided example.

3 => We see the following message when the extension is not activated

TcHmiFramework.js:687 [2022-04-17T07:39:48.523Z][Error] [Source=Framework, Module=TcHmi.System.TriggerManager, Event=Desktop.onAttached, ObjectType=WriteToSymbol] Code: 3005/0xbbd, Message: E_SERVER_COMMAND_ERROR
  Reason: %s%ServerExtensionCSharpEmpty1.WriteValueInit%/s%: Error in command for symbol: 'ServerExtensionCSharpEmpty1.WriteValueInit' in response from server with id: '7'.
  Domain: TcHmi.System.Symbol
  as result of: Code: 2050/0x802, Message: INVALID_DOMAIN
    Reason: Invalid domain
    Domain: TcHmiSrv

See more at https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-server-extension/

 

4 => For the engineering version and for the production HMI server we should store the config file in a different  file

Logfile for the real server is generated in the folder  C:\TwinCAT\Functions\TF2000-HMI-Server\API\1.3.0.0\net48 and log4net.config should be stored in C:\TwinCAT\Functions\TF2000-HMI-Server  for an actual server where we don’t have engineering version.

5 => How to add data to Grid View as a list of JavaScript object

var gridView = TcHmi.Controls.get("TcHmiDatagrid_1");
var vals = [];
const myData = {
  CurrentTime: GetCurrentTime(),
  Test1: test1.getText(),
  Test2: checkBoxStateChecked,
  Test3: !checkBoxStateChecked,
  Test4: "some unused string"
};
vals.push(myData);
gridView.setSrcData(vals);
//let dataText = gridView.getSrcData(); //get source data from gridView

 

 

 

Download the sample from the link given above.

Next, let’s try to understand what is historize at https://www.hemelix.com/scada-hmi/twincat-hmi/twincat-hmi-historize-symbol/

Ask questions related to Hemelix sample code and design at Google group https://groups.google.com/g/hemelix