Thursday 26 April 2007

JavaScript: How to tell if any form fields have been modified

The following function can be used to determine if a form passed as parameter has been modified, returning true or false respectively. You may use it on submit events in order to avoid unnecessary post actions or like I do in other page links in order to warn the user of pending changes.

I have tested this with Firefox 2.0.0.3, but will post results for other browsers as well.

    /**
     * Check if a form has changed
     */    
    function formModified(aForm)
    {                
        for (var i=0; i < aForm.elements.length; i++) {
            // get the form current element
            var curElement = aForm.elements[i];
            // get the current element type            
            var myType = curElement.type;
            // assume that the form is modified
            var valueChanged = true;
            
            if (myType == 'checkbox' || myType == 'radio') {
                // if both the checked state and the DefaultCheck state are equal
                if (curElement.checked == curElement.defaultChecked) {
                    valueChanged = false
                }                     
            } else if (myType == 'hidden' || myType == 'password' || myType == 'text' || myType == 'textarea') {
                // if the current text is the same as the default text i.e. the original in the
                // HTML Document
                if (curElement.value == curElement.defaultValue) {
                    valueChanged = false
                }
            } else if (myType == 'select-one' || myType == 'select-multiple') {
                for (var j=0;  j < curElement.options.length; j++) {
                    if (curElement.options[j].selected && curElement.options[j].defaultSelected) {
                        valueChanged = false
                    }
                }
            } else
                // unhandled type assume no change
                valueChanged = false;
                    
            // the previously checked element has changed
            if (valueChanged)
                return true;
        }

        // all elements have their default value
        return false;
    }

In order to warn the user of pending changes you would have to write something like :

    /**
     *  returns true if a user can leave a page based on wheither the
     *  aform parameter is committed.
     */
    function canLeavePage(aForm)
    {
        // if no changes were made in the form
        if (formModified(aForm) == false)
            return true;

        // else ask user
        return confirm('Form data have been modified.\n' +
                       ' Are you sure you want to leave this page ?') ;
    }

To use this in your Web page, name your form with something like myForm or InputForm and place anywhere in the page a link like :

              <a href="myPage.php"
                onclick="return canLeavePage(InputForm);"/>
                <img src="images/exit.png"
                    border="0"
                    align="middle"
                    hspace="2"
                    vspace="2"
                    title="Exit"/>
                </a>

Enjoy!

Monday 23 April 2007

PHP: Directory Listing

Here is a snipper I use to get the contents of a directory. I have many times searched the WEB -- and my files -- for similar functionality so I thought I might put it here to save me the the next searches. The PHP manual for opendir that contains similar code can be found here.

/**
 * returns an associative array containing the file list of the $directory parameter
 * $fileList[$file_name]contains the size of the file or 0 if file_name is a irectory
 */
function fileList($disrectory)
{
   $files = array();
   if (!is_dir($directory))
      return $files;

   if (false == ($d = @opendir($directory)))
     return NULL;

   while (false != ($f = readdir($d))) {
      $fullName = "$directory/$f";
      $files[$f] = is_file($fullName) ? filesize($fullName) : 0;
   }
   closedir($d);

   return $files;
}

Tuesday 17 April 2007

SAP via Delphi Re-executing the same SAP function

After running the examples from BAPI / RFC with Delphi, I run into the following problem: Each time the Button2Click function was called the grid would show the results already displayed plus any data returned from SAP after the current call. A little bit of debugging showed that the table returned from the SAPFunctions object in statement Table := Funct.tables.item('DATA'), was the one that would not get initialized prior to Funct.call(). Aa I have no clues regarding the available methods of the table or the SAPFunctions object that would allow me to clear the table data before each call, I ended up rewriting the example performing the following changes.

  1. Create one connection object and retain it throughout the entire program session during FormCreate.
  2. Create and destroy a SAPFunctions object at each invocation of the ExecuteButtonClick() function. The way the table returned always has new data returned from SAP. This has been the only way I could avoid the problem of not initializing the returned table. Since I use similar code to perform data transfers between SAP and an other Oracle database at user defined intervals , this optin was more or less a one way.
The actual code of the get cost centers example now looks like this :
unit MainUni;

interface

uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, OleCtrls, SAPLogonCtrl_TLB, Grids,
 StdCtrls, ComCtrls, ExtCtrls, Buttons;

type
 TFormMain = class(TForm)
   PanelLogin: TPanel;
   StatusBar: TStatusBar;
   ButtonExec: TButton;
   Grid: TStringGrid;
   SAPLogonControl: TSAPLogonControl;
   BitBtnClose: TBitBtn;
   procedure ButtonExecClick(Sender: TObject);
   procedure FormCreate(Sender: TObject);
 private
   { Private declarations }
   Table, Funct, Connection : VARIANT ;
 public
   { Public declarations }
 end;

var
 FormMain: TFormMain;

implementation
{$R *.dfm}

Uses
  SAPFunctionsOCX_TLB;

procedure TFormMain.FormCreate(Sender: TObject);
  begin
     (* create a new connection object to be used
      * for all sessions
      *)
     Connection                  := SAPLogonControl.newConnection;
     Connection.System           := 'R3Q';
     Connection.Client           := '100';
     Connection.ApplicationServer:= 'sapqa_prd.shelman.int';
     Connection.SystemNumber     := '00';
     Connection.Language         := 'EN' ;
  end;

