Tips_tutorials   >   Studio103   >   Studio 103 (All Contents)

Introduction

Welcome to the Studio 103 tutorial.

This tutorial is provided to StudioTips Members only. If you are not currently a StudioTips Member please go to www.studiotips.net and click the link provided for becoming a StudioTips Member. The cost of membership is minimal and helps to cover the time and expense of creating these tutorials and maintaining the studiotips.net website.

This tutorial continues from where we left off in the Studio 102 tutorial. In this tutorial we will:

  1. Discuss the differences between visual and non-visual classes.
  2. Create an error handler object for logging and prompt errors.
  3. Write code for visual and non-visual classes that uses the error handler object.

Visual vs. Non-Visual Classes

It took me a few years of writing code in Omnis Studio and reading numerous books on object-oriented programming before I grasped the concept of visual vs. non-visual classes and understood how that applied to the code I was writing in Omnis Studio.

What are visual classes? They are classes which the user can see in the display.

What are non-visual classes? They are classes which the user can not see in the display.

Why be concerned about visual vs. non-visual classes when you write code?

  1. Properly written non-visual code is much easier to reuse.
  2. Non-visual code can be used by web applications. You can not reuse visual class code in web applications.
  3. By paying attention to visual vs. non-visual classes when writing code you can write better code, reduce the lines of code, and do a better job of handling and reporting errors. Good error reporting becomes very important when trying to fix runtime errors.

How does this apply to the code you write?

  1. Non-visual class method code never prompts the user. Whether it is a prompt for input, an OK message, or an error message, the non-visual class code is forbidden to prompt or directly interact with the user in any way.
  2. Only visual class methods are permitted to interact with the user.

Error Handler

I don't think it is possible to write non-visual code without an error handler object.

In the Studio 101 and 102 tutorial we created table classes. The table classes are non-visual objects, however whenever we hit and error in the table class methods we prompted the user with an OK message. Really what choice did we have? There was no process for passing an error back to the visual window class method which sent the $getAllRecords or $dowork message to the table class.

How can we pass an error message back to a visual class method from a non-visual class method?

The technique which I use is to create an error handler object. When a non-visual method hits an error, it logs the error with the error handler object, and then returns false (or null if a return value is expected) to the sender. If the sender is another non-visual object is also returns false (or null if a return value is expected) to its sender. Eventually the visual class method which started the thread is going to get a return value which indicates an error occurred. The visual class method then sends a message to the error handler asking it to prompt the user with the last error that was logged.

The error handler hold the logged error messages in a list and could also write the error to a log file, or a table in the database. For this tutorial we are simply going to store the errors in a list. You can add the code to write the error to a log file or the database on your own.

Once concern with this style of error handling is how to avoid prompting the user multiple times with the same error message. If one visual class method, calls another visual class method, which in turn calls a non-visual class method, and an error is logged by the non-visual class method, both visual class methods will (and should) attempt to prompt the user with the error message. This problem is solved by adding a prompted boolean column to the errors list. The first time the error is prompted, prompted is set to kTrue. Subsequent requests to prompt the current error are ignored by the error handler.

Create Error Handler Object

