Release 10.1A: OpenEdge Development:
Progress Dynamics Advanced Development

> ## Moving client logic support to an extended class

> This chapter begins with a simple example with all client-side code in a custom super procedure for a single viewer. Realistically, much of that support code belongs in an extended viewer class, so that it can serve all viewers. This section illustrates the distinction between a single custom super procedure and a more generic procedure that serves a whole class of objects.

> | To extend the viewer class:

> 1. Copy `src/adm2/custom/viewercustom.i` and `viewercustom.p` to a local directory with the same relative path.

> 2. Edit `viewercustom.i` to uncomment the line in the Main Block that starts the super procedure `viewercustom.p,` as shown:

> 3. Move the generically useful code in your custom super procedure to your local copy of `viewercustom.p`. These are all the support functions shown so far:

> - `widgetHandle`
> - `widgetValue`
> - `widgetModified`
> - `highlightWidget`
> - `unhighlightWidget`
> 4. Move the code that sets the variables where the lists of widget names and handles are stored. In the original `customersuper.p` example, this is done in `initializeobject`. But will this still work when the code is in a super procedure that is part of an extension to the whole viewer class? No, it will not, because the new `viewercustom.p` procedure might be supporting multiple viewers at once. As a result, these lists and any other viewer attributes that the code accesses must be re-established each time a request comes from a viewer. This is the first of several basic principles to keep in mind when creating a super procedure.

> Note: When writing code for a super procedure, always consider whether it will be attached to a single other procedure, or whether it can be attached to multiple other procedures.

> In your `customersuper.p`, the super procedure would only be attached to a single instance of one specific viewer, so it was sufficient to establish needed attribute values on startup in `initializeObject`. In a more general-purpose procedure, you have to support switching the context of the object the code is supporting on each request, so that it will work properly when there is more than one object using the single super procedure instance. In this case, this means that the code to save off attribute values must go somewhere where it will be executed each time there is a request. In this case, this can be a local version of the `displayFields` procedure, since the `rowDisplay` procedure that does any and all checks on display of a new row is called from there.

> So here is a version of `displayFields` for the `viewercustom.p` procedure that gets the attribute values, saves them off, calls `rowDisplay`, and then resets the local variables so there is no danger of a stray call using stale values:

>     PROCEDURE displayFields:
>     /*-------------------------------------------------------------------------
>       Purpose:     Runs a rowDisplay procedure if there is one to allow
>                    custom logic to support any viewer.
>       Parameters:  pcColValues AS CHARACTER.
>       Notes:
>     -------------------------------------------------------------------------*/
>     DEFINE INPUT  PARAMETER pcColValues AS CHARACTER  NO-UNDO.
>     RUN SUPER (INPUT pcColValues).  /* Execute the standard display behavior. */
>     /* Establish the list of fields and handles. */
>         {get AllFieldNames   gcFields}.
>         {get AllFieldHandles gcHandles}.
>         RUN rowDisplay IN TARGET-PROCEDURE NO-ERROR.
>         ASSIGN gcFields = ""
>                gcHandles = "".
>     END PROCEDURE.

> When you create this, define the variables `gcFields` and `gcHandles` in the Definitions section of the procedure so they are scoped to the procedure. Note that because of the principle of designing super procedures to support multiple other procedures, you must do this very cautiously. Generally, it is not good practice to have any variables or other constructs scoped to the super procedure itself, because the values might not apply to the next request of the super procedure, which might come from a different object. In this case, it is something of an optimization to write the code in such a way that, within the context of a single call to `displayFields`, all references to these variables will be valid, so it is better to retrieve them once rather than in every single call to the `widgetHandle` function.

> The block of code that sets the variables and calls `rowDisplay` first checks to see whether that procedure has been implemented at all. This is always good practice when you are extending the behavior of any object or class. This could be stated as a second super procedure principle.

> Note: When extending the behavior of an object or a class of objects using a super procedure, be sensitive to making the additional behavior both optional and efficient.

> In this case, the specific `rowDisplay` for a viewer will probably be implemented in a custom super procedure for that viewer. Some viewers might have this procedure and some might not. So the behavior is made optional here because the statement to `RUN` `rowDisplay` is done `NO-ERROR` so that there is no error if it is not defined in the `TARGET-PROCEDURE`. Simply adding a `NO-ERROR` qualifier on a `RUN` statement will often be sufficient to make sure that your extension will not break the code in objects that do not use your extension.

