Passing parameters between objects


Link to this posting

Postby Ursego » 19 Feb 2013, 21:23

I would like to share with you a very simple, but useful NVO named n_parm. It mimics an associative array with a string key (usually named HashTable or Dictionary in different programming languages), and helps in the very common task of passing of a few parameters in one stroke. It supports the principle of passing inter-objects parameters by name (not by a positional index). It is useful in these situations:

1. Passing data between application units - for example, between windows. OpenWithParm and OpenUserObjectWithParm accept only one parameter to transport data; it can by of type PowerObject, so we can pass n_parm.

2. Passing a lot of arguments to a function. With n_parm, we can pass them all as a single argument (see Avoid too many arguments in functions).

Image

HOW TO ADD THE OBJECT TO THE APPLICATION

1. Save the file spy.pbl on your hard disk (in the folder where the PBLs are stored).
2. Add it to your application's library list.

That PBL contains a few objects, but now we need only n_parm.

HOW TO USE?

The sending ("packing") script adds parameters, which should be transported, to the NVO using uf_set() method which has 2 arguments: the name, used to access the parameter (the "key"), and the value.

The passed parameters are obtained by the receiving ("unpacking") script using (surprise!) uf_get() method which accepts one argument - the parameter's name.

EXAMPLES

We want to pass parameters from w_start to w_finish. Write in w_start (in the script which opens w_finish):

Code: Select all
n_parm   lnv_parm

lnv_parm.uf_set("order_id", ll_order_id)
lnv_parm.uf_set("order_date", ldt_order_date)
lnv_parm.uf_set("last_name", ls_last_name)
lnv_parm.uf_set("record_is_new", true)
lnv_parm.uf_set("first_work_day_in_month", 31)
lnv_parm.uf_set("last_companies_worked", ls_last_companies_worked[]) // array
lnv_parm.uf_set("str_address", lstr_address) // structure
lnv_parm.uf_set("n_emp", inv_emp) // NVO
lnv_parm.uf_set("cb_details", cb_details)
lnv_parm.uf_set("ds_details", ids_details)

OpenWithParm(w_finish, lnv_parm)

Open event of w_finish:

Code: Select all
n_parm   lnv_parm

lnv_parm = Message.PowerObjectParm

il_order_id = lnv_parm.uf_get("order_id")
idt_order_date = lnv_parm.uf_get("order_date")
is_last_name = lnv_parm.uf_get("last_name")
ib_record_is_new = lnv_parm.uf_get("record_is_new")
id_first_work_day_in_month = lnv_parm.uf_get("first_work_day_in_month") // runtime error! It's "integer", not "date"!
ls_last_companies_worked[] = lnv_parm.uf_get("last_companies_worked")
istr_address = lnv_parm.uf_get("str_address")
lnv_emp = lnv_parm.uf_get("n_emp")
lcb_details = lnv_parm.uf_get("cb_details")
lds_details = lnv_parm.uf_get("ds_details")

REMARKS

Parameter name can contain any characters including dashes and internal spaces (external spaces are Trim()med). So, the following names are the same: "first_name", " first_name", "first_name ", " first_name ", but the following are different: "first_name", "first name", "firstname".

Parameter name is NOT case-sensitive: "order_id", "ORDER_ID" and "Order_Id" are treated as the same parameter.

Parameter name is mandatory, it cannot be NULL or empty string.

Parameter value is NOT mandatory, it can be NULL or empty string.

Parameter name must be unique. Subsequent call of uf_set() with a same name overrides an existing parameter having that name (even if they have different data types).

Method uf_get() returns NULL (for arrays - an array with zero upper bound) if the asked parameter doesn't exist (i.e. no parameter with that name has been ever added, or its value is NULL). So, to delete a parameter from the transportation object, simply send NULL as the parameter's value (for an array, send a zero upper bound array).

If you pass an optional (i.e. not mandatory) parameter of a type inherited from PowerObject then check the parameter's existence using uf_exists() function before calling uf_get() to prevent a run-time error trying to accomodate the NULL, returned as "any", in a variable of an object type (sorry for a not elegant solution, but it's a PowerBuilder bug...):

Code: Select all
if lnv_parm.uf_exists("ds_optional") then
   lds_optional = lnv_parm.uf_get("ds_optional")
end if

The described problem exists only when the requested parameter has not been added (i.e. uf_set() was never called for it); if it was added but its value is NULL then there is no problem and uf_get() works fine, so add a null object in the packing script if you don't want to use uf_exists() in the unpacking script:

Code: Select all
DataStore lds_empty
lnv_parm.uf_set("ds_details", lds_empty)

Of course, uf_exists can also be used to check mandatory parameters:

Code: Select all
if not lnv_parm.uf_exists("ds_mandatory") then f_throw(PopulateError(0, "Parameter 'ds_mandatory' not passed.")) // f_throw: code.intfast.ca/viewtopic.php?f=2&t=1
lds_mandatory = lnv_parm.uf_get("ds_mandatory")

The class is autoinstantiated - no CREATEs and DESTROYs!

