Overblog Folge diesem Blog
Edit post Administration Create my blog

Calling a form and passing a context

20. Februar 2009 , Geschrieben von Andreas Weiden Veröffentlicht in #Forms

The task

One common task in a forms-application is call one form form another one and give some context-information to that new form when calling it. In some cases you also want to get a kind of "return-value" when the called form is finished to that you can react on it. There are several solutions for this task i will describe here:

Test-case

For the examples i take a simple application consisting of two forms-modules, both based on the EMPLOYEES-tables form the HR-demo-schema. The first form, i call it OVERVIEW is a tabular form where you can query data from the employees-table, but cannot change anything. Beneath the table there are two buttons, one called "Edit" and one called "New". Both will call the second form i call EDIT_EMP which can either be used to edit an existing record form the EMPLOYEES-table, or to create a new employee. What action should be possible in the second form is given as context from the OVERVIEW-form.
In detail,

  • when pressing the Edit-button i want to give the EMPLOYEE_ID from the selected record to the EDIT_EMP, along with the information to edit that record
  • when pressing the New-button i want to call EDIT_EMP with a blank record which enables the user to enter data for a new employee.

In the examples i will deal only with CALL_FORM (for first). There are some issues with OPEN_FORM which are similar (or equal) but also some which may differ.

Use parameters to pass a context

One approach of passing a context from one OVERVIEW to EDIT_EMP is to use Parameters.
To do that create two parameters in the Object-Navigator in EDIT_EMP:

  • P_EMPLOYEE_ID with datatype Number
  • P_MODE with datatype Char

To give a context from OVERVIEW to EDIT_EMP you now have to create a parameter-list, fill in the appropiate parameters and values and pass the parameters when calling the EDIT_EMP.

Here the example-code for the WHEN-BUTTON-PRESSED-trigger on the Edit-button in OVERVIEW:

DECLARE
  pl PARAMLIST:=GET_PARAMETER_LIST('PL_EDIT_EMP');
BEGIN
  -- Delete list if existing
  IF NOT ID_NULL(pl) THEN
    DESTROY_PARAMETER_LIST(pl);
  END IF;
  -- Create new
  pl:=CREATE_PARAMETER_LIST('PL_EDIT_EMP');
  -- Add parameter for Mode
  ADD_PARAMETER(pl, 'P_MODE', TEXT_PARAMETER, 'EDIT');
  -- Add parameter for EMPLOYEE_ID
  ADD_PARAMETER(pl, 'P_EMPLOYEE_ID', TEXT_PARAMETER, :EMP.EMPLOYEE_ID);
  -- Now call the second form, giving the parameter-list
  CALL_FORM('EDIT_EMP',NO_HIDE,DO_REPLACE,NO_QUERY_ONLY,pl);
END;


And now in EDIT_EMP you have to "take over" the parameters and react on them. A good trigger to do so is the WHEN-NEW-FORM-INSTANCE-trigger. Heres the code that will react on the Edit-Mode and query the appropiate record.

IF :PARAMETER.P_MODE='EDIT' THEN
  -- go to the employee-block
  GO_BLOCK('EMP');
  -- execute the query (the block has a WHERE-condition using EMPLOYEE_ID=:PARAMETER.P_EMPLOYEE_ID)
  EXECUTE_QUERY;
  -- Restrict createing new records
  SET_BLOCK_PROPERTY('EMP', INSERT_ALLOWED, PROPERTY_FALSE);
END IF;


Important here is the WHERE-condition which is set at the EMP-block to restrict the query to the parameter-value.
Thats it for the parameter-approach.

Pro's:

  • You can see in the Module-definition of the called form which parameters it accepts.
  • The values of the parameters are specific to the called "instance" of the form, so if one form is called more than one time, each form can have it's own values

Con's:

  •  Parameters are one-way, means you can pass parameters when from the calling form to the called form, but you cannot return values back to the called form.

Using globals to pass a context

With this approach you make use of the Global-variable-concept in forms to pass a context. Globals are not defined at design-time, but implicitly at runtime when you first assign a value to them or use the Built-In DEFAULT_VALUE.

Lets do the same thing as before using Globals:

Heres the WHEN-BUTTON-PRESSED-trigger on the Edit-Button:

-- Define global for Mode
:GLOBAL.MODE:='EDIT';
-- Define global for Employee-Id
:GLOBAL.EMPLOYEE_ID:=:EMP.EMPLOYEE_ID;
-- Now call the second form
CALL_FORM('EDIT_EMP',NO_HIDE,DO_REPLACE,NO_QUERY_ONLY);


