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.
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):
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...):
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:
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
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.
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)
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.
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.
Re: Passing parameters between objects
Posted: 04 Apr 2013, 04:16
by Rincevent
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)
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.
Re: Passing parameters between objects
Posted: 04 Apr 2013, 10:32
by Ursego
1: No need to call uf_get() twice - use uf_exists() to check if the parameter has been passed:
if anv_parm.uf_exists("serialNo") then ll_serialNo = anv_parm.uf_get("serialNo") end if
2: What is ipo_objects?
Re: Passing parameters between objects
Posted: 04 Apr 2013, 11:58
by Rincevent
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 ?
Re: Passing parameters between objects
Posted: 04 Apr 2013, 15:46
by Ursego
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.
Re: Passing parameters between objects
Posted: 04 Nov 2013, 22:28
by Mhaecoros3
Great object! I am wondering why it's not a built-in functionality of pb???