The maximum total quantity of passed parameters of all types is 65535 (the max. value of uint). There is no chance the limit will be reached in a real-life application, so the object doesn't check overflow (it would downgrade performance).

A transported parameter itself can be of type n_parm, so you can pass nested constructions of any depth.

FIELDS FOR FREQUENTLY PASSED PARAMETERS

In each application there is a set of business attributes used frequently. For example, a Human Resources application can deal a lot with emp_id and dept_id. They are transported between objects so many times that I would recommend to create public fields in n_parm to convey them - il_emp_id and il_dept_id. The generic functions uf_set() and uf_get() should be used only for one-off and rarely used parameters. The advanages of using fields are obvious - type safety, efficiency, no chance to misspell the parameter's name. But too many such fields will make the object less lightweight, so you have to decide where the borderline between frequent and rare parameters is. BTW, n_parm already has two fields created for you - ib_positive_response and is_passed_from - they will be described later.

CONSTANTS FOR PARAMETERS' NAMES

It's a good idea to create public string constants for the names of the parameters, transported using uf_set and uf_get (not custom-made fields) and use these constant with both uf_set() and uf_get() instead of hard-coded names. For example, if you have to pass somebody's country of birth then create a constant COUNTRY_OF_BIRTH and write

Code: Select all
lnv_parm.uf_set(lnv_parm.COUNTRY_OF_BIRTH, ls_country_of_birth) // source object's script
...
is_country_of_birth = anv_parm.uf_get(lnv_parm.COUNTRY_OF_BIRTH) // target object's script

instead of

Code: Select all
lnv_parm.uf_set("country of birth", ls_country_of_birth) // source object's script
...
is_country_of_birth = anv_parm.uf_get("country of birth") // target object's script


So, there never will be a situation when a source script sets "country of birth" but a target script tries to obtain "country_of_birth", "birth country" or whatever else. Populate those constants with the values which are exactly as their names (like "COUNTRY_OF_BIRTH" for COUNTRY_OF_BIRTH) - that will ensure parameters uniqueness per n_parm's instance. The constants can be created in n_parm itself (as in the given example) or in another class (read here about NVOs, created especially for constants and accessed by class name with no need to declare variables of their type).

GETTING DATA BACK FROM A RESPONSE WINDOW

Usually, response windows have a positive response button (OK or Yes) and a negative response button (Cancel or No), and the information which one of them was clicked should be passed back to the calling script. This task can be accomplished using n_parm's public field ib_positive_response.

Script of the response window's OK/Yes button:

Code: Select all
n_parm lnv_parm

lnv_parm.uf_set...
lnv_parm.uf_set...
lnv_parm.ib_positive_response = true // signal that user clicked OK/Yes
CloseWithReturn(parent, lnv_parm)

Script of the response window's Cancel/No button:

Code: Select all
n_parm lnv_parm

lnv_parm.ib_positive_response = false // signal that user clicked Cancel/No; this line is optional - ib_positive_response is false by default
CloseWithReturn(parent, lnv_parm)

Script which opens the response window:

Code: Select all
n_parm   lnv_parm

OpenWithParm(w_response, lnv_parm)
lnv_parm = Message.PowerObjectParm
if lnv_parm.ib_positive_response then
   // ...read the parameters, passed back...
end if

HOW TO UNPACK DIFFERENT SETS OF PARAMETERS

The field is_passed_from will help in situations when the unpacking script is called from different flows (objects) which send different sets of parameters. For example, the unpacking function, named uf_retrieve, retrieves data using flow-dependant criteria:
@ by Employee ID - when called from Flow 1;
@ by First Name and Last Name - when called from Flow 2;
@ by Department ID - when called from Flow 3.

Packing script of Flow 1:

Code: Select all
lnv_parm.is_passed_from = "Flow 1"
lnv_parm.uf_set("emp_id", ll_emp_id)
lds_emp = lnv_emp.uf_retrieve(lnv_parm)

Packing script of Flow 2:

Code: Select all
lnv_parm.is_passed_from = "Flow 2"
lnv_parm.uf_set("first_name", ls_first_name)
lnv_parm.uf_set("last_name", ls_last_name)
lds_emp = lnv_emp.uf_retrieve(lnv_parm)

Packing script of Flow 3:

Code: Select all
lnv_parm.is_passed_from = "Flow 3"
lnv_parm.uf_set("dept_id", ll_dept_id)
lds_emp = lnv_emp.uf_retrieve(lnv_parm)

Unpacking script (uf_retrieve) processing the different parameters' sets:

Code: Select all
choose case anv_parm.is_passed_from
case "Flow 1"
   ll_emp_id = anv_parm.uf_get("emp_id")
   // ...retrieve by Employee ID...
case "Flow 2"
   ls_first_name = anv_parm.uf_get("first_name")
   ls_last_name = anv_parm.uf_get("last_name")
   // ...retrieve by First and Last Names...
case "Flow 3"
   ll_dept_id = anv_parm.uf_get("dept_id")
   // ...retrieve by Department ID...