> Note that you cannot check for `rowDisplay` by looking for it in the 4GL procedure handle attribute `TARGET-PROCEDURE:INTERNAL-ENTRIES` because this Progress attribute returns only those entry points that are actually coded in the `TARGET-PROCEDURE`, or for which there are prototypes compiled into the procedure. In this case, the `TARGET-PROCEDURE` is the viewer procedure, and `rowDisplay` will normally be implemented in its custom super procedure, so the entry point won’t be found, as shown in Figure 1–4.

> There is one more critical detail in the `RUN` statement, and that is the reference to `TARGET-PROCEDURE`. This brings up the third key point in working with super procedures.

> Note: Always remember to invoke internal procedures and functions `IN` `TARGET-PROCEDURE` from a super procedure. The same applies to `PUBLISH` and `SUBSCRIBE` statements in super procedures.

> This is one of the most common errors made when people write code for super procedures or, even worse, when people move code from an object procedure such as a static viewer to a super procedure that supports it. Code that worked fine before can simply stop working (that is, stop being executed at all) for this reason. Always keep this relationship between object and super procedure in mind.

> Figure 1–4 shows the relationships between the procedures in this case.

> Figure 1–4: Relationships between objects and super procedures

> In addition to the standard set of super procedures starting with `smart.p` and ending with `viewer.p`, the `Customer` viewer has two additional super procedures, `viewercustom.p`, which applies to the whole class, and `customersuper.p`, which applies just to the one viewer. This is the case whether the base object procedure is a static viewer such as custstviewv.w, or an instance of the generic dynamic viewer procedure rydynviewv.w. Any statement invoked `IN` `TARGET-PROCEDURE` starts at the viewer procedure itself and works its way up the stack until it finds an implementation of the entry point being run. If this entry point has a `RUN` `SUPER` statement in it, then it continues up the object’s super procedure stack looking for the next version of it.

> Note that the super procedures themselves are not super procedures of each other. So any call that wants to find an implementation of an entry point in the stack must be made `IN` `TARGET-PROCEDURE`. The `TARGET-PROCEDURE` of the customer-specific super procedure `customersuper.p` is the viewer itself. The `TARGET-PROCEDURE` of the generic `viewercustom.p` is likewise the viewer itself.

> The statement `RUN` `rowDisplay` `IN` `TARGET-PROCEDURE` attempts to run it in the viewer procedure. According to recommended coding practice, `rowDisplay` will not be found there because there is no custom code in the viewer itself, even if it is a static procedure. But the Progress interpreter then looks up the viewer’s super procedure stack and finds `rowDisplay` in `customersuper.p`, and executes it there.

> This leads to an interesting problem with this structure where 4GL functions are concerned. The Progress compiler must have a function definition for any static function reference in a procedure’s code. The only alternative to this is to use the `DYNAMIC-FUNCTION` syntax, which is rather clumsy for a model where you want the simplest possible coding style in your client logic.

> So you want to be able to reference functions like `widgetHandle` directly in code in customersuper.p, even though the functions are no longer defined there. To do this without error, you must include function prototypes of all the support functions in your super procedure.

> There is an OpenEdge tool to generate the prototypes for you, called ProtoGen. It is available from the ProTools palette, as shown in Figure 1–5.

> Figure 1–5: ProTools palette

> In the Prototype Generator, you first enter the name of the super procedure it should generate prototypes for. In this case, it is the local version of `src/adm2/custom/viewercustom.p`. You then enter the name of the include file that the tool generates containing the prototype definitions. In the example it should be called `viewcustomprto.i`, as shown in Figure 1–6.

> Figure 1–6: Prototype Generator window

> Select the Generate button to create the include file.

> In its default form, however, it is not exactly what you need. This is because the generator assumes that the include file will be added to an object procedure, such as the viewer procedure, so that functions in its super procedures can be referenced. For this reason, it creates function prototypes defined as being `IN` `SUPER`, meaning that the interpreter should invoke the function, and it will be found in a super procedure. For example:

>     FUNCTION widgetHandle RETURNS HANDLE
>       (INPUT pcWidget AS CHARACTER) IN SUPER.

> But in the present case, the function references are in another super procedure, viewercustom.p, which, as explained, is not a super procedure of customersuper.p. So the function prototypes must be modified to tell the interpreter to execute the function `IN` `TARGET-PROCEDURE`. When this happens, the interpreter searches up the stack and finds the function prototypes in another super procedure of the viewer, namely viewercustom.p, as shown:

>      /** Prototype include file: C:\ICFLocal\src\adm2\custom\viewcustomprto.i
>     * Created from procedure: C:\ICFLocal\src\adm2\custom\viewercustom.p at 13:31
>     on 06/17/02
>     * by the PROGRESS PRO*Tools Prototype Include File Generator
>     */
>     FUNCTION disableWidget RETURNS LOGICAL
>       (INPUT pcField AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION enableWidget RETURNS LOGICAL
>       (INPUT pcField AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION highlightWidget RETURNS LOGICAL
>       (INPUT pcField AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION setWidgetAttr RETURNS LOGICAL
>       (INPUT pcField AS CHARACTER,
>        INPUT pcAttr AS CHARACTER,
>        INPUT pcValue AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION unhighlightWidget RETURNS LOGICAL
>       (INPUT pcField AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION widgetHandle RETURNS HANDLE
>       (INPUT pcWidget AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION widgetModified RETURNS LOGICAL
>       (INPUT pcField AS CHARACTER) IN TARGET-PROCEDURE.
>     FUNCTION widgetValue RETURNS CHARACTER
>       (INPUT pcField AS CHARACTER) IN TARGET-PROCEDURE.

> Note: You can remove the prototype for the internal procedure `displayFields`, which is not needed. This is what the resulting include file should look like. The exact function prototypes depend on how many supporting functions you have in viewercustom.p.

> Next, modify the Definitions section of your `customersuper.p` to include the prototype file, as shown:

>     {src/adm2/custom/viewcustomprto.i}

> Now any references to the supporting functions in your custom super procedure will compile and execute successfully.

> This entire exercise of moving commonly used code into a higher level in the super procedure stack illustrates a fourth principle to keep in mind whenever you are developing an application.

> Note: Always factor out common code into the highest possible point in the super procedure stack.

> A key aspect of Progress Dynamics as an application framework is that it provides a great deal of standard behavior that can be used in all objects of a given type. Whenever you develop code of your own, try to use it to extend that standard behavior whenever possible. In a sense this is just good normal programming practice, employing the concept of modular programming. However, the Progress 4GL with its super procedures, published events, and object attributes provides a specific structure within which you can implement modular code. Whenever you write 4GL code, always consider what general use it can be, and always take the time to remove commonly useful code and move it up to a higher level in the tree. When you do this, consider the range of objects that might want to take advantage of it. Does the code apply to just a subset of viewers, for example, so that it would be worthwhile to create a new specialized viewer class for just those objects? Or does it apply to all viewers? To all `Datavis` objects, which are visual objects that display database data? To all visual objects? To all client-side objects? This same set of questions can be asked about behavior you are adding to any part of the overall SmartObject class tree. Taking the time to write code that can then be reused by many other objects is an investment that will pay off many times over. The continuation of the viewer event example below is another illustration of this.

> If you look at the UI event example, you will see that the supporting code needs further refinement. In the case of the `rowDisplay`, this is always called in the context of a single row’s display event, and putting code into `displayFields` to set and unset the local context (the values of `AllFieldNames` and `AllFieldHandles`) can safely be done there. But a UI event can occur on any event for any widget, so it is not as easy to encapsulate the context setting in such a general way.

> The code for the example event procedure `CreditLimitLeave` must be modified to set and unset the context. Because this might be happening all over the place in your code, you should extract the code out into another pair of functions implemented in viewercustom.p, which you can call `setDisplayContext` and `clearDisplayContext,` as shown in the following example:

>     FUNCTION setDisplayContext RETURNS LOGICAL
>       (  ) :
>     /*-------------------------------------------------------------------------
>       Purpose:  Establish the list of fields and handles in the current
>                 viewer.
>         Notes:
>     -------------------------------------------------------------------------*/
>       {get AllFieldNames   gcFields}.
>       {get AllFieldHandles gcHandles}.
>       RETURN TRUE.
>     END FUNCTION.

>     FUNCTION clearDisplayContext RETURNS LOGICAL
>       (  ) :
>     /*-------------------------------------------------------------------------
>       Purpose:  Clears any local storage of viewer context information.
>         Notes:
>     -------------------------------------------------------------------------*/
>       ASSIGN gcFields = ""
>              gcHandles = "".
>       RETURN TRUE.
>     END FUNCTION.

> Now the `displayFields` procedure in viewercustom.p can be cleaned up a little to use these two functions, as shown:

>     PROCEDURE displayFields:
>     /*-------------------------------------------------------------------------
>       Purpose:     Runs a rowDisplay procedure if there is one to allow
>                    custom logic to support any viewer.
>       Parameters:  pcCOlValues AS CHARACTER.
>       Notes:
>     -------------------------------------------------------------------------*/
>     DEFINE INPUT  PARAMETER pcColValues AS CHARACTER  NO-UNDO.
>       RUN SUPER (INPUT pcColValues).
>       DYNAMIC-FUNCTION ('setDisplayContext' IN TARGET-PROCEDURE).
>       RUN rowDisplay IN TARGET-PROCEDURE NO-ERROR.
>       clearDisplayContext().
>     END PROCEDURE.

> So why does the reference to `setDisplayContext` have to execute `IN` `TARGET-PROCEDURE` (which in turn requires the use of the `DYNAMIC-FUNCTION` syntax, which allows you to specify where the function is invoked), even though the `setDisplayContext` function is implemented in the same source procedure as this version of `displayFields`? It is because of another aspect in the use of `TARGET-PROCEDURE` that it is important to go over.

> In addition to using `IN` `TARGET-PROCEDURE` in code references that are written in the object the code supports, or that are written in another super procedure in the stack, it is important to remember to use `IN` `TARGET-PROCEDURE` even if the entry point your code is running is implemented in the super procedure itself, if the entry point makes another reference to the `TARGET-PROCEDURE`. This is because the `TARGET-PROCEDURE` reference in the called entry point only evaluates properly if it has bounced back from the base object itself. If the entry point is instead invoked directly in `THIS-PROCEDURE` (which is implicit if there is no qualifier on the `RUN` statement or function invocation), then within the called entry point, the value of `TARGET-PROCEDURE` is the same as `THIS-PROCEDURE`, since that is where the call originated.

> Figure 1–7 illustrates the difference between the references.

> Figure 1–7: Improper function reference without TARGET-PROCEDURE

> In contrast, the `setDisplayContext` reference in the example uses the special `{get}` pseudo-syntax to retrieve the attribute values, and this includes the necessary reference to `TARGET-PROCEDURE` to point at the object where the attribute is defined, so any reference to the function must itself be made `IN` `TARGET-PROCEDURE` for things to resolve properly.

> So with the function reference explicitly made `IN` `TARGET-PROCEDURE`, the function itself will be able to resolve it properly, as Figure 1–8 shows.

> Figure 1–8: Proper function reference with TARGET-PROCEDURE

> Now each event procedure sets and clears the context, so all references to the fields and handles within all invoked functions resolve properly without the attributes being retrieved each time, as shown:

>     PROCEDURE CreditLimitLeave:
>     /*-------------------------------------------------------------------------
>       Purpose:     Check the CreditLimit against the balance on LEAVE.
>       Parameters:  <none>
>     -------------------------------------------------------------------------*/
>       setDisplayContext().
>       IF widgetModified('CreditLimit') THEN
>       IF DECIMAL(widgetValue('CreditLimit')) -
>          DECIMAL(widgetValue('Balance')) < 5000
>            THEN highlightWidget('Balance').
>       ELSE unhighlightWidget('Balance').
>       clearDisplayContext().
>     END PROCEDURE.

> | To bring all this into sync:

> 1. Make the changes to the `viewercustom.p` procedure, save it, and compile it.

> 2. Regenerate the `viewcustomprto.i` prototype include file using the ProtoGen tool.

> 3. Edit the `viewcustomprto.i` file to change `IN` `SUPER` to `IN` `TARGET-PROCEDURE`.

> 4. Compile all the viewers that must use this support procedure. This includes any static viewers, such as `custstviewv.w` from the static example, as well as the generic dynamic viewer procedure `rydynviewv.w`.

> As noted before, the formula for the credit limit check itself, which is duplicated in `rowDisplay` and `CreditLimitLeave`, should be removed to another function where it only must be defined once.

> There is another way in which this example is simplified to the point where it does not actually set a good example: The formula that says that a customer balance within $5,000 of the credit limit represents a warning condition is really business logic, even though the way that condition is dealt with is simply to change something in the user interface. If that logic must be changed, or modified depending on the user organization or other conditions, then you have a maintenance headache to deal with because the code that expresses the condition is compiled into client user interface procedures and deployed to all the client systems.

> It is much better to bring as much of the logic as possible over from the server at run time. At a minimum, this means that the $5,000 figure should be turned into an application property that is retrieved as needed, perhaps at startup along with a whole set of other client logic properties, in a single call to a business logic procedure running on the server. In that way, the logic that maintains that figure and determines what the proper figure is for this user or this organization can be maintained on the server along with other business logic.

Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095