procedure TFormMain.ButtonExecClick(Sender: TObject);
  var
     txt : String;
     r : integer;
     SAPFunctions: TSAPFunctions;
  begin
     SAPFunctions := nil;

     // parameter "true" = SilentLogOn
     if Connection.LogOn(0, false) = true then
        try
           (* Create a new SAPFunctions obejct and
            * assign the existing connection to it
            *)
           SAPFunctions := TSAPFunctions.Create(Self);
           SAPFunctions.Connection := Connection;

           Funct := SAPFunctions.add('RFC_READ_TABLE');
           Funct.exports('QUERY_TABLE').value := 'CSKT';

           // attempt to call
           if not Funct.call then
              // called failed display the error in the statusbar
              StatusBar.SimpleText := Funct.Exception
           else begin
              // Call is successfull display returned data
              Table := Funct.tables.item('DATA');
              grid.rowCount := Table.rowcount + 1;
              grid.cells[0,0] := 'RecNo';
              grid.cells[1,0] := 'Client';
              grid.cells[2,0] := 'CostCent-No';
              grid.cells[3,0] := 'CostCent-Des.';
              for r := 1 to grid.rowCount -1 do begin
                 txt := Table.value(r,1);
                 grid.cells[0,r] := IntToStr(r);
                 grid.cells[1,r] := copy(txt,0,3);
                 grid.cells[2,r] := copy(txt,9,10);
                 grid.cells[3,r] := copy(txt,27,20);
              end;
           end;
        finally
           // close connection
           Connection.LogOff;
           // get rid of the SAPFunctions Object
           SAPFunctions.Free;
        end
     else
        StatusBar.SimpleText := 'Unable to login';
  end;

end.

Wednesday 11 April 2007

BAPI / RFC with Delphi 6

A sad story with a happy end.

I wanted to write a small application that would update some Oracle tables based on data read from SAP using Delphi 6. Very soon I realized that it all came down to installing the various ActiveX components required for Delphi to connect to SAP, be able to execute functions and read the returned results.

And this is where the sad part of the story begins. Looking around at various sources I could find some instructions explaining how to perform the required installation but none of them seemed to be accurate enough or straightforward enough in order to produce the desired results. In my opinion the problem occurs because our German colleagues who provided the "how-to's" tried to translate the menus from their German Delphi to English and that caused the misunderstanding. Additionally, while following the directions provided I also run into compilations errors increasing my frustration. The following article provides my how to install SAP ActiveX controls on Delphi 6 and I hope to make it as clear as possible.

I have to admit that I owe everything to the guy who wrote and maintained the BAPI/RFC Programming with Delphi Page and to my good friend Marilena who found the page for me. The page has enough examples required in order for the average Delphi programmer to understand how to program the necessary communication. An other page that gives information regarding the pre-installation steps is this one found at SDN.

So here is the list of the steps I followed in order to install SAP ActiveX controls on my version of Delphi 6 If you are using Delphi 2006 then it you might beter have a look here.

  1. Create an additional package page to host the new components.
    1. Right click on the Delphi palette → Properties → Click the Add button.
    2. Type a name for the new page SAP will do just fine
    3. Use the Move Up and Move Down buttons to position the page at the correct position. Caution : Since the page will initially be empty it will not appear on the Delphi palette.
  2. Create an additional directory to hold the units for the new controls. In my case I created : C:\Program Files\Borland\Delphi6\Imports\SAPControls\.
  3. Go to main menu Component → Import ActiveX Control
    1. Select the SAP Logon Control Version 1.1 (wdtlog.OCX). The displayed class name should be TSAPLogonControl
    2. Select SAP as the Palette Page
    3. Change the directory of the unit file name to the new path created in the previous step.
    4. Click install. The first time select to create a new package, name it SAPControls and put it the path we are already using. The controls we will install next will be added to the package as well.
    5. Perform the same steps this time installing the TSAPBapiControl class from SAP BABPI Control Version 1.2
  4. Go to main menu Project → Import Type Library.
    1. Select SAP Remote Function Call Controll (Version 5.0). This will provide you with the TSAPFunctions, TFunction, TParameter, TExports, TImports, TStructure clases
    2. Click install and choose the SAPCOntrols package created previously.
  5. Press compile to compile the project. At this point I got many errors, but I will show you how to resolve them.
Many errors appear in functions with code like the following :
procedure TStructure.Set_Value(index: OleVariant; Param2: OleVariant);
 begin
   DefaultInterface.Set_Value(index) := Param2;
 end;
This should be corrected like this :
procedure TStructure.Set_Value(index: OleVariant; Param2: OleVariant);
 begin
   // DefaultInterface.Set_Value(index) := Param2;
   DefaultInterface.Value[index] := Param2;
 end;
The general error pattern is that there are unknown identifiers for functions of the form Set_XXX(i). These should be changed to indexed properties like XXX[i] Additionally I have had many functions not returning a value. The worst part is that the Delphi complier did not issue any warnings for some of them, so I had to look at each one separately. The worst case was the NewConnection method of the TSAPLogonControl class. The generated code was
function  TSAPLogonControl.NewConnection: IDispatch;
 begin
   DefaultInterface.NewConnection;
 end;
and it had to be corrected to
function  TSAPLogonControl.NewConnection: IDispatch;
 begin
   // had to add Result :=
   Result := DefaultInterface.NewConnection;
 end;
Failure to correct this results to all new Connections being returned null afterwards, making connections to R3 practically impossible. After you correct all the functions with warnings, you should be finished. Just press "Install" on the package dialog and you 're done The examples in BAPI RFC with Delphi page are good and they may get you started immediately. If I ever get any examples of my own I will post them in this page for future reference.