case else
   f_throw(PopulateError(0, "anv_parm.is_passed_from has invalid value '" + nvl(anv_parm.is_passed_from, "NULL") + "'."))))
   // f_throw(): http://code.intfast.ca/viewtopic.php?t=1
   // nvl(): http://code.intfast.ca/viewtopic.php?t=5
end choose


Functions overloading is a more elegant and object-oriented solution than choose case, but we cannot overload the unpacking function if different sets of parameters produce a same function signature. In our example, we need two overloads with the same signature uf_retrieve(long) - that is impossible. But even if that problem would not exist, overloading would not be very convenient because the number of passed parameters in a real application can be pretty large (not one or two as you see here), and the parameters' sets can change during the lifetime of the application - wanna maintain overloaded functions with 30-40 arguments? So, having one unpacking function with only one argument of the type n_parm definitely simplifies development.

* * *

As you see, uf_set() and uf_get() are used to pass parameters of all data types (internally, they are stored in an array of type any). This approach is extremely easy to use, but not type-safe: for example, you can set a parameter as long but get it into a datetime var - that will result in a run-time error. Personally, I prefer convenience of use (after all, you will get a run-time error doing a unit test), but if type safety if your religion then use a class named nvuo_parm - it has special getters and setters for parameters of different types (like uf_set_i for integer, uf_set_s for string etc.). You can download it here.
User avatar
Ursego
Site Admin
 
Posts: 143
Joined: 19 Feb 2013, 20:33

Link to this posting

Postby Rincevent » 04 Apr 2013, 04:16

Hello,

First I want to say thanks to you for sharing all the knowledge on this forum/site, excellent info well categorized and presented, very nice ;)

Ok now my 2 questions/remarks about this topic and n_parm.

1 :
Is it right to say that you should always do a isnull() test on each parameter before assigning it to a variable ?
Because if you don't do and the parameters was not set (because it was an optional param) you get an error trying to put an initialized Any variable into a specific kind of variable (let's say an Integer for example)

In other words you can't do this :
Code: Select all
ll_serialNo = n_parm.uf_get("serialNo")
if isnull(ll_serialNo) then

because you'll get an error so you have to do this :
Code: Select all
if not isnull(n_parm.uf_get("serialNo")) then
      ll_serialNo = n_parm.uf_get("serialNo")

wich bother me a little because of the 2 uf_get()

2 : Is the ipo_objects array really usefull since Objects arguments are always passed as reference ?
I mean if i do n_parm.uf_set("myWindow", This ) in a window function it seems that i put a reference to my window in an index of ia_values array and when i do the uf_get if got it back and I can assign it to a local window variable for example and i can use it like i want so it seems to me that the ipo_objects array is useless.
Rincevent
 
Posts: 2
Joined: 02 Apr 2013, 10:48

Link to this posting

Postby Ursego » 04 Apr 2013, 10:32

1:
No need to call uf_get() twice - use uf_exists() to check if the parameter has been passed:

Code: Select all
if anv_parm.uf_exists("serialNo") then
  ll_serialNo = anv_parm.uf_get("serialNo")
end if

2:
What is ipo_objects?
User avatar
Ursego
Site Admin
 
Posts: 143
Joined: 19 Feb 2013, 20:33

Link to this posting

Postby Rincevent » 04 Apr 2013, 11:58

1:
Yeah.. more or less the same thing... except one of the fct° is called uf_exists() instead testing the return of uf_get()
My concern was more "if i want to avoid an error i HAVE to test the value first, I can't get the value in a variable and then test if it's null"
And you just confirmed that.
But that's ok i can live with it.

2:
mmm... Lets replace this question by what is the real use of ib_is_object ?
Rincevent
 
Posts: 2
Joined: 02 Apr 2013, 10:48

Link to this posting

Postby Ursego » 04 Apr 2013, 15:46

1:
I most cases, the passed parameters are mandatory, so we WANT a failure if they are not passed. But if once in a while there is an optional parameter then we simply use uf_exists().

2:
We need to know if the element of ia_values[], returned by uf_get(), is of a reference or a value type, and each N-th element of ib_is_object[] stores that information for each N-th element of ia_values[]. If the returned element is not null, there is no trouble - we simply return it. But the application fails in runtime when "any" var with NULL of a type, inherited from PowerObject, is assigned to another "any" var (in our case it is the var via which the function physically returns the value; it is stored on the stack - please don't confuse it with the variable which accommodates the returned value in the calling script!), so we return a variable of type PowerObject (even thou pointing nothing) preventing the problematic assignment between "any" vars. Please have a look at the code of uf_get() to see that.
User avatar
Ursego
Site Admin
 
Posts: 143
Joined: 19 Feb 2013, 20:33

Link to this posting

Postby Mhaecoros3 » 04 Nov 2013, 22:28

Great object! I am wondering why it's not a built-in functionality of pb???
Mhaecoros3
 
Posts: 1
Joined: 01 Nov 2013, 19:34




Ketones are a more high-octane fuel for your brain than glucose. Become a biohacker and upgrade yourself to version 2.0!



cron
Traffic Counter

eXTReMe Tracker