In a traditional 2 tier environment where the processing of business rules is contained within the presentation layer it is possible for a field or entity trigger to contain code something like the following:
if (condition) $1 = value message $text(1234) $prompt = "field.entity" return(-1) endif
This contains the following elements:
message
part will display a message in the message area of the user's screen.$text(1234)
part will obtain the text of the message from the dynamic object library (DOL) at run time using the identity of 1234
coupled with the current values of $variation
and $language
.$1 = value
loads a value into a predefined area so that it can be substituted in the message text at the point designated by "%%$1". Note that any field or variable name can be used - I am just using $1 as an example.$prompt = "field.entity"
part forces the cursor to be placed on a specific field when reporting this error, otherwise it could appear on a field that is unrelated to the error message.The problem starts when a form (or server page) calls a separate self-contained service component to perform this validation. Neither the $text
or $prompt
function is available in self-contained components so the details must be passed to a component where they are available.
The obvious solution would be to include all these variables in the argument list for the operation on the service component so that they can be returned to the form component for processing. However, when I first encountered this problem this particular solution was not available to me. I was attempting to use Object Services in 7.2.04 to convert my architecture from 2 tier to 3 tier, and those of you who have looked at object services will realise that their signatures are fixed and cannot be expanded to include extra arguments.
Other options I considered were:
postmessage
to send a message to the form component - this will only work if the form includes its return address among the arguments passed to the service, but as indicated earlier it is not possible to alter the signature of object services to include extra arguments. Another problem with this idea is that it would push the messages to the form component one at a time when in fact it is the form component that should initiate a pull of all the messages only when it is ready to process them.If the messages cannot be passed back directly from the service to the form then an alternative option would be to do it indirectly via an intermediate component. This intermediate component (or Message Object) should hold on to all messages until they are requested by the form component. This can be shown in the following diagram:
This is the sequence of events:
This approach has the following advantages:
You should also notice that messages generated within SERVICEn do not have to pass through SERVICE2 and SERVICE1 in order to get back to the form.
In my implementation I made the message object a detached instance so that it is created once then permanently available throughout the session until the application terminates. This avoids the overhead of creating a new instance for each message. It also means that the messages can be held within the structure of the message object and need not be written to or retrieved from the database. Any messages held by the message object are cleared out as soon as they have been passed back to the form component.
This is the operation which I created within the message object to receive the message:
operation WRITE ; write a message to the message log params string pi_MsgType : in string pi_MsgString : in string pi_MsgData : in endparams if (pi_MsgString = "") return(0) creocc "SESS_MSG_LOG",-1 seq_no = $curocc(SESS_MSG_LOG) msg_date = $date msg_time = $clock msg_type = pi_MsgType msg_string = pi_MsgString msg_data = pi_MsgData return(0) end WRITE
I created an include proc called SET_ERROR which contains code similar to the following:
params string pi_MessageText : in endparams variables string lv_MsgData endvariables putitem/id lv_MsgData, "ComponentName", $componentname putitem/id lv_MsgData, "InstanceName", $instancename putitem/id lv_MsgData, "EntName", $entname putitem/id lv_MsgData, "FieldName", $fieldname activate "MSG_OBJ".WRITE("E",pi_MessageText,lv_Msgdata) if ($procerror) putmess "FATAL ERROR - Unable to activate Message Object" return(<FATAL_ERROR>) endif return($status)
When a service wishes to generate an error message it uses code similar to the following:
call SET_ERROR("1234")
call SET_ERROR("1234;1=value1;2=value2;...")
call SET_ERROR("1234;1=value1;2=value2;$prompt=fieldname")
Line 1 is the simplest format.
Line 2 is for when the message text contains places where values are substituted when the $text
statement is processed.
Line 3 is for when the message requires the cursor to be placed on a particular field.
As is usual with associative lists the ;
represents <GOLD>semi-colon.
Here is an example of the code that I now have in all my <on error> triggers:
call SET_ERROR($dataerrorcontext) return(-1)
As I have 4 types of message (fatal, error, warning and information) I have a separate proc for each one. This is so the message type does not have to be specified as an additional argument within the call
statement. It is easier to code (and easier to spot when you are reading the code) to have the message type built into the proc name. Well, that's my theory and I'm sticking to it. Note that the proc also includes some useful details such as $componentname
, $entname
and $fieldname
in the arguments that it passes to the message object.
So much for getting the messages into the message object. The next stage is to get the messages out and process them. For this I created a global proc called GET_MESSAGE which is called in the form immediately after each call to a service. These are the steps it goes through:
$status = 0
.$componentname
, $entname
and $fieldname
from MsgData.$prompt=fieldname
string and that fieldname exists within the current component then set $prompt accordingly (but only the first $prompt
within each activation).$text
then extract substitution values (see line 2 above) into $1 - $5, then obtain the full text from the DOL file.$procerrorcontext
and $dataerrorcontext
then extract each part and write it to the message frame on a separate line.Here is an example of the output produced by this arrangement:
Message from component MNU_0010L, instance MNU_0010L, entity MNU_SECURITY, field SEC_CLASS_ID ERROR=-1123 MNEM=<UPROCERR_NPARAMETERS> DESCRIPTION=Wrong number of parameters COMPONENT=MNU_0010L PROCNAME=REFRESH_CHILDREN TRIGGER=OGF LINE=25 ADDITIONAL: COMPONENTNAME=MNU_0010C INSTANCENAME=MNU_0010C OPERATIONNAME=EXEC **************************************** Message from component MNU_0010L, instance MNU_0010L, entity MNU_SECURITY, field SEC_CLASS_ID 90023: PROC ERROR - see message frame for details
As you can see the end result is that all the logic for dealing with messages is concentrated in two components - the Message Object which receives and holds messages, and the GET_MESSAGE proc which displays the results to the user. This means that global changes can be made simply by amending just one of these components. One possibility, for example, would be to write all error messages to a log file, or in a production environment generate an e-mail message for each FATAL error. Could you achieve this in your environment just by changing one component?
A rather esoteric ability that this mechanism made easy was in dealing with conditions which may be an error in some circumstances, but not in others. For example, in my menu system every form component must have an entry defined in the menu database and must be included in the user's access profile. Fields in a form may be populated by running a popup form, which is a type of picklist. Popup forms are not required in access profiles because access to a form implies automatic access to any popups required by that form. However, part of my popup processing includes a lookup on the menu database to see if any runtime attributes have been defined. This is optional, so it does not matter if a record does not exist on the menu database. The code for checking the contents of the menu database is inside a common routine used for activating both forms and popups, which means that the activation of a popup could result in both of the following error messages:
For form components these are genuine errors, but for popups they are irrelevant. So how do I stop them from being displayed? Put some extra code in the Message Object? In the GET_MESSAGE proc? One way would be to have a separate activation procedure for popups which would be a duplicate of that for forms, but with the exclusion of the code which generates these error messages. Having duplicate code is something I wish to avoid, so After scratching my head for a few moments the answer came to me in a flash. Use the same code, allow the error messages to be generated, but take them out before they are given to the form component! I achieved this by creating a new operation in my Message Object which would locate specified messages and delete them. It is invoked with proc code similar to the following:
call IGNORE_MESSAGE("M_91003;M_91001")
Thus I can use the same routine to activate popups that I use for forms, and simply tell the message object to ignore those errors that do not apply. Anything left over at this point will be a genuine error and will be displayed as normal.
This solution offers the following benefits:
$text
statementExamples of this code in action can be found in my demonstration application which can be downloaded from my Building Blocks page.
Tony Marston
1st February 2001
mailto:tony@tonymarston.net
mailto:TonyMarston@hotmail.com
http://www.tonymarston.net