Error/Message Handling for self-contained services

Tony Marston - 1st February 2001

Introduction

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:

The Problem

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:

My Solution

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:

messageobject1.gif

This is the sequence of events:

This approach has the following advantages:

messageobject1.gif

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:

  1. call SET_ERROR("1234")
  2. call SET_ERROR("1234;1=value1;2=value2;...")
  3. 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:

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.

Conclusion

This solution offers the following benefits:

Examples 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

counter