And also, you take over the context in the WHEN-NEW-FORM-INSTANCE-trigger of EDIT_EMP:

-- Make sure the Gloabls exist
DEFAULT_VALUE(NULL, 'GLOBAL.MODE');
DEFAULT_VALUE(NULL, 'GLOBAL.EMPLOYEE_ID');
IF :GLOBAL.MODE='EDIT' THEN
  -- go to the employee-block
  GO_BLOCK('EMP');
  -- execute the query (the block has a WHERE-condition using EMPLOYEE_ID=:GLOBAL.EMPLOYEE_ID)
  EXECUTE_QUERY;
  -- Restrict creating new records
  SET_BLOCK_PROPERTY('EMP', INSERT_ALLOWED, PROPERTY_FALSE);
END IF;



Again, the WHERE-condition at the block EMP is important to be set.
So far, the Global-approach is quite similar to the parameter-approach.

Pro's:

  • Globals can be seen by every module in a forms-session, so you can also use them to return a value from the called form back to the calling form

Con's:

  • Globals are not defined declarative so you have to look into the code
  • Globals can be seen by every module in a forms-session, so if one form is called more than one time, the second call will overwrite the globals from the first call.

Returning values from a called form

As returning a value from a clalled dialog is a common requirement, lets enhance the example and return a flag from the EDIT_EMP to indicate if the user has changed the edited employee, so that we can do a requery in that case to reflect the changes in the overview.

First, we implement the WHEN-BUTTON-PRESSED-trigger on the save-button in EDIT_EMP:

IF :SYSTEM.FORM_STATUS='CHANGED' THEN
  -- save changes, if needed
  DO_KEY('COMMIT_FORM');
  -- set "saved"-flag
  :GLOBAL.USER_HAS_SAVED:='TRUE';
  -- exit
  EXIT_FORM;
END IF;


And also the WHEN-BUTTOn-PRESSED-trigger on the cancel-button:

-- reset saved-flag
:GLOBAL.USER_HAS_SAVED:='FALSE';
-- exit without saving
EXIT_FORM(NO_VALIDATE);


Last we have to adjust the Edit-button code to react on the return-value

-- Define global for Mode
:GLOBAL.MODE:='EDIT';
-- Define global for Employee-Id
:GLOBAL.EMPLOYEE_ID:=:EMP.EMPLOYEE_ID;
-- Now call the second form
CALL_FORM('EDIT_EMP1',NO_HIDE,DO_REPLACE,NO_QUERY_ONLY);
-- Make sure the global exists
DEFAULT_VALUE('FALSE', 'GLOBAL.USER_HAS_SAVED');
-- Check retur value
IF :GLOBAL.USER_HAS_SAVED='TRUE' THEN
  -- Requery data
  GO_BLOCK('EMP');
  EXECUTE_QUERY;
END IF;

 

Encapsulating the logic

Both approaches have pros and cons and both have in common the same problems when a form is called from more than one other form:

  • The names of the parameters/globals must match on both sides.
  • The passed values must match on both sides
  • When enhancing the called form its sometimes hard to find all calling forms to adjust the call

So i took the following approach to "hide the complexity" and make it quite straight-forward to do the communication:

  1. Create a pll to contain all communication logic.
  2. For each form that can be called, create a package in it
  3. Inside each package, create the following functions/procedures
  • a procedure to call the form, having all the parameters needed to call as IN-parameters, or
  • a function to call the form, having all the parameters needed to call as IN-parameters and returning the result
  • a procedure for retrieving the callers parameters from the package to the called form.
  • a procedure to set the return-value from the called form into the package.
  • All communication with the called form in done exclusively using this package
  • In case there are different modes or variants to call a form, there may also be different "calling"-procedures/functions.

 

This approach has some (in my eyes big) advantages:

  • The way the communication between caller and calling-form is implemented is hidden and doesn't really matter to the caller
  • All usage of Global-names/parameternames is only done in this package, so the chance of having typos in it is quite small
  • Parameters are given with its "real" datatype and can even be checked before calling the form.
  • If the signature to call a form must change the signature of the package has to be changed, and with a recompile it's quite eays to find modules using the changed module, for they will no longer compile


So, lets change the example to match this approach:

First, heres the code for the package:

PACKAGE PK_EDIT_EMP IS

  -- Call the dialog to edit the employee with the given ID
  -- function return TRUE if user changed the Employee, else FALSE
  FUNCTION FK_EDIT_EMPLOYEE(i_nEmployeeId IN NUMBER)
  RETURN BOOLEAN;

  -- retrieve the callersparameters
  -- o_vcMode will be EDIT for editing NEW for creating a new Employee
  -- o_nEmployeeId will give the Id of the employee to be queried when o_vcMode is EDIT
  PROCEDURE PR_GET_PARAMETERS(o_vcMode      OUT VARCHAR2,
                              o_nEmployeeId OUT NUMBER);

  -- Sets the return-value                            
  PROCEDURE PR_SET_RETURN(i_bSaved IN BOOLEAN);  

END;

PACKAGE BODY PK_EDIT_EMP IS
 
  VCC_GLOBAL_MODE   VARCHAR2(30):='GLOBAL.EDIT_EMP_MODE';
  VCC_GLOBAL_EMPID  VARCHAR2(30):='GLOBAL.EDIT_EMP_EMPID';
  VCC_GLOBAL_RETURN VARCHAR2(30):='GLOBAL.EDIT_EMP_RETURN';
 
  FUNCTION FK_EDIT_EMPLOYEE(i_nEmployeeId IN NUMBER)
  RETURN BOOLEAN IS
  BEGIN
   -- Set the globals
    COPY('FALSE', VCC_GLOBAL_RETURN);
    COPY('EDIT', VCC_GLOBAL_MODE);
    COPY(TO_CHAR(i_nEmployeeId), VCC_GLOBAL_EMPID);
    -- call the form
    CALL_FORM('EDIT_EMP1',NO_HIDE,DO_REPLACE,NO_QUERY_ONLY);
    -- return result
    RETURN (NAME_IN(VCC_GLOBAL_RETURN)='TRUE');
  END;  

  -- ----------------------------------------------------------------
 
  PROCEDURE PR_GET_PARAMETERS(o_vcMode      OUT VARCHAR2,
                              o_nEmployeeId OUT NUMBER) IS
  BEGIN
    -- transfer globals to out-params
    o_vcMode:=NAME_IN(VCC_GLOBAL_MODE);
    o_nEmployeeId:=TO_NUMBER(NAME_IN(VCC_GLOBAL_EMPID));
  END;

  -- ----------------------------------------------------------------

  PROCEDURE PR_SET_RETURN(i_bSaved IN BOOLEAN) IS
  BEGIN
    -- map boolean to Char-result
    IF i_bSaved THEN
      COPY('TRUE', VCC_GLOBAL_RETURN);
    ELSE
      COPY('FALSE', VCC_GLOBAL_RETURN);
    END IF;
  END;

END;


Now the WHEN-BUTTON-PRESSED-trigger in OVERVIEW:

IF PK_EDIT_EMP.FK_EDIT_EMPLOYEE(:EMP.EMPLOYEE_ID) THEN
  -- Requery data
  GO_BLOCK('EMP');
  EXECUTE_QUERY;
END IF;


The WHEN-NEW-FORM-INSTANCE-trigger in EDIT_EMP:

DECLARE
  vcMode      VARCHAR2(4);
  nEmployeeId NUMBER;
BEGIN
  PK_EDIT_EMP.PR_GET_PARAMETERS(vcMode, nEmployeeId);
  -- to avoid using the globals directly in the form, created a item 
  -- in y control-block
  -- and restrict the query  on that
  :BL_CONTROL.IT_EMPLOYEE_ID:=nEmployeeId;
  -- go to the employee-block
  GO_BLOCK('EMP');
  -- execute the query (the block has a 
  -- WHERE-condition using EMPLOYEE_ID=:BL_CONTROL.IT_EMPLOYEE_ID
  EXECUTE_QUERY;
  -- Restrict createing new records
  SET_BLOCK_PROPERTY('EMP', INSERT_ALLOWED, PROPERTY_FALSE);
END;



Note that for avoiding to "break" the encapsulation i created a control-block with an item to store the employee_id in.

And last, the two button "Save"

IF :SYSTEM.FORM_STATUS='CHANGED' THEN
  DO_KEY('COMMIT_FORM');
  PK_EDIT_EMP.PR_SET_RETURN(TRUE);
  EXIT_FORM;
END IF;


and "Cancel"

PK_EDIT_EMP.PR_SET_RETURN(FALSE);
EXIT_FORM(NO_VALIDATE);

Diesen Post teilen

Repost 0

Kommentiere diesen Post