Theme:
v
Example

Here is an example extracted from my APL2000 2008 Conference Paper.

Developing a Client Server C# & APL+Win Application

Introduction

Another possible way of interfacing C# and APL is to write Client Server C# & APL+Win applications.

In this case, C# and APL both have a very specific role:

  • C# is the Client and APL the Server
  • C# handles the User Interface and APL handles calculations, files, etc.

Basically, your application becomes a .Net application.

You develop your User Interface using Visual Studio and C#.

When time comes to write event handlers, you decide if you want to write the event handler in C# or if you want to sub-contract the event handler to APL. You can also use a mix of the 2.

This way of writing applications is very powerful and efficient because you use each software (C# and APL) for its best strength: C# and Visual Studio are unbeatable for writing User Interfaces and by far; APL is also hard to beat for its computational power, its flexible (colossal) component files, etc.

You must know that when writing applications this way:

  1. You get a total access to your APL workspace from C# (variables, functions, system variables, system functions, commands, User Command Processor, etc.)
  2. Calls into the APL workspace from C# are extremely easy
  3. Calls into the APL workspace from C# are extremely fast: there is virtually “no” overhead at all involved.
  4. You can exchange any kind of data between APL and C#, even the most complex nested arrays!

I am known to have always been fond of developing User Interfaces: this is the thing I like to do best. And I have enjoyed developing APL User Interfaces for years and years.

Having learnt how to do it with C# and Visual Studio, I think I will never again write any User Interface in APL. My clear choice now is to use C#/Visual Studio to develop User Interfaces and to use APL in the background for data processing, files, etc. In some cases I would use SQL Server instead of APL files, especially for multi user transactional applications.

Interfacing C# and APL this way has 2 other advantages:

  1. It forces you to totally separate the User Interface from the Business logic of your application, something APL developers have never been taught to do, but which is a basic rule adopted by a vast majority of application developers all around the world
  2. It allows you to easily transform your application to an Internet ClickOnce Client Server C# & APL+Win application (more about this later)

Requirements

So what do you need to write a Client Server C# & APL+Win application?

Believe it or not, you just need to register APL+Win as an ActiveX Server and you need to use a short and simple little DLL.

Registering APL+Win as a ActiveX Server

Most often this has already been done when APL+Win was installed on your computer, if you answered Yes to the final installation questions.

But in any case, if you want to be sure your APL+Win is registered as an ActiveX Server, you can at any time open a DOS Command Prompt window and do the following:

     regsvr32 path\aplwco.dll
     path\aplw.exe MyApp.ini /RegServer

where path represents the directory where APL is installed on your computer (for example: C:\APLWIN80) and MyApp.ini represents your application .INI file

The APLServer DLL

Here is how to create an APLServer C# DLL which will allow you to write Client Server C# & APL+Win application:

  1. Start Visual Studio 2008
  2. Select File / New / Project…
  3. Select Class Library and enter LescasseConsulting.APLServer for the project name
  4. Once the project is created, right click on Class1.cs in Solution Explorer and select Rename
  5. Enter: APLServer.cs
  6. Click Yes to answer the Visual Studio prompt
  7. Select the whole code in APLServer.cs and replace it with the following code (see other examples showing how to use the APL Server DLL):
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace LescasseConsulting.AplServer
    {
        /// 
        /// Apl ActiveX Server Class
        /// This class allows to call and use APL+Win from a C# application
        /// 
        public class AplServer : APLW.WSEngineClass
        {
            public delegate void NotifyEvent(object Code, object Data);
            public event NotifyEvent NotifyClient;
            
            public AplServer()
            {
            }
    
            public AplServer(string ws)
            {
                SysCall1("LOAD", ws);
                this.Notify += new APLW._WSEngineEvents_NotifyEventHandler(AplServer_NotifyClient);
            }
    
            void AplServer_NotifyClient(ref object Code, ref object Data, out object Result)
            {
                Result = null;
                NotifyClient(Code, Data);
            }
    
            public object CallFn(string func)
            {
                return Call0(func);
            }
    
            public object CallFn(string func, object rarg)
            {
                return Call1(func, rarg);
            }
    
            public object CallFn(string func, object rarg, object larg)
            {
                return Call(func, rarg, larg);
            }
    
            public object CallSysFn(string func)
            {
                return SysCall0(func);
            }
    
            public object CallSysFn(string func, object rarg)
            {
                return SysCall1(func, rarg);
            }
    
            public object CallSysFn(string func, object rarg, object larg)
            {
                return SysCall(func, rarg, larg);
            }
    
            public void CloseAPL()
            {
                Close();
            }
    
            public object Execute(string str)
            {
                return Exec(str);
            }
    
            public object GetSysVar(string var)
            {
                return get_SysVariable(var);
            }
    
            public void SetSysVar(string var, object pValue)
            {
                set_SysVariable(var, pValue);
            }
    
            public object GetVariable(string var)
            {
                return get_Variable(var);
            }
    
            public void SetVariable(string var, object pValue)
            {
                set_Variable(var, pValue);
            }
    
            public void RunSysCommand(string str)
            {
                SysCommand(str);
            }
        }
    }
    
  8. In the Visual Studio toolbar select Release instead of Debug in the first combo box
  9. Press F6 to compile the DLL

Developing your first Client Server C# & APL+Win application

You can now proceed to develop your first Client Server C# & APL+Win application.

Creating the C# Client Application

Knowing the limited amount of time devoted to this lecture, our Client+Server C# & APL+Win application will be a simple one: a minimal human resources enterprise database.

  1. Start Visual Studio 2008
  2. Select File/New Project…
  3. Select the Windows Forms Application template
  4. Enter: APL2000.Conf2008App as the application Name
  5. Check the Create directory for Solution check box
  6. Click OK
  7. In the Properties pane, change the StartPosition property to CenterScreen so that the form will get displayed in the center of the screen when the application starts

    The C# application will be the Client application. It will contain the whole User Interface of our application.

    The APL application will be the Server application. It will contain the Data Layer (i.e. the database and the necessary APL routines to use the database) as well as the Business Logic Layer.

Adding a Reference to the LescasseConsulting.APLServer DLL

Since we will want to use APL from C# we need to reference the LescasseConsulting.APLServer DLL in our application.

To do so:

  1. Right click on References in Solution Explorer (just below Properties)
  2. Select: Add Reference…
  3. In the Add Reference dialog box, click on the Browse button
  4. Navigate to the LescasseConsulting.AplServer\LescasseConsulting.AplServer\Bin\Release directory
  5. Double click on the LescasseConsulting.AplServer.DLL file name (this adds a reference to this DLL to the project)
  6. Double click on the Interop.APLW.DLL file name (this adds a reference to this DLL to the project)
  7. Right click on the Form1 form and select View Code (alternatively, right click on Form1.cs in Solution Explorer and select View Code)

    The Form1.cs code should look like this:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    namespace APL2000.Conf2008App
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        }
    }
    
  8. Add the following using clause at the bottom of the using clauses section:
    using LescasseConsulting.AplServer;
    
  9. Declare a private field called apl of type AplServer, just above the Form1() constructor
  10. Create an instance of the AplServer apl object, pointing to the C:\aplwin\ele\conf2008.w3 workspace, in the Form1() constructor

    That’s all it takes to be able to use anything you want in the APL+Win CONF2008.W3 workspace from your C# application!

    The complete class code should now look like this:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using LescasseConsulting.AplServer;
    
    namespace APL2000.Conf2008App
    {
       public partial class Form1 : Form
       {
          private AplServer apl;
    
          public Form1()
          {
             InitializeComponent();
             apl = new AplServer(@"C:\aplwin\ele\conf2008");
          }
       }
    }
    

    To understand this well with an analogy to APL, consider that the private AplServer apl; instruction role is only to declare a global variable called apl, but at this stage the apl variable is just declared, it is not yet instantiated and therefore you can’t yet use it.

    The apl = new AplServer(@"C:\aplwin\ele\conf2008"); instruction is the one which creates apl as an instance of the AplServer object, informing it to point to the CONF2008 workspace (note that you should not include the .W3 extension when passing an APL workspace as a parameter to the AplServer object constructor)

    Because you have declared the apl variable (we say field in C#) outside of any method, it is a global field for the whole class and can therefore be used in any method of the Form1 class. Had we included the private AplServer apl; instruction within the Form1() method, the apl field would have been a local “variable” to the Form1() constructor and therefore would have been destroyed as soon as the Form1() constructor would have finished executing, just as a local APL variable stops existing as soon as the function it is localized in finishes executing.

Creating the C# User Interface

Let’s create our User Interface; we want to be able to navigate thru the records of our database:

  1. Locate the BindingNavigator object in the VS2008 toolbox (it is located in the Data section)
  2. Drag and drop a BindingNavigator object onto the form
  3. Drag and drop 4 Labels to the form and change their captions to: Department, Name, First Name and Birth Date
  4. Drag and drop a ComboBox to the form and rename it: cbDept
  5. Drag and drop 2 TextBox to the form and rename them txtName and txtFirstName
  6. Finally drag and drop a DateTimePicker object to the form and rename it dtpBirthDate
  7. Click on the Form1 form and change its Text property (in the Properties pane) to:
    Human Resources Database
  8. Align controls and resize the form so that it looks like the following:
    UseAPLFromCSharp1.jpg

    We are done with the User Interface design of our application.

    We now need to handle events, like clicks on the BindingNavigator buttons.

    But before that, we need to populate the Department combo box with our Enterprise departments. We will do that in the Form1 Load event: this event is the first event which occurs just before the form gets displayed when you start the C# application so it’s a good place for initializing controls on the form.

  9. Double click on a free area of the form (pay attention not to double click on any control)

    This creates an event handler in the Form1.cs code and displays it:

    private void Form1_Load(object sender, EventArgs e)
    {
    
    }
    
  10. Add the following code to this event handler:
    object depts = apl.CallFn("GetDepts");
    cbDept.DataSource = (string[])depts;
    

    The first instruction calls the niladic GetDepts function in the CONF2008.W3 APL workspace and returns its result. The result of calling ANY function in an APL workspace is a C# object of type object. This type is the parent of all other C# types and as such can internally contain absolutely anything.

    We know that the GetDepts APL function returns a “vector of character vectors” (we call this an “array of strings” in C#).

    Therefore to really get an array of strings (noted: string[] in C#), we must cast the depts variable which is an object, to its real content which is a string[].

    This is done by using the following notation: (string[])depts

    We then simply assign this array of strings to the DataSource property of our cbDept ComboBox. That’s enough to populate the ComboBox.

    Isn’t this really easy?

    Note that we could have done all that in the following way:

    string[] depts = (string[])apl.CallFn("GetDepts");
    cbDept.DataSource = depts;
    

    or even more simply (didn’t we all use to be one liners at one time as APL developers?):

    cbDept.DataSource = (string[])apl.CallFn("GetDepts");
    

    avoiding having to create the depts variable at all.

    So, our Form1_Load event handler now looks like this:

    private void Form1_Load(object sender, EventArgs e)
    {
       cbDept.DataSource = (string[])apl.CallFn("GetDepts");
    }
    

    Start the application and open the cbDept ComboBox:

    UseAPLFromCSharp2.jpg

    As you can see the cbDept ComboBox has indeed been populated with departments extracted from an APL+Win Colossal File by an APL+Win function! Great!

    Now we would also like to display the first database record when the form loads.

    We can do this by adding the following code to the Form1_Load event handler:

    object[] record1 = (object[])apl.CallFn("GetRecord", 1);
    int dept = (int)record1[0] - 1;
    string name = (string)record1[1];
    string firstname = (string)record1[2];
    int[] birthdate = (int[])record1[3];
    cbDept.SelectedIndex = dept;
    txtName.Text = name;
    txtFirstName.Text = firstname;
    dtpBirthDate.Value = new DateTime(birthdate[0], birthdate[1], birthdate[2]);
    

    Let’s explain this code : on the first line we call the GetRecord APL+Win function with a argument of 1 and capture its result in variable record1 which is declared to be an array of objects (object[]).

    Here is the GetRecord APL function:

        ∇ R←GetRecord number;tie;offset
    [1]   ⍝∇ R←GetRecord number -- Retrieves record <number> from database
    [2]
    [3]   tie←TieDb
    [4]   :if number>0
    [5]   :andif number<(2⊃⎕cfsize tie)-DbOffset
    [6]       R←⎕cfread tie,number+DbOffset
    [7]       R[4]←EncodeDate R[4]  ⍝ business layer EncodeDate function
    [8]   :else
    [9]       R←'No record at this location!'
    [10]  :endif
        ∇
    
          ⎕vr'EncodeDate'
        ∇ R←EncodeDate date
    [1]   ⍝∇ R←EncodeDate date -- Transforms a scalar (yyyymmdd) date
    [2]   ⍝∇                      into a 3-element vector (yyyy mm dd)
    [3]   ⍝∇ Note: Belongs to the Business Layer
    [4]
    [5]   R←⊂10000 100 100⊤date
        ∇
    

    We will explain a little later on that we have created an EncodeDate and DecodeDate subroutine to separate the Data Layer stuff from the Business Layer stuff.

    Remember that the result of calling an APL function is ALWAYS an object in C#, but that an object can contain anything, i.e. any other type or structure of types. Here, we know that the GetRecord APL function returns a nested vector containing integers and strings:

          GetRecord 1
     3 Swain Rex  1950 1 1
    
          ]display GetRecord 1
    .…------------------------.
    |   .…----..…--..…-------.|
    | 3 |Swain||Rex||1950 1 1||
    |   '-----''---''~-------'|
    '¹------------------------'
    

    Therefore, since this vector is heterogeneous, we must declare it as an array of objects in C#.

    To understand this well, just realize that each element of an array of objects is itself an object and as such can in turn contain anything. So, the first element is an object which contains an integer, the second and third element are objects which contain strings and the fourth element is an object which contains a vector of integers.

    It then becomes easy to extract each element to its internal type, using the appropriate cast expression: this is what’s done in the following 4 instructions which create 4 local variables (dept, name, firstname and birthdate):

    int dept = (int)record1[0] - 1;
    string name = (string)record1[1];
    string firstname = (string)record1[2];
    int[] birthdate = (int[])record1[3];
    

    It is then easy and simple to update the form controls with these 4 pieces of data:

    cbDept.SelectedIndex = dept;
    txtName.Text = name;
    txtFirstName.Text = firstname;
    dtpBirthDate.Value = new DateTime(birthdate[0], birthdate[1], birthdate[2]);
    

    Note that the DateTimePicker Value property is a DateTime object: you can see that using Intellisense, by typing a dot (.) after dtpBirthDate and navigating to the Value property: here is what Intellisense displays:

    UseAPLFromCSharp3.jpg

    In C#, as long as you respect types, you’ll be ok and when you understand that this is a strict universal rule, C# becomes pretty easy to understand.

    Here, Intellisense tells us that a DateTimePicker Value property returns a DateTime object, therefore, if we want to set the dtpBirthDate Value property we must provide it with an instance of a DateTime object. This is exactly what the following instruction does:

    dtpBirthDate.Value = new DateTime(birthdate[0], birthdate[1], birthdate[2]);
    

    Finally, as we did before, we can easily avoid having to create temporary local variables and our Form1_Load handler can easily be refactored to the following code:

    private void Form1_Load(object sender, EventArgs e)
    {
        cbDept.DataSource = (string[])apl.CallFn("GetDepts");
        object[] record1 = (object[])apl.CallFn("GetRecord", 1);
        cbDept.SelectedIndex = (int)record1[0] - 1;  //department
        txtName.Text = (string)record1[1];           //name
        txtFirstName.Text = (string)record1[2];      //first name
        dtpBirthDate.Value = new DateTime(           //birth date
            ((int[])record1[3])[0],
            ((int[])record1[3])[1], 
            ((int[])record1[3])[2]
            );
    }
    

    You may want to note a few things:

    • First, we have added comments to make our code more readable
    • Second, the last statement is written over 5 lines to also improve readability
    • Finally, the last statement comment is embedded within the statement itself (yes, this is possible in C#!!!)
  11. We will need to display other database records using the BindingNavigator buttons.

    Analyzing this quickly, we see that we will need to use the same instructions as the ones used in the Form1_Load event handler to display the first record.

    So, in order to not duplicate code (something to avoid in any language, at any price), we think it would be a good idea to build a method which we could call and reuse for displaying a record.

    In order to do so, highlight the lines you’d like to remove from the Form1_Load event handler and add to the new method, then right click on the selected code and choose Refactor / Extract Method…

    UseAPLFromCSharp4.jpg

    You are then prompted to enter a method name: type: DisplayRecord:

    UseAPLFromCSharp5.jpg

    then click OK.

    Visual Studio automatically refactors your code to the following, automatically for you:

    private void Form1_Load(object sender, EventArgs e)
    {
        cbDept.DataSource = (string[])apl.CallFn("GetDepts");
        DisplayRecord();
    }
    
    private void DisplayRecord()
    {
        object[] record1 = (object[])apl.CallFn("GetRecord", 1);
        cbDept.SelectedIndex = (int)record1[0] - 1;  //department
        txtName.Text = (string)record1[1];           //name
        txtFirstName.Text = (string)record1[2];      //first name
        dtpBirthDate.Value = new DateTime(           //birth date
            ((int[])record1[3])[0],
            ((int[])record1[3])[1],
            ((int[])record1[3])[2]
            );
    }
    

    This is almost perfect. However, we would like to pass the record number as an argument to the DisplayRecord method.

    We can easily do this as follows (changes highlighted):

    private void Form1_Load(object sender, EventArgs e)
    {
        cbDept.DataSource = (string[])apl.CallFn("GetDepts");
        DisplayRecord(1);
    }
    
    private void DisplayRecord(int recno)
    {
        object[] record1 = (object[])apl.CallFn("GetRecord", recno);
        cbDept.SelectedIndex = (int)record1[0] - 1;  //department
        txtName.Text = (string)record1[1];           //name
        txtFirstName.Text = (string)record1[2];      //first name
        dtpBirthDate.Value = new DateTime(           //birth date
            ((int[])record1[3])[0],
            ((int[])record1[3])[1],
            ((int[])record1[3])[2]
            );
    }
    

    Our next step is to enable the BindingNavigator buttons.

  12. Create a new method called EnableButtons as follows:
    private void EnableButtons()
    {
        bindingNavigatorMoveFirstItem.Enabled = true;
        bindingNavigatorMoveLastItem.Enabled = true;
        bindingNavigatorMoveNextItem.Enabled = true;
        bindingNavigatorMovePreviousItem.Enabled = true;
    }
    
  13. Add the following statements at the top of the Form1 class, just below the apl declaration:
    private int recordNo;
    private int totalRecs;
    

    This declares a “global variable” recordNo as an integer. This variable will hold the currently displayed record number. The totalRecs variable will hold the total number of records in our APL database.

  14. Add the following statement at the beginning of the DisplayRecord method to update the recordNo variable:
    recordNo = recno;
    
  15. Add the following statement to the Form1_Load event handler:
    totalRecs = (int)apl.CallFn("GetNumberOfRecords");
    

    This initializes the totalRecs variable once for all (remember it is a “global variable”) to the total number of records in the APL database.

  16. Double click on the bindingNavigatorMoveNextItem button in the BindingNavigator control to create its Click event handler and add the following code:
    private void bindingNavigatorMoveNextItem_Click(object sender, EventArgs e)
    {
        DisplayRecord(recordNo + 1);
    }
    
  17. Double click each of the 3 other BindingNavigator buttons and enter the following code:
    private void bindingNavigatorMovePreviousItem_Click(object sender, EventArgs e)
    {
        DisplayRecord(recordNo - 1);
    }
    
    private void bindingNavigatorMoveLastItem_Click(object sender, EventArgs e)
    {
        DisplayRecord(totalRecs);
    }
    
    private void bindingNavigatorMoveFirstItem_Click(object sender,EventArgs e)
    {
        DisplayRecord(1);
    }
    

    We must now get sure the BindingNavigator control displays the current record and the total number of records. The right place to put this code in is the DisplayRecord method of course.

  18. Add the following 2 instructions at the end of the DisplayRecord method:
    bindingNavigatorCountItem.Text = "of " + totalRecs.ToString();
    bindingNavigatorPositionItem.Text = recordNo.ToString();
    

    These 2 instructions set the Text property of the BindingNavigator controls used to display the current record and the total number of records. Note that since a Text property is of type string, we must use the ToString() method to convert the recordNo and totalRecs integer variables to strings.

  19. Now you can test the application. Just click on the Visual Studio UseAPLFromCSharp12.jpg toolbar button to start the application.

    The form should display the first record from our APL Colossal File database:

    UseAPLFromCSharp6.jpg

    and you can click on the BindingNavigator buttons to display the various database records:

    UseAPLFromCSharp7.jpg

    You may note that the birth date is displayed using the current culture of the local computer running the application. This works without us having had to write any line of code!

    Note that each time you click on a button, a call to the GetRecord APL function is made, the APL Colossal File database is opened, a record is read from this database and returned to the C# application, then displayed in the form.

    As you can see by testing this application, all this is absolutely instantaneous.

    We have one more step to perform to finish our APL database records navigation system: when we reach either end of the database, we should disable the corresponding buttons to prevent the user from trying to go past the end of the database.

  20. Change the EnableButtons method as follows (changes highlighted):
    private void EnableButtons()
    {
        bindingNavigatorMoveFirstItem.Enabled = (recordNo > 1);
        bindingNavigatorMoveLastItem.Enabled = (recordNo < totalRecs);
        bindingNavigatorMoveNextItem.Enabled = (recordNo < totalRecs);
        bindingNavigatorMovePreviousItem.Enabled = (recordNo > 1);
        bindingNavigatorCountItem.Enabled = true;
    }
    

    Note that we must also call the EnableButtons method each time we use the DisplayRecord method, so add a call to EnableButtons at the end of the DisplayRecord method:

    private void DisplayRecord(int recno)
    {
        recordNo = recno;
        object[] record1 = (object[])apl.CallFn("GetRecord", recno);
        cbDept.SelectedIndex = (int)record1[0] - 1;  //department
        txtName.Text = (string)record1[1];           //name
        txtFirstName.Text = (string)record1[2];      //first name
        dtpBirthDate.Value = new DateTime(           //birth date
            ((int[])record1[3])[0],
            ((int[])record1[3])[1],
            ((int[])record1[3])[2]
            );
        bindingNavigatorCountItem.Text = "of " + totalRecs.ToString();
        bindingNavigatorPositionItem.Text = recordNo.ToString();
        EnableButtons();
    }
    

    Our application is now finished as far as displaying records from the database is concerned.

    But we would like to add one more functionnality: the ability to add new records to the database.

  21. To do so, add a button to the BindingNavigator control.
    Note: increase the form width if there’s not enough room to display the button
    Change its Name property to: bnSave
    Change its DisplayStyle property to: Text
    Change its Text property to: Save
  22. Add the following statement to the EnableButtons method:
    bindingNavigatorAddNewItem.Enabled = true;
    
  23. Double click on the + BindingNavigator button to create its Click handler and enter the following code:
    private void bindingNavigatorAddNewItem_Click(object sender, EventArgs e)
    {
        cbDept.Text = "";
        txtName.Text = "";
        txtFirstName.Text = "";
        dtpBirthDate.Value = new DateTime(1900,1,1);
        bindingNavigatorMoveFirstItem.Enabled = false;
        bindingNavigatorMoveLastItem.Enabled = false;
        bindingNavigatorMoveNextItem.Enabled = false;
        bindingNavigatorMovePreviousItem.Enabled = false;
        bindingNavigatorCountItem.Enabled = false;
        bindingNavigatorCountItem.Text = "";
        bindingNavigatorPositionItem.Text = "";
        bnSave.Enabled = true;
    }
    

    This code is so simple to understand that I won’t explain it. Simply note that it is not obvious to empty the content of the dtpBirthDate control, so I am displaying a default birth date of 1 january 1900. In a real application I would use the 2 instructions that allow to empty this control (which consists in setting the Format property to Custom and the CustomFormat property to an empty string).

  24. Now double click on the BindingNavigator bnSave button and enter the following code, which is a little bit more complex:
    private void bnSave_Click(object sender, EventArgs e)
    {
        if (cbDept.Text.Trim().Length == 0
         || txtName.Text.Trim().Length == 0
         || txtFirstName.Text.Trim().Length == 0
         || dtpBirthDate.Value == new DateTime(1900, 1, 1))
            MessageBox.Show(
                "Please enter valid data in each field",
                this.Text,
                MessageBoxButtons.OK,
                MessageBoxIcon.Stop
                );
        else
        {
            int dept = cbDept.SelectedIndex + 1;
            string name = txtName.Text.Trim();
            string firstname = txtFirstName.Text.Trim();
            int[] birthdate = new int[]{
                dtpBirthDate.Value.Year,
                dtpBirthDate.Value.Month,
                dtpBirthDate.Value.Day
                };
            object[] arg = { dept, name, firstname, birthdate };
            object result = apl.CallFn("AddRecord", arg);
            if (result is string)
                MessageBox.Show(
                    (string)result,
                    this.Text,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error
                    );
            else
            {
                totalRecs = (int)result;
                DisplayRecord(totalRecs);
                bnSave.Enabled = false;
            }
        }
    }
    

    Let’s comment this code:

    The initial if statement:

        if (cbDept.Text.Trim().Length == 0
         || txtName.Text.Trim().Length == 0
         || txtFirstName.Text.Trim().Length == 0
         || dtpBirthDate.Value == new DateTime(1900, 1, 1))
    

    checks that none of the fields is empty and that the Birth Date field has been changed from the default: 1 January 1900.

    || is the “or” primitive in C#.

    Note how you can call methods and properties in cascade, for example:

        cbDept.Text.Trim().Length
    

    In this expression, cbDept.Text extracts the content of the cbDept ComboBox, then the Trim() method is applied to remove leading and trailing blanks, then the Length property is applied to calculate the resulting string length.

    In case, the if statement returns true, we display a MessageBox with an error message.

        MessageBox.Show(
             "Please enter valid data in each field",
             this.Text,
             MessageBoxButtons.OK,
             MessageBoxIcon.Stop
             );
    

    Otherwise, the program runs the else part of the code.

    This is the interesting part that will call the APL+Win AddRecord function to add a new record to our Colossal File database.

    We first extract data from the form controls and store them in 4 local variables (dept, name, firstname and birthdate):

        int dept = cbDept.SelectedIndex + 1;
               string name = txtName.Text.Trim();
               string firstname = txtFirstName.Text.Trim();
               int[] birthdate = new int[]{
                   dtpBirthDate.Value.Year,
                   dtpBirthDate.Value.Month,
                   dtpBirthDate.Value.Day
                   };
    

        object[] arg = { dept, name, firstname, birthdate };
    

    This is necessary because we need to pass a nested vector to the AddRecord APL function.

    The next instruction calls the AddRecord APL+Win function, passing the arg variable as its argument:

        object result = apl.CallFn("AddRecord", arg);
    

    Here is how the AddRecord APL function is defined:

        ∇ R←AddRecord data;tie
    [1]   ⍝∇ R←AddRecord data -- Add a new record to the database
    [2]   ⍝∇                     and returns its record number
    [3]
    [4]   :if 4=⍴data
    [5]   :andif 323 82 82 323≡⎕dr¨data
    [6]   :andif 3=⍴4⊃data
    [7]       data[4]←DecodeDate data[4]  ⍝ business layer DecodeDate function
    [8]       R←(data ⎕cfappend tie←TieDb)-DbOffset
    [9]   :else
    [10]      R←'Not saved: wrong data!'
    [11]  :endif
    [12]
    [13]
        ∇
    
        ∇ R←DecodeDate date
    [1]   ⍝∇ R←DecodeDate date -- Transforms a 3-element vector (yyyy mm dd) date
    [2]   ⍝∇                      into a scalar (yyyymmdd) date
    [3]   ⍝∇ Note: Belongs to the Business Layer
    [4]
    [5]   R←100⊥∊date
        ∇
    

    Note that we have isolated the little piece of Business Logic into a separate DecodeDate function. The AddRecord function is part of the Data Layer and the DecodeDate function is part of the Business Layer.

    Why is this so important?

    Imagine that we one day we decide to change the BirthDate format in the database (currently BirthDates are saved as YYYYMMDD integers in the database). In such a case, having totally separated DecodeDate from AddRecord, we would only have to change DecodeDate and we would not have to make any change at all to our Data Layer (i.e. to the AddRecord function).

    Note that we capture the result of the AddRecord function as an object (called result) in C#: the AddRecord APL function may either return an integer representing the record number just added to the database or a character string error message.

    We must therefore check if the result variable is a string and in this case, display a MessageBox error message:

    if (result is string)
        MessageBox.Show(
            (string)result,
            this.Text, 
            MessageBoxButtons.OK,
            MessageBoxIcon.Error
            );
    

    If result is not a string, we know it is an integer and we know the record has been successfully added to the database, so we can display it in our User Interface:

    else
    {
        totalRecs = (int)result;
        DisplayRecord(totalRecs);
        bnSave.Enabled = false;
    }
    

    Our application is now done. We can test it.

  25. Click the UseAPLFromCSharp8.jpg toolbar button to test the application.

    Click the UseAPLFromCSharp9.jpg BindingNavigator button to start entering a new record.

    UseAPLFromCSharp10.jpg

    When done, click the Save button to save the record to the APL+Win Colossal File database: the form then displays the record with its record number and total number of records updated; the navigation buttons are enabled:

    UseAPLFromCSharp11.jpg

Conclusion

In this section we have developed a simple albeit complete Client+Server C# & APL+Win application.

We have seen how C# handles the whole User Interface and APL handles the database and calculations, thus forcing a perfect separation between the User Interface Layer and the Data and Business Layers.

We’ve seen how easy it is to call APL functions from C# and pass them arguments which maybe nested arrays (of any depth and complexity) and how easy it is to get results sent back from APL again as nested arrays if needed.

We have also seen how to handle errors that may occur in APL and how to display them in the C# User Interface.

As a summary here is the complete code of the APL CONF2008.W3 workspace:

    ∇ R←AddRecord data;tie
[1]   ⍝∇ R←AddRecord data -- Add a new record to the database
[2]   ⍝∇                     and returns its record number
[3]
[4]   :if 4=⍴data
[5]   :andif 323 82 82 323≡⎕dr¨data
[6]   :andif 3=⍴4⊃data
[7]       data[4]←DecodeDate data[4]  ⍝ business layer DecodeDate function
[8]       R←(data ⎕cfappend tie←TieDb)-DbOffset
[9]   :else
[10]      R←'Not saved: wrong data!'
[11]  :endif
[12]
[13]
    ∇

    ∇ BuildDb;dbName;tie;depts;Z;persons
[1]   ⍝∇ BuildDb -- Builds a small Sample Human Resources Entreprise Database
[2]
[3]   dbName←DbName
[4]
[5]   ⍝ Create database
[6]   tie←1+⌈/⎕cfnums,0
[7]   :if FileExist dbName
[8]       dbName ⎕cftie tie
[9]       dbName ⎕cferase tie
[10]  :endif
[11]  dbName ⎕cfcreate tie
[12]
[13]  ⍝ Create data
[14]  depts←'Engineering' 'Human Resources' 'Marketing' 'Sales' 'Production'
[15]  persons←10 4⍴⊂''
[16]  persons[;1]←?10⍴⍴depts
[17]  persons[;2]←'Swain' 'Atkins' 'Brooks' 'House' 'Duxler' 'Blaze' 'Glantz' 'Gregg' 'Groutsch' 'Tillman'
[18]  persons[;3]←'Rex' 'Tom' 'Steve' 'Carl' 'William' 'Joe' 'Gert' 'John' 'Jim' 'Mark'
[19]  persons[;4]←19500100+⍳10
[20]
[21]  ⍝ Populate database
[22]  Z←depts ⎕cfappend tie
[23]  Z←(⊂'')⎕cfappend¨9⍴tie
[24]  Z←(⎕split persons)⎕cfappend¨tie
    ∇

    ∇ R←DbName
[1]   ⍝∇ R←DbName - Returns the database full path name
[2]
[3]   R←'c:\aplwin\ele\conf2008.sf'
    ∇

    ∇ R←DbOffset
[1]   ⍝∇ R←DbOffset -- Returns number of component preceding first record in database
[2]
[3]   R←10
    ∇

    ∇ R←DecodeDate date
[1]   ⍝∇ R←DecodeDate date -- Transforms a 3-element vector (yyyy mm dd) date
[2]   ⍝∇                      into a scalar (yyyymmdd) date
[3]   ⍝∇ Note: Belongs to the Business Layer
[4]
[5]   R←100⊥∊date
    ∇

    ∇ R←EncodeDate date
[1]   ⍝∇ R←EncodeDate date -- Transforms a scalar (yyyymmdd) date
[2]   ⍝∇                      into a 3-element vector (yyyy mm dd)
[3]   ⍝∇ Note: Belongs to the Business Layer
[4]
[5]   R←⊂10000 100 100⊤date
    ∇

    ∇ exists←FileExist filename;h;max_path
[1]   ⍝∇ exists←FileExist filename -- Determine if a file exists
[2]
[3]   max_path←⎕wcall'W_Const' 'MAX_PATH'
[4]   h←↑⎕wcall'FindFirstFile'filename(((11×4)+max_path+14)⍴⎕tcnul)
[5]   :if exists←h≠⎕wcall'W_Const' 'INVALID_HANDLE_VALUE'
[6]       h←⎕wcall'FindClose'h
[7]   :endif
    ∇

    ∇ R←GetDepts;tie
[1]   ⍝∇ R←GetDepts -- Returns the list of all departments
[2]
[3]   tie←TieDb
[4]   R←⎕cfread tie,1
    ∇

    ∇ R←GetNumberOfRecords;tie
[1]   ⍝∇ R←GetNumberOfRecords -- Returns the total number of records in the database
[2]
[3]   tie←TieDb
[4]   R←⌊¯11+2⊃⎕cfsize tie
    ∇

    ∇ R←GetRecord number;tie;offset
[1]   ⍝∇ R←GetRecord number -- Retrieves record  from database
[2]
[3]   tie←TieDb
[4]   :if number>0
[5]   :andif number<(2⊃⎕cfsize tie)-DbOffset
[6]       R←⎕cfread tie,number+DbOffset
[7]       R[4]←EncodeDate R[4]  ⍝ business layer EncodeDate function
[8]   :else
[9]       R←'No record at this location!'
[10]  :endif
    ∇

    ∇ R←TieDb
[1]   ⍝∇ R←TieDb -- Ties the database and returns tie number
[2]
[3]   R←DbName ⎕cfstie 0
    ∇

Here is the complete code of the C# Form1.cs class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using LescasseConsulting.AplServer;

namespace APL2000.Conf2008App
{
    public partial class Form1 : Form
    {
        private AplServer apl;
        private int recordNo;
        private int totalRecs;

        public Form1()
        {
            InitializeComponent();
            apl = new AplServer(@"C:\aplwin\ele\conf2008");
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            bindingNavigator1.Enabled = true;
            cbDept.DataSource = (string[])apl.CallFn("GetDepts");
            totalRecs = (int)apl.CallFn("GetNumberOfRecords");
            DisplayRecord(1);
            EnableButtons();
        }

        private void DisplayRecord(int recno)
        {
            recordNo = recno;
            object[] record1 = (object[])apl.CallFn("GetRecord", recno);
            cbDept.SelectedIndex = (int)record1[0] - 1;  //department
            txtName.Text = (string)record1[1];           //name
            txtFirstName.Text = (string)record1[2];      //first name
            dtpBirthDate.Value = new DateTime(           //birth date
                ((int[])record1[3])[0],
                ((int[])record1[3])[1],
                ((int[])record1[3])[2]
                );
            bindingNavigatorCountItem.Text = "of " + totalRecs.ToString();
            bindingNavigatorPositionItem.Text = recordNo.ToString();
            EnableButtons();
        }

        private void EnableButtons()
        {
            bindingNavigatorMoveFirstItem.Enabled = (recordNo > 1);
            bindingNavigatorMoveLastItem.Enabled = (recordNo < totalRecs);
            bindingNavigatorMoveNextItem.Enabled = (recordNo < totalRecs);
            bindingNavigatorMovePreviousItem.Enabled = (recordNo > 1);
            bindingNavigatorCountItem.Enabled = true;
            bindingNavigatorAddNewItem.Enabled = true;
        }

        private void bindingNavigatorMoveNextItem_Click(object sender, EventArgs e)
        {
            DisplayRecord(recordNo + 1);
        }

 
        private void bindingNavigatorMovePreviousItem_Click(object sender, EventArgs e)
        {
            DisplayRecord(recordNo - 1);
        }

        private void bindingNavigatorMoveLastItem_Click(object sender, EventArgs e)
        {
            DisplayRecord(totalRecs);
        }

        private void bindingNavigatorMoveFirstItem_Click(object sender, EventArgs e)
        {
            DisplayRecord(1);
        }

        private void bindingNavigatorAddNewItem_Click(object sender, EventArgs e)
        {
            cbDept.Text = "";
            txtName.Text = "";
            txtFirstName.Text = "";
            dtpBirthDate.Value = new DateTime(1900,1,1);
            bindingNavigatorMoveFirstItem.Enabled = false;
            bindingNavigatorMoveLastItem.Enabled = false;
            bindingNavigatorMoveNextItem.Enabled = false;
            bindingNavigatorMovePreviousItem.Enabled = false;
            bindingNavigatorCountItem.Enabled = false;
            bindingNavigatorCountItem.Text = "";
            bindingNavigatorPositionItem.Text = "";
            bnSave.Enabled = true;
        }

        private void bnSave_Click(object sender, EventArgs e)
        {
            if (cbDept.Text.Trim().Length == 0
             || txtName.Text.Trim().Length == 0
             || txtFirstName.Text.Trim().Length == 0
             || dtpBirthDate.Value == new DateTime(1900, 1, 1))
                MessageBox.Show(
                    "Please enter valid data in each field",
                    this.Text,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Stop
                    );
            else
            {
                int dept = cbDept.SelectedIndex + 1;
                string name = txtName.Text.Trim();
                string firstname = txtFirstName.Text.Trim();
                int[] birthdate = new int[]{
                    dtpBirthDate.Value.Year,
                    dtpBirthDate.Value.Month,
                    dtpBirthDate.Value.Day
                    };
                object[] arg = { dept, name, firstname, birthdate };
                object result = apl.CallFn("AddRecord", arg);
                if (result is string)
                    MessageBox.Show(
                        (string)result,
                        this.Text,
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Error
                        );
                else
                {
                    totalRecs = (int)result;
                    DisplayRecord(totalRecs);
                    bnSave.Enabled = false;
                }
            }
        }
    }
}