In this section we'll create an error handler object, oErrorHandler, with some basic methods.

  1. Create a new object class in the Contacts library.
  2. Name the object, oErrorHandler.
  3. Double-click the oErrorHandler class to edit its methods.
  4. Select the $construct method and enter the following code.

    ; Define the errors list.
    Do List.$cols.$add('libname',kCharacter,kSimplechar,100)
    Do List.$cols.$add('classname',kCharacter,kSimplechar,100)
    Do List.$cols.$add('methodname',kCharacter,kSimplechar,100)
    Do List.$cols.$add('prompted',kBoolean)
    Do List.$cols.$add('datetime',kDate,kDatetime)
    Do List.$cols.$add('errormssg',kCharacter,kSimplechar,1000000)

    Calculate iErrorsList as List

    Quit method kTrue

  5. Right-click the Class methods and select Insert New Method.
  6. Name the method, $logError.
  7. Add the following parameters to the $logError method

    Variable - prcmethod (This is a reference to the method which logs the error.)
    Type - Item reference

    Variable - pMssg
    Type - Character
    Subtype - 1000000
  8. Enter the following code in the $logError method.

    ; Add the error to the errors list.
    Do iErrorsList.$add(prcmethod.$lib().$name,prcmethod.$class().$name,prcmethod().$name,kFalse,#D,pMssg)

    ; Set the last line in the list to be the current line.
    Do iErrorsList.$line.$assign($ref.$linecount)

    Quit method kTrue

  9. Right-click the Class methods and select Insert New Method.
  10. Name the method $promptonceLastError.
  11. Enter the following code in the $promptonceLastError method.

    ; Only open the prompt if this is the first time the error is being prompted.
    If iErrorsList.prompted=kFalse
       
       ; Set the prompted flag to true to avoid multiple prompts of the same error message.
       Calculate iErrorsList.prompted as kTrue
       
       ; Open an OK prompt message.
       Calculate Mssg as iErrorsList.errormssg
       Calculate Method as con(iErrorsList.libname,'/',iErrorsList.classname,'/',iErrorsList.methodname)
       OK message Error (Icon) {[Mssg]////Logged by://[Method]}
       
    End If

Instantiate the Error Handler

We need to instantiate the error handler object with task wide scope so that all the classes and methods in the Contacts application can send messages to the error handler object. The Contacts library Startup_Task task variables have task wide scope.

  1. Double-click the Startup_Task class in the Contacts library.
  2. Add a variable to the Task tab of the variables section of the method editor as follows:

    Variable - errhndlr
    Type - Object
    Subtype - oErrorHandler
  3. Close the method editor window.
  4. Close and reopen the Startup_Task to instantiate the oErrorHandler object class with the errhndlr task variable.

Test the Error Handler

We'll test the error handler by logging an error message to it, and then asking it to prompt the last error.

  1. Enter the following code in the Programmer Test Method.

    ; Log an error.
    Do errhndlr.$logError($cmethod,'Test error message')

    ; Prompt the last error.
    Do errhndlr.$promptonceLastError()

    ; Attempt to prompt the same error again.
    Do errhndlr.$promptonceLastError()

  2. Select Contacts menu > Programmer Test Method and step through the test code.

    All going well the error handler will prompt the error message on the first request and ignore the second request.

Modify the Non-Visual tBase Methods

Now we need to remove our OK messages in the non-visual tBase table class and replace them with code that logs the errors with the error handler.

  1. Double-click the tBase table class to get at the class method.
  2. Select the $:PrimaryKeyColName method.
  3. Modify the method code as follows.

    ; Assume that the first column in the schema or query class is the primary key.
    Calculate ColName as $cinst.$cols.1.$name

    ; If the column name does not include the suffix '_pkey' report an error and set the colname variable to null.
    If pos('_pkey',low(ColName))=0
       Calculate Mssg as con("Unable to find the primary key column in the ",$cinst.$sqlclassname," SQL class.")
       Do errhndlr.$logError($cmethod,Mssg)
       Calculate ColName as #NULL
    End If

    Quit method ColName

  4. Select the $dowork method.
  5. Modify the last part of the method code as follows.

    If FlagOK
       
       If pos(',',$cinst.$servertablenames)
          
          Calculate Mssg as con("Unable to execute $dowork on a query class that has columns from more than one table.")
          Do errhndlr.$logError($cmethod,Mssg)
          Calculate FlagOK as kFalse
          
       Else
          ; Do the built-in default $dowork method.
          Do default Returns FlagOK
          If not(FlagOK)
             Calculate Mssg as "Flag false after running the default $dowork method."
             Do errhndlr.$logError($cmethod,Mssg)
          End If
       End If
    End If

    Quit method FlagOK

  6. Select the $getAllRecords method.
  7. Modify the method code as follows.

    ; Prepare the ORDER BY text.
    If len(pOrderBySQL)
       Calculate OrderBy as pOrderBySQL
    Else
       Calculate OrderBy as $cinst.$:DefaultOrderBy
    End If

    ; Prepare the SQL text to exclude the empty zero(0) primary key record.
    Calculate ColName as $cinst.$:PrimaryKeyColName
    If len(ColName)=0
       Calculate FlagOK as kFalse
    Else
       
       If pos("WHERE ",$cinst.$extraquerytext)
          Calculate SQLText as con("AND ",ColName," <> 0")
       Else
          Calculate SQLText as con("WHERE ",ColName," <> 0")
       End If
       
       Calculate SQLText as con(SQLText,' ',OrderBy)
       
       ; Select all the records in the table.
       Do $cinst.$select(SQLText) Returns FlagOK
       If not(FlagOK)
          Calculate Mssg as con("Flag false after $cinst.$select(",SQLText,") for the $sqlclassname ",$cinst.$sqlclassname,".")
          Do $ctask.errhndlr.$logError($cmethod,Mssg)
       Else
          
          ; Fetch all the records in the table.
          Do $cinst.$fetch(kFetchAll) Returns FetchStatus
          If not(FetchStatus)
             Calculate FlagOK as kFalse
             Calculate Mssg as con("Flag false after $cinst.$fetch(kFetchAll) for the $sqlclassname ",$cinst.$sqlclassname,".")
             Do $ctask.errhndlr.$logError($cmethod,Mssg)
          Else
             
             ; Set the current line to the first line.
             Do $cinst.$line.$assign(1)
             
          End If
       End If
    End If
    Quit method FlagOK

  8. Select the $setPrimaryKey method.
  9. Modify the method code as follows.

    ; Check if the primary key column is null and not zero.
    Calculate ColName as $cinst.$:PrimaryKeyColName
    If len(ColName)=0
       Calculate FlagOK as kFalse
    Else
       If isnull($cinst.[ColName])|$cinst.[ColName]=0
          Calculate FlagOK as kTrue
       Else
          
          ; Get a new statement object from this table instance's session object.
          Do $cinst.$sessionobject().$newstatement() Returns StmntObj
          
          ; Select and fetch the maximum primary key column value from the table's records.
          Calculate TableName as $cinst.$:BaseTableName
          If len(TableName)=0
             Calculate FlagOK as kFalse
          Else
             
             Calculate SQLText as con("SELECT MAX(",ColName,") FROM ",TableName)
             Do StmntObj.$execdirect(SQLText) Returns FlagOK
             If not(FlagOK)
                Calculate Mssg as con("SQL error when issuing the following SQL Text: ",SQLText," for the $sqlclassname ",$cinst.$sqlclassname,".")
                Do $ctask.errhndlr.$logError($cmethod,Mssg)
             Else
                
                Do StmntObj.$fetchinto(MaxPKey) Returns FetchStatus
                If FetchStatus=kFetchError
                   Calculate FlagOK as kFalse
                   Calculate Mssg as con("SQL error when fetching the max pkey after issuing the following SQL Text: ",SQLText," for the $sqlclassname ",$cinst.$sqlclassname,".")
                   Do $ctask.errhndlr.$logError($cmethod,Mssg)
                Else
                   
                   ; Set the primary key column to the maximum primary key value plus one.
                   Calculate $cinst.[ColName] as MaxPKey+1
                End If
             End If
          End If
       End If
    End If
    Quit method FlagOK

Modify the Visual Methods

We also have to find any visual class methods which send message to tBase and add code that will prompt the user if an error is returned to the visual class method.

Note

There is something important to note here! One of the guidelines which I follow for error handling is that only public methods of visual classes prompt the user with error messages. Private methods log errors but simply return the flag to the sender. This reduces the number of methods which need to check for flag false and send a $promptonceLastError.

The easiest way to find all the locations where messages are being sent to the tBase table class is to use the Omnis Studio Find and Replace window.

  1. Ctrl/Cmnd+F to open the Omnis Studio Find and Replace window.
  2. Enter .$getAllRecords in the Find entry field. By including the . period character the find will be restricted to the Do ListName.$getAllRecords() method lines.
  3. Select the Class tab in the Find and Replace window.
  4. Select the Contacts library in the left list.
  5. Select a class in the right list and then press Ctrl/Cmnd+A to select all the class in the Contacts library.
  6. Click the Find All button. Methods which we are going to need to modify will appear in the Find and Replace list.
  7. Double-click the first line in the list. This takes you to the wCountryList.$construct method.
  8. Modify the end of the method so that it sends a $promptonceLastError message to the error handler if the flag is false.

    If not(FlagOK)
       Do errhndlr.$promptonceLastError()
    End If
    Quit method FlagOK

  9. Close the method editor window to return to the Find and Replace window.
  10. The second line in the list is the wStateprovList.buildLists method. Because this is a private method we do not need to add $promptonceLastError to the end of the method.
  11. The fourth line in the list is the wTowncityList.buildLists method. Because this is a private method we do not need to add $promptonceLastError to the end of the method.
  12. The last line in the list is the wTowncityList.StateProvName.event_evAfter method. Because this is a private method we do not need to add $promptonceLastError to the end of the method, however we do need to modify the public $event method of the wTowncityList.StateProvName field which calls the private event_evAfter method.
  13. Select the $event method of the StateProvName field.
  14. Modify the On evAfter code in the $event method as follows:

    Do method event_evAfter Returns FlagOK
    If not(FlagOK)
       Do errhndlr.$promptonceLastError()
    End If



    The $event method is not called by another method, so there is no need to include Quit method FlagOK at the end of an $event method.
  15. Close the method editor window to return to the Find and Replace window.

Repeat the above process for all the locations where $dowork messages are being sent to the tBase table class.

  1. Ctrl/Cmnd+F to open the Omnis Studio Find and Replace window.
  2. Enter .$dowork in the Find entry field. By including the . period character the find will be restricted to the Do ListName.$dowork() method lines.
  3. Select the Class tab in the Find and Replace window.
  4. Select the Contacts library in the left list.
  5. Select a class in the right list and then press Ctrl/Cmnd+A to select all the class in the Contacts library.
  6. Click the Find All button. Methods which we are going to need to modify will appear in the Find and Replace list.
  7. For each of the listed public visual class methods modify the code to test for flag false and send a $promptonceLastError message to the error handler if the flag is false.
  8. In each of the listed private visual class methods modify the $event method which calls the private method to test for flag false and send a $promptonceLastError message to the error handler if the flag is false.

Error Handling Code Guidelines

Using an error handler affects how you write your code. This section gives you some guidelines to follow for writing code when using an error handler.

Single Exit Point

Adding the $promptonceLastError code to the visual class methods was relatively easy because the methods were written with a single exit point at the end of the method.

For the first few years of writing code in Omnis Studio I wrote methods with multiple exit points. Steve McConnell in his excellent book, Code Complete, pointed out that code with multiple exit points is difficult to read and statistically has a greater number of errors. I switched to writing methods which have a single exit point at the end of the method and have to agree with Steve's findings.

Here is a sample of how I used to write code with multiple exits points.

; Check the incoming parameter
If pReportList.$linecount=0
   Calculate Mssg as "The report list is empty, unable to print the report."
   Do $ctask.errhndler.$logError($cmethod,Mssg)
   Quit method kFalse
End If

; Check the records in the list.
For pReportList.$line from 1 to pReportList.$linecount step 1
   
   Do method checkData (pReportList) Returns FlagOK
   If not(FlagOK)
      ; The called method will have logged an error.
      Quit method kFalse
   End If
   
End For

; Find the report class for printing the report.
Do $clib.$reports.$findname(pReportClassName) Returns rClass
If isnull(rClass)
   Calculate Mssg as con("Unable to find the specified report class, ",pReportClassName,".")
   Do $ctask.errhndler.$logError($cmethod,Mssg)
   Quit method kFalse
End If

; If we get this far we can print the report.
Set report name [pReportClassName]
Print report {(pReportList)}
Quit method kTrue

Here is a sample of how I now write code with a single exit points.

; Note: The FlagOK parameter init value is always set to kFalse.

; Check the incoming parameter
If pReportList.$linecount=0
   Calculate Mssg as "The report list is empty, unable to print the report."
Else
   
   ; Preset the flag to true before the loop.
   Calculate FlagOK as kTrue
   
   ; Check for any inactive records from the list.
   For pReportList.$line from 1 to pReportList.$linecount step 1
      
      Do method checkData (pReportList) Returns FlagOK
      If not(FlagOK)
         ; The called method will have logged an error.
         Break to end of loop
      End If
   End For
   
   If FlagOK
      
      ; Find the report class for printing the report.
      Do $clib.$reports.$findname(pReportClassName) Returns rClass
      If isnull(rClass)
         Calculate Mssg as con("Unable to find the specified report class, ",pReportClassName,".")
         Do $ctask.errhndler.$logError($cmethod,Mssg)
         Calculate FlagOK as kFalse
      Else
         
         ; If we get this far we can print the report.
         Set report name [pReportClassName]
         Print report {(pReportList)}
         
      End If
   End If
End If
Quit method FlagOK

Debugging methods which have a single exit point at the end of the method is much easier. You can always place a breakpoint at the end of the method, let the method execute, and check the return flag or value at the end of the method to see if it is correct.

Always Return Something

Always return something from a method! If the method is so simple that it can never have an error end the method with Quit method kTrue anyway.

When it come to return value I have two categories of methods:

  1. Methods that return a flag (true or false)
  2. Methods that return a value. (string, number, item reference, list, or row)

Communicating an error from these two categories of methods is simple.

  1. Methods that return a flag return kFalse if there is an error.
  2. Methods that return a value return #NULL if there is an error.

Testing Return Values

Testing the return value from methods that return a flag (true or false) is easy. Depending on the situation you test for If FlagOK (good) or If not(FlagOK) (error).

Do method checkData (List) Returns FlagOK
If FlagOK
   
   Do method saveData (List) Returns FlagOK
   
End If
Quit method FlagOK

Testing the return value from a method that returns a value is normally easy. Depending on the situation you test for If not(isnull(RetValue)) (good) or If isnull(RetValue) (error).

Do method retClassRef (ClassName) Returns rClass
If isnull(rClass)
   Calculate FlagOK as kFalse
Else
   
   Do method openClassInstance (rClass) Returns rInst
   If isnull(rInst)
      Calculate FlagOK as kFalse
   Else
      Calculate FlagOK as kTrue
   End If
End If
Quit method FlagOK

Testing for isnull() does not work for list and row variables. For lists or rows you must check the $colcount rather than isnull(). If List.$colcount>0 (good), or List.$colcount=0 (error).

; Testing a list or row for isnull() does NOT work.
Do method retDefinedList (SQLClassName) Returns List
If isnull(List)
   Calculate FlagOK as kFalse
Else
   Do List.$getAllRecords() Returns FlagOK
End If
Quit method FlagOK

; Testing a list or row for $colcount does work.
Do method retDefinedList (SQLClassName) Returns List
If List.$colcount=0
   Calculate FlagOK as kFalse
Else
   Do List.$getAllRecords() Returns FlagOK
End If
Quit method FlagOK

When to Log an Error

This is a very simple rule. The method that first detects an error, logs the error.

When to Prompt an Error

Ideally, only the following methods send $promptonceLastError messages to the error handler.

  1. $construct and $destruct methods of the Startup_Task.
  2. $event method of any visual object.
  3. $construct method of any visual object.
  4. $construct or $print method of a report class.
  5. $timer method of a timer object class.
  6. $construct method of an object class or a table class (try to avoid these).
The above methods are generally at the top of the methods stack because these methods are called by Omnis Studio. If we diligently test the return value in the above methods and send a $promptonceLastError to the error handler in the event of an error condition from the above method, all other methods in our application simply need to return something and theoretically never issue a $promptonceLastError.

What is an Error?

For each method you write, carefully decide what is an error, and what is not an error.

Originally when I wrote the $getAllRecords table class method I returned false if no records were fetched. Later as I wrote more application code I ran into situations where a table might not have any records and in that situation it was not an error.

Now I was in trouble, I had to return false because other methods were depending on false to indicate no records were fetched, but for the empty table, this really was not an error.

Eventually I came to the conclusion that for table class methods, fetching zero records should not be considered an error. Only SQL errors and check data errors would be classified as errors. It would be up to the sender method to check the List.$linecount and decide whether or not zero fetched records was a problem for them.

Always specify in the method description the return value and error conditions.

In the StudioWorks framework we eventually added a special class of errors, called minor errors, with an accompanying $logMinorError method. The $logMinorError method adds the minor error to the errors list in RAM, but does not write the error to the log file.

A situation where we would log a minor error is when the non-visual oLogon object is asked by the visual Sign-In window to $logon to the database, but the user has entered the wrong user name or password. The non-visual oLogon object sends a $logMinorError message to the error handler and returns false to the Sign-In window. The minor error becomes the current error in the errors list, but is not written to the log file. The Sign-In window sends a $promptonceLastError message to the error handler and the error handler opens a prompt window with the current error. The prompt window notifies the user that they have entered the wrong user name or password.

In the StudioWorks framework we also added an errortype property to the errors list. The errortype determines the type of error prompt window the user sees. If the errortype is minor the prompt window title doesn't use the word Error, and the error message information does not include the source information of the method which logged the error.

There is a lot you can do to extend the oErrorHandler object which we have built in this tutorial.

Summary

Well that wraps up the Studio 103 tutorial.

I hope that this tutorial has helped you with:

  1. Understanding the difference between visual and non-visual classes.
  2. Seeing the importance of using an error handler object.
  3. Writing non-visual class code that has a single exit point at the end of the method, always returns something to the sender, and never prompts the user.

If this tutorial has been helpful please send an emai to doug@vencor.ca. It's always encouraging to hear from developers who have benefited from these tutorials. Be sure to include any suggestions for improvements or additional topics you would like to see covered.

Warning

The Studio 104 tutorial is not yet written. Send an email to doug@vencor.ca if you would like to receive it.

In the Studio 104 tutorial we will continue developing the Contacts application and do the following:

The Studio 104 tutorial is only available to StudioTips Members. Omnis Studio developers are encouraged to become StudioTips Members. The cost of membership is well worth the benefits of being a member.

Visit www.studiotips.net to find out more and to become a StudioTips Member. Your support is greatly appreciated!

Happy coding!

Doug Kuyvenhoven
Vencor Software