Explanation and Examples of Non-Visual Classes

Microsoft Corporation

Abstract

Many products that support object-oriented programming only do so for the creation of graphical user interface (GUI) objects. In Microsoft® Visual FoxPro™, an entire universe of classes lies beneath the surface of the user interface, just waiting for our imagination and skill to bring them to light. These classes are the non-visual classes.

Non-Visual Classes Defined

A non-visual class is any class that is not designed primarily for display. For example, a command button is a class that is specifically designed to display on a form. A timer class, on the other hand, does not show on a form at all.

Non-visual classes in Visual FoxPro are typically descendants of the Custom or Timer classes and often will have no display component attached to them at all. However, as we will see later, non-visual classes may have a visual component attached to them.

Why Create Non-Visual Classes?

There are many reasons why a non-visual class may be created. In fact, there are as many reasons to create non-visual classes as there are to create visual ones. Principally, the reasons are:

So what's the difference between non-visual and visual classes? The basic difference lies in the kind of classes we will create. While visual classes typically center on the user interface, non-visual classes play key roles in management functions. Non-visual classes also incorporate the type of classes that are most often neglected in discussions of object orientation in systems development: business classes.

Types of Non-Visual Classes

Wrapper Classes

When we talk about classes written for management roles, there are many different aspects of management we will want to create a class for. One type of management would be to manage the interface between one program and another. A good example of this would be the management of the interface between Visual FoxPro code and DLLs, FLLs, or other function libraries. These classes are created to make it easier to use these sources' functions, to enhance their capabilities, and to encapsulate the definition, loading, and error trapping required when working with the function library.

This process is known as wrapping a class around some existing functionality. Appropriately, we call these classes wrapper classes.

Manager Classes

Another typical non-visual class would be a class that manages other classes, for example a class that handles multiple instances of forms. Such a class would allow us to create "tile all windows" functions and the like. These classes are known as manager classes.

Business Classes

A business class is a class that is designed to model an entity in a business environment. A good example would be a customer class. These classes are a combination of information and actions designed to do what a business entity needs to do within the context of the problem domain (that is, the environment being modeled and automated).

The responsibilities of a business class are often determined after careful analysis and design. Business class responsibilities can be very abstract in nature and require careful modeling before implementation. Some common responsibilities might be to:

Business classes are a little more difficult to classify as visual or non-visual. Business classes may, and often do, have visual components. Or it might be based on a visual class (for example, a form class) with the appropriate properties and methods added to the form. Which category does it belong in? It depends on how the object is created.

Let's look at a sample wrapper class first.

Wrapper Classes

The purpose of a wrapper class, as we discussed earlier, is to create a class that manages and perhaps even enhances the functionality of some other code. Any code can be wrapped into a class. If you have an old procedure library written in prior versions of FoxPro®, you could wrap a class around it if you like. The tough part is deciding when it is appropriate to wrap a class around something.

The best reason to wrap a class around something is to make it easier and better to use. A perfect example of a candidate for a wrapper class would be a .DLL or .FLL. These function libraries can be obscure, their parameters difficult to determine, and their error-handling requirements rather extensive. For example, if you are using an .FLL library, such as FOXTOOLS.FLL, what do you do if someone else's code unloads it accidentally with SET LIBRARY TO? Can you rely on the fact that the library is there all the time? Take the example of calling some of the Windows® application programming interface (API) functions, such as the functions to write and read from .INI files. These can be difficult to learn to use.

When a class is wrapped around some other piece of code, the class developer has the ability to control which portions of the .DLL or .FLL are available to the outside world, how they are called, and even what values are returned.

Wrapper classes carry a myriad of benefits with them. First of all, if a .DLL or .FLL is used with a wrapper class, the developers who use that class do not have to know anything about the .DLL or .FLL that serves as the basis for the class. They also do not have to be concerned with issues of loading the .DLL or .FLL or registering the functions contained therein.

In effect, the result is a much reduced learning curve and coding time for all concerned.

Let's look at the following example of a wrapper class. This class is a wrapper around a library of functions that ships with Visual FoxPro called FOXTOOLS.FLL.

*  Program...........: FTOOLS.PRG
*  Author............: Menachem Bazian, CPA
*  Copyright.........: (c) Flash Creative Management, Inc. 1994, 95
*  Project...........: COMMON
*  Created...........: 11/29/1994
*) Description.......: Wrapper class for FoxTools
*  Major change list.:

