Tips_tutorials   >   Studio103   >   Studio 103 (All Contents)
Welcome to the
tutorial.This tutorial is provided 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.
only. If you are not currently a please go toThis tutorial continues from where we left off in the
tutorial. In this tutorial we will: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?
How does this apply to the code you write?
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.In this section we'll create an error handler object, oErrorHandler, with some basic methods.
; 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
; 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
; 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
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.
We'll test the error handler by logging an error message to it, and then asking it to prompt the last error.
; 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()
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.
; 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
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
; 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
; 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
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.
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 window.
If not(FlagOK)
Do errhndlr.$promptonceLastError()
End If
Quit method FlagOK
Do method event_evAfter Returns FlagOK
If not(FlagOK)
Do errhndlr.$promptonceLastError()
End If
Repeat the above process for all the locations where $dowork messages are being sent to the tBase table class.
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,
, 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
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:
Communicating an error from these two categories of methods is simple.
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
Ideally, only the following methods send $promptonceLastError messages to the error handler.
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 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 window. The minor error becomes the current error in the errors list, but is not written to the log file. The 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.Well that wraps up the
tutorial.I hope that this tutorial has helped you with:
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.
The doug@vencor.ca if you would like to receive it.
tutorial is not yet written. Send an email toIn the
tutorial we will continue developing the application and do the following:The StudioTips Members. The cost of membership is well worth the benefits of being a member.
tutorial is only available to . Omnis Studio developers are encouraged to becomeVisit www.studiotips.net to find out more and to become a . Your support is greatly appreciated!
Happy coding!
Doug Kuyvenhoven