*-- This is a wrapper class for FoxTools. The following functions have been
*-- added as methods to this class:
*--
*-- DriveType
*-- JustPath
*-- JustDrive
*-- AddBs
*-- CleanDir
*--
*-- A couple of quick notes here:
*--
*-- JustPath has been modified to add a backslash where necessary to the return
*-- value.
*--
*-- CleanDir deals with the issue of directories specified with ..\. It returns
*-- an "adjusted" directory name.
*--
*-- In all cases when running a FoxTools function, the class will check to make 
*-- sure that FoxTools is still loaded (the user may have released on their 
*-- own). If this class loads FoxTools, it released it when the object is
*-- released.

DEFINE CLASS ftools AS custom
    PROTECTED lLoaded

    PROCEDURE init
        this.lLoaded = .F.
        this.loadlib()
    ENDPROC

    PROCEDURE destroy
        IF this.lLoaded
            RELEASE LIBRARY (SYS(2004)+"foxtools.fll")
        ENDIF
    ENDPROC

    PROCEDURE loadlib
        IF !"FOXTOOLS" $ SET("library")
            SET LIBRARY TO (SYS(2004)+"FOXTOOLS")
            this.lLoaded = .T.
        ENDIF
    ENDPROC

    FUNCTION drivetype(tcDrive)
        LOCAL lnRetVal
        lnRetVal = (drivetype(tcDrive))
        RETURN lnRetVal
    ENDFUNC

    FUNCTION justpath(tcString)
        LOCAL lcRetVal
        lcRetVal = (this.addbs(justpath(tcString)))
        RETURN lcRetVal
    ENDFUNC

    FUNCTION addbs(tcString)
           LOCAL lcRetVal
            lcRetVal = (addbs(tcString))
        RETURN lcRetVal
    ENDFUNC

    FUNCTION cleandir(tcString)
        RETURN(UPPER(sys(2027, tcString)))
    ENDFUNC

    PROCEDURE error (tnError, tcMethod, tnLine)
        LOCAL lcMessage

        tcMethod = UPPER(tcMethod)

        DO CASE
            CASE tnError = 1      && File not found 
                        && Cause by the library not loaded
                this.loadlib()
                RETRY

            OTHERWISE
                ?? CHR(7)
                lcMessage = "An error has occurred:" + CHR(13) + ;
                    "Error Number: " + PADL(tnError,5) + CHR(13) + ;
                    "      Method: " + tcMethod + CHR(13) + ;
                    " Line Number: " + PADL(tnLine,5)

                =MESSAGEBOX(lcMessage, 48, "Ftools Error")
        ENDCASE
    ENDPROC
ENDDEFINE

This class shows the various purposes of a wrapper. Let's look at each one individually:

Ease of Use: Encapsulating Error Trapping

When a library is loaded within Visual FoxPro, it can be unloaded by other objects by issuing one command. The FTOOLS class automatically loads FOXTOOLS.FLL (if it is not already loaded) when the object is instantiated. If the library is released by another module or object and a FoxTools function is called, Visual FoxPro will generate an error #1 (File Not Found). In that case, the error method calls the LoadLib method to reload the library. This provides developers with a simple way to use FoxTools without having to worry whether someone else's code unloaded the library.

Enhancing Existing Functionality

The JustPath function in FoxTools calculates what portion of a filename string is the path designation and returns that path as a string. The string may or may not have a backslash at the end. In order to promote consistency, the method that calls JustPath also calls the AddBs method to add a backslash at the end of the string if one doesn't already exist. This is an example of enhanced functionality that provides developers with a simple and consistent return value.

Adding Functionality

The CleanDir method is designed to adjust a path string for "backsteps." For example, a path string of "C:\WINAPPS\VFP\SAMPLES\DATA\...\GRAPHICS\" would adjust to "C:\WINAPPS\VFP\SAMPLES\ GRAPHICS\". This function does not call FoxTools at all; however, its functionality is related to the other functions included in this class. By adding a method for this function, we are giving developers access to related functionality in one place, without requiring them to load multiple classes.

The ability to create and use wrapper classes is a major benefit to software development. Since the complexity of working with something can be hidden within a class without compromising the functionality, developers who use the wrapper will immediately notice an increase in their productivity by having it in their arsenals, without the cost of learning its intricacies.

Manager Classes

A second type of non-visual class that is often created is a manager class. This class will typically manage instances of another class. A good example is the management of multiple instances of a form to ensure that subsequent instances are properly placed on the screen with a header that is identifiable (for example, Document1, Document2, and so on).

This next example deals with that issue and shows a manager class along with a simple form class to manage.

*  Program...........: MANAGER.PRG
*  Author............: Menachem Bazian, CPA
*  Copyright.........: (c) Flash Creative Management, Inc. 95
*  Created...........: 05/03/95
*) Description.......: Sample Manager Class with Managed Form Class
*  Major change list.:

*-- This class is designed to manage a particular form class and make
*-- sure that when the forms are run they are "tiled" properly.

DEFINE CLASS FormManager AS Custom
    DECLARE aForms[1]
    nInstance = 0

    PROCEDURE RunForm
        *-- This method runs the form. The instance of the form class
        *-- is created in the aForms[] member array.
    
        LOCAL lnFormLeft, llnFormTop, lcFormCaption
        nInstance = ALEN(THIS.aForms)

        *-- Set the Top and Left Properties to Cascade the new Form.
        IF nInstance > 1 AND TYPE('THIS.aForms[nInstance -1]') = 'O' ;
                AND NOT ISNULL(THIS.aForms[nInstance -1])
            lnFormTop = THIS.aForms[nInstance -1].Top + 20
            lnFormLeft = THIS.aForms[nInstance -1].Left + 10
        ELSE
            lnFormTop = 1
            lnFormLeft = 1
        ENDIF

        *-- Set the caption to reflect the instance number.
        lcFormCaption = "Instance " + ALLTRIM(STR(nInstance))

        *-- Instantiate the form and assign the object variable 
        *-- to the array element.

        THIS.aForms[nInstance] = CREATEOBJECT("TestForm")
        THIS.aForms[nInstance].top = lnFormTop
        THIS.aForms[nInstance].left = lnFormLeft
        THIS.aForms[nInstance].caption = lcFormCaption
        THIS.aForms[nInstance].show()
        
        *-- Redimension the array so that more instances of 
        *-- the form can be launched.
        DIMENSION THIS.aforms[nInstance + 1]
    ENDPROC
ENDDEFINE

*-- This class is a form class that is designed to work with 
*-- the manager class.

DEFINE CLASS TestForm AS form
    Top = 0
    Left = 0
    Height = 87
    Width = 294
    DoCreate = .T.
    BackColor = RGB(192,192,192)
    BorderStyle = 2
    Caption = "Form1"
    Name = "Form1"

    ADD OBJECT label1 AS label WITH ;
        FontName = "Courier New", ;
        FontSize = 30, ;
        BackStyle = 0, ;
        Caption = (time()), ;
        Height = 61, ;
        Left = 48, ;
        Top = 12, ;
        Width = 205, ;
        Name = "Label1"
ENDDEFINE

Note that forms are instantiated through the RUNFORM method rather than directly with a CREATEOBJECT function. This allows the manager class to maintain control over the objects it instantiates.

Manager classes are very useful. They allow a simple way to encapsulate code that would normally have to be duplicated every time an object is instantiated into one single place.

Business Classes

Business classes are object oriented representations of business entities (a Customer, for example). The responsibilities of these classes will vary depending on the behavior of a particular object within the problem domain.

The purpose of a business class is multifold. At an abstract level, it is possible to determine what the basic functionality of a business object would typically be and then to create a class around it. For example, the basic responsibilities of a business object might be to:

These functions could be abstracted in a class as follows:

DEFINE CLASS BaseBusiness AS custom
    cAlias = ""
    oData = .NULL.
    
    PROCEDURE INIT
        IF !EMPTY(this.cAlias) AND !USED(this.cAlias)
            =MessageBox("Alias is not open!", 16)
        ENDIF
    ENDPROC

    PROCEDURE next
        SELECT (this.cAlias)
        SKIP 1
        IF EOF()
            GO BOTTOM
        ELSE
            this.readrecord()
        ENDIF
    ENDPROC

    *-- Additional methods here for movement would mimic 
    *-- procedure NEXT.

    PROCEDURE readrecord
        *-- This procedure is initially empty.
        SCATTER NAME this.oData
    ENDPROC

    *-- Additional methods for saving would follow mimicing
    *-- procedure readrecord.

ENDDEFINE

In order to create a customer object, for example, all we would need to do is subclass it as follows:

DEFINE CLASS customer AS basebusiness
    cAlias = "Customer"
ENDDEFINE

The fields in the customer alias would be automatically added as members of oData. Thus, if an object called oCust were instantiated from the customer class, the cName field would be held in oCust.oData.cName.

Of course, the beauty of this method of development is that there is little coding to do from one business class to another. In effect, all you do is code by exception.

Conclusion

Object-oriented programming (OOP) goes well beyond the world of the graphical user interface in Visual FoxPro. Non-visual classes represent a powerful weapon in our OOP arsenal for rapid, bug-free systems development.

Acknowledgments

We acknowledge the help of Flash Creative Management, Inc., Hackensack, NJ, in providing portions of this material.