Understanding Visual DataFlex

  • Language Guide

  • The Application Framework

  • Data Dictionary Guide

  • Classes and Objects

  • Learning from the Examples


Application Framework

The Application Framework

This section explains how to write Visual DataFlex programs using an organized, methodical approach known as the Application Framework. It also covers additional concepts that are useful and important to this methodology. The Application Framework is designed to help you use Visual DataFlex to do what you want—quickly, reliably, and easily.

The Studio builds applications according to the rules defined by the Application Framework method. In fact, it does this so well that it is really not required that you understand what the Application Framework method is or why it works as it does. Reading and understanding this section is, therefore, not required to write programs in Visual DataFlex. While not required, the information provided should make it easy to customize and debug your applications.

While simple samples are provided in this section, you are encouraged to study the example applications provided with Visual DataFlex. In addition, any program you create using the Studio will create Application Framework-style programs for you. After creating a complex multi-view panel, you may wish to study the generated code.

  • Object Packages

  • High Level Object Components

  • Views

  • Creating a View

  • Lookup Lists

  • Other Components in Windows Applications

  • Web Objects and Web Applications

  • Framework Concepts

  • Putting together a View


Object Packages

An object package is a package that creates an object. As you start to assemble an application, you will find that you are placing a group of objects together. A normal application will need an error object, a help object, a confirm object, and a number of view objects. You will usually only need one of each of these objects. The object packages allow you to pick the objects you need and place them in your program with use statements. It is our goal that object packages be so independent that they can be written and tested stand-alone and then included in any program with the knowledge that they will work with any combination of other object packages. In order for this to work, object packages must be completely encapsulated and must have a simple and well-defined interface. Package objects will very often reside at the desktop level. Package objects often need to be available to all other objects in a program. Some of these objects can be placed at the desktop making them available to all objects. Other objects (usually visual ones) are placed within your main panel object.

Some object packages are provided with Visual DataFlex (the help, error, and confirm object packages). Most object packages you will create yourself. Typically, you will create list-object packages and view-object packages. When you create an object package, you want to place everything in that package required to make it work. Because of this, an object package will often use class packages and other object packages. The use command does not re-include any package that has previously been included in the program. You might have five views that all need the same list-object package and data-server class package. You would include these packages in all views. When the views are compiled and tested stand-alone, all packages will get included and your test program will run. When the five views are added to a single program, a those packages that were included in more than one of the test programs will only get included once in the final program.

It is very important that object packages provide a simple, defined access method to their objects. In many cases, there will only be one way to access a package object. Lookup-list object packages provide a single access message, the message popup (Send popup to oMy_List ). This should be the only message you ever send to the lookup list. All you need to know is the message name and the object name. This allows the programmer in charge of the object (the "owner" of the object package) to make any changes needed in the object.

Unlike class packages, it is not expected that you can extend object packages without changing the package's code. It is also not expected that the provided object packages will act exactly as you wish. For example, the confirm package (DfConfrm.pkg) displays a number of prompts whose text may not be to your liking. You should feel free to create your own object packages (using the provided object packages as a template). You will not, however, want to change the package's access methods (e.g., the save-a-record confirmation must always be accessed with the save_confirmation message).


High Level Object Components

A Framework Application consists of a main container object, which contains a number of high level component object packages. In a web application, the main object is a web application container. All of the high level web object components are placed in this container. In a windows application, the main container is actually two objects; a visual panel object and a child client-area object. All of your high level windows object components are placed inside of the client-area object. In both types of applications, these main container objects are quite simple and require little customization. The High Level Components are the objects that make an application unique and this is where you will spend your development time.

High Level Components in Windows Applications

Most of the high level object components in a Windows Application are visual. Views are used to view, edit and maintain data. Report Views are used to set up and run reports. Lookup Lists are used to display database data for lookup purposes. Business Process components, a non-visual object, are used to handle update processing.

High Level Components in Web Applications

Web Applications contain two types of web-object components: Web Business Process objects and Web Service Objects. Web Business Process Objects are used to handle processing for web browser requests. Web Service Objects are use to handle processing for web service requests.

Common Characteristics of all High Level Components

The high level components all operate using the same underlying principles. Those are:

  • They are created as separate source components.

  • They can be built, tested and maintained individually.

  • Once working they can be added to one or more applications in a building block fashion.

  • They all use the same rules for encapsulation.

  • They use the same methods for placing and accessing objects.

  • They all use Data-Dictionaries to access and maintain their data

  • The same Data-Dictionary classes are used by all components.

  • The rules for assembling Data-Dictionary Objects into a structure (which is the basis of the Framework) is the same in all components.

  • The interface used to access Data-Dictionaries is the same in all components.

  • They all designed to be developed within the Visual DataFlex studio. This is important. The Studio applies many of the concepts and rules discussed in this Framework section automatically. The Studio will help head you down the proper path and make the development process faster and more reliable.

Because of these shared characteristics, once you learn the rules for creating one high level component, you can apply these rules to all high level components. This document will focus primarily one type of component - the View component. Once you understand how a View works, you will be able to easily apply this understanding to all of the other component types.


Views

A view is a specialized object package. It is a stand-alone data-entry unit. An example of a view might be a customer entry form or an order entry form. Views are used in Windows Applications.

As more views get added to a single program, the complexity of the program will increase. If the views are allowed to interact with other views, it will become increasingly difficult to manage and maintain a large program. By making each view independent of other views, you will be able to control this complexity. A large program will merely be a collection of small programs (views).

We must be able to create and test views by themselves. When we eventually add them to the larger program, we can do so with the knowledge that the new view will not affect or be affected by other views in the program.

A view will often consist of other object packages and class packages. For example, a view will usually contain a data dictionary object structure. Each DDO defined in the structure will need a data-dictionary class and those classes are included in the view component with the use statement. In addition views will often need several object packages for lookup lists, and an object package for confirmations.

Views can be coded and tested in an object package file. The conventional file extension for view object packages is .vw.

Once a view is completed, it may be added to a program through a simple selection process within the Studio. Even if you were coding this manually, this only requires a few lines of code in a main program. This, along with a view's independence, makes it quite easy to add and remove views from a program.

The following sample shows a windows application main program. This program consists of a main menu and a series of use commands that includes the required views. Notice that the entire program is quite small.

Use DfAllEnt.pkg

Use dfPanel.pkg

Use DfAClnt.pkg

Use cHtmlHelp.pkg

Object oHtmlHelp is a cHtmlHelp

End_Object

Object oApplication is a cApplication

Set psCompany to "Data Access Worldwide"

Set psProduct to "Visual DataFlex Examples"

Set psVersion to "12.0"

Set psProgram to "Order"

Set psHelpFile to "Examples.chm"

Set peHelpType to htHtmlHelp

End_Object // oApplication

Object oMain is a Panel

Set Label to "Order Entry Sample Application"

Set Location to 2 35

Set Size to 127 209

DFCreate_Menu Main_Menu

#Include File_PM.inc

DFCreate_Menu "&View" ViewPopupMenu is a ViewPopupMenu

On_Item "&Customer Entry View \aCtrl+1" Send Activate_oCustomerView

On_Item "&Inventory Item View \aCtrl+2" Send Activate_oInventoryView

On_Item "&Order Entry \aCtrl+3" Send Activate_oOrderEntryView

On_Item "&Sales Person Entry View \aCtrl+4" Send Activate_oSalesPersonView

On_Item "&Vendor Entry View \aCtrl+5" Send Activate_oVendorView

End_Pull_Down

Set Status_Help to "Available Views"

DFCreate_Menu "&Report" ReportPopupMenu is a ViewPopupMenu

On_Item "&Basic Report - Customer List \aCtrl+6" Send Activate_oCustomerListBR

On_Item "&Crystal - Address Labels \aCtrl+7" Send Activate_oAddressLabelsCR

On_Item "&Crystal - Customer List \aCtrl+8" Send Activate_oCustomerListCR

On_Item "&Crystal - Orders \aCtrl+9" Send Activate_oOrdersCR

On_Item "&Crystal - Orders by Customer" Send Activate_oOrdersByCustomerCR

On_Item "&Crystal - Report Information" Send Activate_oReportInformationCR

On_Item "&Crystal - Report Preview" Send Activate_oReportPreviewCR

On_Item "&WinPrint - Customer List" Send Activate_oCustomerListWP

On_Item "&WinPrint - Items per Order" Send Activate_oItemsPerOrderWP

On_Item "&WinPrint - Orders" Send Activate_oOrdersWP

On_Item "&WinPrint - Orders by Customer" Send Activate_oOrdersByCustomerWP

End_Pull_Down

Set Status_Help to "Available Reports"

#Include Navi_PM.inc

#Include Win_PM.inc

#Include HelpA_PM.inc

End_Menu

Use OrderToolbar.pkg // Tool-Bar object.

Object oClientArea is a AppClientArea

Use Customer.vw

Use Invt.vw

Use Order.vw

Use SalesP.vw

Use Vendor.vw

Use Basic\CustomerListBR.rv

Use Crystal\AddressLabelsCR.rv

Use Crystal\CustomerListCR.rv

Use Crystal\OrdersCR.rv

Use Crystal\OrdersByCustomerCR.rv

Use Crystal\ReportInformationCR.rv

Use Crystal\ReportPreviewCR.rv

Use WinPrint\CustomerListWP.rv

Use WinPrint\ItemsPerOrderWP.rv

Use WinPrint\OrdersWP.rv

Use WinPrint\OrdersByCustomerWP.rv

On_Key Key_Ctrl+Key_1 Send Activate_oCustomerView

On_Key Key_Ctrl+Key_2 Send Activate_oInventoryView

On_Key Key_Ctrl+Key_3 Send Activate_oOrderEntryView

On_Key Key_Ctrl+Key_4 Send Activate_oSalesPersonView

On_Key Key_Ctrl+Key_5 Send Activate_oVendorView

On_Key Key_Ctrl+Key_6 Send Activate_oCustomerListBR

On_Key Key_Ctrl+Key_7 Send Activate_oAddressLabelsCR

On_Key Key_Ctrl+Key_8 Send Activate_oCustomerListCR

On_Key Key_Ctrl+Key_9 Send Activate_oOrdersCR

End_Object

Use DefaultStatusbar.pkg // Status-Bar object.

Use DfAbout.pkg

Object oAbout is An AboutDialog

Set ProductName to "Order Sample Application"

Set Copyright to "Copyright: 2006 Data Access Corporation"

Set Author to "Author: Data Access Worldwide"

End_Object

Procedure Activate_About

Send Popup_Modal of oAbout

End_Procedure

End_Object

// open Order Entry view on application startup

Send Activate_oOrderEntryView of (oClientArea(oMain))

Start_UI


Creating a View

The rules for creating a view are actually quite simple. Create a container client. Inside the container object, place all of your data-dictionary objects (DDOs). The DDOs must be placed side by side (as siblings of each other) and properly connected to each other with set ddo_server message. Place your data-entry objects (DEOs) below your DDOs. The DEOs should be nested according to navigational needs. At least one of the DDOs must be assigned to at least one of the DEOs with the set server message, and one of the assigned DDOs should be identified as the view’s main DDO. That's it.

The Container Object

A view must be fully encapsulated. This means that the entire view must be contained inside a single object. The container object should be based on the dbView class. The external program will communicate with this view by sending messages to this dbView object.

Views are activated by sending a special message to the view’s parent object. This message must be created along with the view. A special command, Activate_view (and, optionally, Deferred_view ) is used to create this method. By convention the name of this message should be the name of object, preceded with "activate_" (e.g., the view oCustomer would have an access method of Activate_oCustomer). This might be the only external access method to this object.

The Data-Dictionary Objects

As stated, a view contains a group of connected data-dictionary objects. Before you create the data dictionary objects, you must first create the data-dictionary classes. Using the Database Builder utility you can very easily create data dictionary subclass for each data table in your database. These classes will be based on the DataDictionary class. These classes contain all of your special database rules.

Each data-dictionary subclasses will be placed in its own own package file(s). The conventional file extension for data-dictionary-subclass packages is ".dd". Any component that needs to use a particular data table and the database rules that are represented in its data dictionary class, may simply use the package file that contains it.

A single program will often have multiple views that contain DDOs that access the same data file and therefore are based on the same data-dictionary subclass. Because these DDOs will reside in different views, they are each encapsulated by their respective container object, and no conflict will occur among them, even if they have the same name.

There are explicit rules for placing DDOs in a view client. These are:

  • All DDOs are based on specialized data-dictionary subclasses, each created for a particular data table.

  • All the DDOs of a view are placed inside the view client as sibling objects. The parent-table DDOs are created first, then the child-table DDOs.

  • Parent/child relationships are established in DDOs by setting the DDO_server property in the child-table object. The link between a parent and child is established by the child DDO updating the parent DDO. This link must be established and it must be correct.

  • Within your view, you should identify define a main DDO. This is done by setting the view property Main_DD . The main DDO is usually that of the file that the view is centered around. In simple views, this will often be the child-most table’s DDO. In more-complex views, this may not be the case. In an order-entry view, the order-header DDO would be the main DD (even though there is a child-table order-detail DDO).

  • All objects referenced in the DDO_server property (there can be, and often are, more than one) and the Main_DD property (only one of these, ever) are referenced using the standard object name access syntax (e.g. set DDO_server to oCustomer_dd).

  • Depending on the requirements of your view, you may need to provide a relates-to constraint between a child and parent DDO. This is done with by placing the statement set constrain_file to parent_file inside of the DDO object. The constraint link must be made in addition to the DDO_Server link. (They are separate links). As a general rule, do not create a relates-to constraint unless it is truly needed.

When you create and connect DDOs in a view, you are creating a data-dictionary structure. The DDOs will all act in a coordinated fashion—this is what you want. A data-dictionary structure must be encapsulated inside the view. You would never connect two DDOs across two views.

Placing your objects in a view is easy. Care must be taken to make sure that the updating links are properly established. The following diagram provides an example of a database structure, a visual representation of the data-server structure (and linking) and a sample of how this structure is coded.

A Typical Database File Structure

Data-Server Structure and Updating Links

Coding the Data-Server Structure

Object View_Object is a dbView

Object Customer_DD is a Customer_DataDictionary

End_Object

Object Terms_DD is a Terms_DataDictionary

End_Object

Object Vendor_DD is a Vendor_DataDictionary

End_Object

Object Orders_DD is an Orders_DataDictionary

set ddo_server to Customer_DD

set ddo_server to Terms_DD

End_Object

Object Inventory_DD is an Inventory_DataDictionary

set ddo_server to Vendor_DD

End_Object

Object OrderDtl_DD is an OrderDtl_DataDictionary

set ddo_server to Orders_DD

set ddo_server to Inventory_DD

set constrain_file to Orders.File_number

End_Object

:

The Data-Entry Objects (DEOs)

Your data-entry objects are placed below your DDOs in the view. The DEOs are created with the following rules:

  • There are two types of DEOs: Controls and Containers. The Controls are used to display and edit data. Examples of controls are single line edits (dbForm), multi-line edits (dbEdit), checkboxes (dbCheckbox) and grids (dbGrid). Containers are used to visually group controls and container. Examples of containers are groups (dbGroup) and tab-dialogs (dbTabDialog).

  • Container DEOs may contain control DEOs and other Container DEO. This is referred to as nesting of DEOs. Nesting DEOs has no effect on database behaviors (saving, deleting, clearing, finding). You choose to nest or not to nest DEOs based solely on visual requirements and required navigational behaviors. You never place DEOs inside of DDOs.

  • DEOs must be connected to DDOs by setting the server property. The DDO that you connect to determines the DEO's saving, clearing, finding and deleting behaviors.

  • All DDOs set in the server property are referenced with the standard object access syntax (e.g. Set Server to Customer_dd).

  • DEOs should only use DDOs that are contained inside the same view.

In some cases, all DEOs will connect to a single DDO (although this DDO will often connect to other DDOs). In other cases, different DEOs within a view will use different DDOs. An order entry view will have its header DEOs using a header DDO and the line-detail DEOs using a detail DDO. In all cases, DEOs only use DDOs that are internal to the current view.

Object Referencing

Although there are several ways to access an object in DataFlex, the Framework is designed to use only one method for referencing an object – you simply refer to the object. This is referred to as the standard object access syntax. For example:

Set Server to Customer_dd

Get Value of oCustomerState to sState

Send UpdateValue of oMyHelper sParam1

Send Message of object_name {param1 .. param_n}

Get Message of object_name {param_1 .. param_n} to ReturnValue

Set Message of object_name to ParamValue

Basically, this method says "Find an object named object_name that is a child of the current-object (the current-object is the object sending the message). If the child object is found, the message is sent to this object. If this were all this could do, it would be a limited method for accessing objects. Delegation, however, makes this an extremely powerful object-access method.

If object_name is not found among the children of the current object, the message gets delegated to the current object’s parent object. The parent object then looks for an object named object_name among its child objects. At this point, object_name might be found among siblings of the original current object. If not, delegation occurs again, and the siblings of the parent object are reviewed. Understanding the role delegation plays here is crucial.

Note:

All objects are accessed using the standard object access syntax. This provides access to child objects, sibling objects, and sibling-of-parent objects on up the object tree. Other ways of referencing objects are never used.

If an object cannot be accessed using this access syntax, you should not be accessing it from where you are trying to. For example, a parent object should not directly communicate with a grandchild object. The principle of encapsulation and this access method discourages this. (An exception to this rule, the use of object neighborhoods, will be discussed later.)

Compatibility Note:

Earlier versions of DataFlex required slightly more complex syntax for standard object access where the object name needed to be part of an expression. Examples of this syntax are Send MyMessage of (oMyObject(self)) and Send MyMessage of (oMyObject(current_object)). This more complicated syntax is not required and can be replaced with the recommended and simpler Send MyMessage of oMyObject.

Constructing a View

This can all be summarized in the following way. An object view consists of a dbView, which encapsulates all DDOs and DEOs. All DDOs are placed as sibling objects inside the container. They are connected to each other with the set DDO_Server message. DEOs are placed underneath the DDOs and are nested (or not nested) according to navigational needs. The DEOs are connected to the data-server structure with the server property. Because views completely encapsulate their DDOs and their DEOs, they are independent stand-alone units. A view can be added to any program without fear of this new view creating side effects in other parts of the program.

A View-Object Package

Views should be created inside their own package files. In addition to the view object, this package should use still other package files and incorporate a view-access method into the view. The following example shows what a simple Studio generated view-object package looks like:

//AB/ Project Vendor Entry View

//AB/ Object prj is a View_Project

//AB/ Set ProjectName to "Vendor Entry View"

//AB/ Set ProjectFileName to "vendor.vw"

//AB/ Set GenerateFileName to "Vendor.vw"

//AB/ Set Size to 190 314

// Project Object Structure

// oVendorView is a dbView

// oVendor_DD is a DataDictionary

// oContainer1 is a dbContainer3d

// oVendor_Id is a dbForm

// oVendor_Name is a dbForm

// oVendor_Address is a dbForm

// oVendor_City is a dbForm

// oVendor_State is a dbComboForm

// oVendor_Zip is a dbForm

// oVendor_Phone_Number is a dbForm

// oVendor_Fax_Number is a dbForm

// Register all objects

Register_Object oContainer1

Register_Object oVendor_Address

Register_Object oVendor_City

Register_Object oVendor_DD

Register_Object oVendor_Fax_Number

Register_Object oVendor_Id

Register_Object oVendor_Name

Register_Object oVendor_Phone_Number

Register_Object oVendor_State

Register_Object oVendor_Zip

Register_Object oVendorView

//AB-IgnoreStart

Use DFAllEnt.pkg

//AB-IgnoreEnd

//AB-IgnoreStart

Use VENDOR.DD

//AB-IgnoreEnd

DEFERRED_VIEW Activate_oVendorView FOR ;

;

Object oVendorView is a dbView

Set Border_Style to Border_Thick

Set Label to "Vendor Entry View"

Set Location to 6 6

Set Size to 156 286

Set piMaxSize to 156 350

Set piMinSize to 156 180

//AB-DDOStart

Object oVendor_DD is a Vendor_DataDictionary

End_Object // oVendor_DD

Set Main_DD to oVendor_DD

Set Server to oVendor_DD

//AB-DDOEnd

Object oContainer1 is a dbContainer3d

Set Size to 129 273

Set Location to 4 4

Set peAnchors to anAll

Object oVendor_Id is a dbForm

Entry_Item Vendor.Id

Set Label to "Vendor ID:"

Set Size to 13 42

Set Location to 4 67

Set peAnchors to anTopLeft

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_Id

Object oVendor_Name is a dbForm

Entry_Item Vendor.Name

Set Label to "Vendor Name:"

Set Size to 13 186

Set Location to 18 67

Set peAnchors to anTopLeftRight

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_Name

Object oVendor_Address is a dbForm

Entry_Item Vendor.Address

Set Label to "Street Address:"

Set Size to 13 186

Set Location to 34 67

Set peAnchors to anTopLeftRight

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_Address

Object oVendor_City is a dbForm

Entry_Item Vendor.City

Set Label to "City:"

Set Size to 13 90

Set Location to 49 67

Set peAnchors to anTopLeft

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_City

Object oVendor_State is a dbComboForm

Entry_Item Vendor.State

Set Label to "State:"

Set Size to 13 32

Set Location to 64 67

Set peAnchors to anTopLeft

Set Form_Border to 0

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

Set Entry_State Item 0 to FALSE

End_Object // oVendor_State

Object oVendor_Zip is a dbForm

Entry_Item Vendor.Zip

Set Label to "Zip/Postal Code:"

Set Size to 13 66

Set Location to 79 67

Set peAnchors to anTopLeft

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_Zip

Object oVendor_Phone_Number is a dbForm

Entry_Item Vendor.Phone_number

Set Label to "Phone Number:"

Set Size to 13 126

Set Location to 94 67

Set peAnchors to anTopLeftRight

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_Phone_Number

Object oVendor_Fax_Number is a dbForm

Entry_Item Vendor.Fax_number

Set Label to "Fax Number:"

Set Size to 13 126

Set Location to 108 67

Set peAnchors to anTopLeftRight

Set Label_Col_Offset to 2

Set Label_Justification_Mode to jMode_Right

End_Object // oVendor_Fax_Number

End_Object // oContainer1

//AB-StoreStart

//AB-StoreEnd

CD_End_Object // oVendorView

//AB/ End_Object // prj

This is a complete view package. Notable elements are:

  • Use DFAllent.pkg, which is the package that allows your program to use the standard data-entry classes as well as other features required in most data-entry programs. This includes all of the high-level classes.

  • Use Vendor.dd, the data-dictionary-subclass package. Using DataBase Builder, you will have created a data-dictionary subclass for this data file. The Vendor_DataDictionary class is contained in this class package. This package also opens the data files. This view needs and uses the one DD class for the vendor table.

  • Deferred_view activate_oVendorView for; creates the view-access Message activate_oVendorView, which will activate the oVendorView dbView object. This message will be used by your main program in the view pulldown to activate the view. This can be sent from any object.

  • Object oVendorView is a dbView creates the actual view object, a dbView. It contains a properly connected data-dictionary structure and data-entry objects. In this case, all DEOs are using the same DDO, so the server is set only once.


Lookup Lists

A single lookup-list object (selection list) will often get used by multiple views. A vendor entry view and a check entry view will both use the same vendor list. Therefore, it does not make sense to encapsulate such a list inside a view (it should be accessible to all views). Normally, a file’s DataDictionary subclass will specify what lookup list should be assigned to which field. When this field is used in a DEO, it will automatically attempt to use the lookup list assigned to it in its file’s data dictionary. Because of this, lookup lists tend to be assigned in the data-dictionary subclass (with a use command) and not directly in the view.

Lookup List components are based on the dbModalPanel class and usually contain a DDO structure, a dbList object (that is connected to the DDO structure) and several Button objects. The same rules for creating and connecting DDOs used with Views can be applied to Lookup Lists.

Lookup Lists components have a file extension of “.sl”.

The Visual DataFlex sample workspaces all contain examples of Lookup List objects.


Other Components in Windows Applications

A windows application will often contain components designed to handle reporting and update processing. Usually reports are placed inside of Report View components and update processes are placed inside of Business Process components. Both of these components types are used in the same way that views are in that they:

  • Are self contained. They can be developed and tested stand-alone

  • Usually contain data-dictionary object structures that are connected and accessed according to the same rules used in views

  • Tend to have a single (or just a few) external access methods

Report Views

Report View Components are based on the ReportView class. They are designed to hold a DDO structure, any visual objects required to set up and run a report, and one or more reports objects. The report objects inside of the ReportView can be based on a number of different classes and technologies. Reports based on the cWinReport2 class use the Visual DataFlex Winprint-2 Report engine to generate reports. Reports based on the CrystalReport class allows a developer to integrate the Crystal reports and the Crystal Report Writer into their applications.

Depending on the reporting technology used, the report may or may not use data-dictionaries to access the report data. If DDOs are not needed, they will not have to be included in your view. If you are generating reports that update your data, you are strongly encouraged to use DDOs in your report view.

Report View components have a file extension of “.rv”.

Business Processes

Business Process objects are objects that contain all of the logic to perform application update logic. Where data-dictionaries are used to handle rules for your database, business process objects are used to encapsulate higher level rules and processes. An example of a business process might be an end of month posting and update. Business Processes objects (BPOs) are built on top of and use data dictionary rules. A BPO will usually contain a DDO structure and will use the DDOs to handle all record finding and all data updates. BPOs will usually not contain visual interfaces. It is expected that a BPO will contain one or more custom access methods (i.e., methods that you must create within the BPO) and those methods will be passed all of the information needed to run a process. A BPO may display a status panel, may allow for a process to be interrupted and may update a log as it progresses. All of these capabilities are built into the component.

Business Process objects are based on the BusinessProcess class. Unlike the other components a BPO class can either be created as a stand-alone object package component or it may be embedded within another component. This flexibility is provided to support the different ways a BPO may be used.

If you are creating a BPO that needs to be available as a resource for your entire application, you will want to create the BPO as a object-package component. This component is added once to your application in the same way that a view or report-view is added. Because it will be a sibling of all of your views, it can be accessed from any other object.

When Business Process objects are created as components they will have a file extension of “.bp”.

If you are creating a BPO that only needs to be used by a single view, you will want to embed the WBO within that view. In this case, you are encapsulating the BPO within the view and hiding it from the rest of your application. An embedded BPO should still contain its own DDO structure and it should only use that structure. It should not rely on any DDOs that might be available in its parent object (most likely a dbView or ReportView).


Web Objects and Web Applications

Web Objects are used in Web Applications and provide a similar functionality in web applications that views provide in windows applications. A web object will either be a Web Browser Objects or a Web Service Object. Web Browser Objects (WBOs) are used with browser based applications; Web Service Objects (WSOs) are used to provide web-services. The conventional file extension for both types of web objects is .wo.

Web Objects are designed using the same principles used for views. A web object contains a DDO structure, which must be properly placed linked using the same rules applied to views. Since web objects are non-visual they will not use data entry objects to interact with the DDOs. Instead they will contain custom methods that perform the needed business processes. Those methods will send needed messages to the Data Dictionaries to perform all database processes.

Web Application interfaces used to access web-objects must be exposed to the Internet. As part of the development process, you must specify which methods inside of a web-object should be public. This referred to as publishing the web-interface. Web Browser objects expose this interface to the Active-Server Page being used to access it. Web Server Objects expose the interface as a web-service and formally makes it definition available as a service-description document (WSDL).

Just like views, web-objects can be created and tested individually. You can build a Web Object, test it in program and then assemble any number of web objects in to a single Web Application knowing that with proper encapsulation the single application will work as expected.

Web Browser Object and Web Service Object components both have a file extension of “.wo”.


Framework Concepts

The framework defines a specific set of rules for assembling high-level components. These rules define how objects should be assembled, how they should be connected and how they should be accessed. You also need to know why these rules work. To do this, you need to understand the following topics:

  • The importance of encapsulation.

  • How delegation is used to send a message.

  • Object access methods and delegation - how delegation is used to find an object's ID.

  • How object access methods allows objects within a component to communicate.

  • Dependencies between objects and how they are used within a component.

Once these topics are understood, you will see why a high level component such as a view is assembled as it is.


Encapsulation

One of the most-important principles of object-oriented programming is encapsulation. An object is a "black box". The contents of the "black box" should be a mystery. The object is used by sending it messages. These messages are part of the object's public interface. As with any "black box", the simpler and more elegant the interface, the better.

A television set is an excellent example of an object. It is a box with a very limited public interface (power on, channel up/down, volume up/down). Anyone can use this interface. It also has a protected interface. This is usually hidden behind some kind of panel. This allows the more-experienced user to perform more-complicated tasks (adjust the color, horizontal tracking, etc.). It also allows the user to mess things up in a way they never could using the public interface. However, the user cannot break the television using the protected interface. Finally, the television has a private interface. By removing the back panel of the set, you have access to all components inside the "black box". Note that the back panel has a big warning on it saying that there are no user-serviceable parts inside the set. You enter at your own risk. You can now break the television set. Opening the back of the television is a serious breach of encapsulation.

Objects often contain child objects. The separate existence of these objects should not be apparent to users. A television object contains many sub-component objects. You should never directly access or even need to know about these objects. This brings us to one of the most-important rules of encapsulation. An object may know only about its child objects. You never, under any circumstances, send messages to descendants of an object's child. In other words, you don't send messages to grandchild objects, great-grandchild objects, etc. Disobey this rule in your program, and you've thrown out one of the major advantages of object-oriented programming.

The usual question that is asked at this point is, "How does an object communicate with a grandchild object?" The answer is, "You will never need to." Your object should contain all the messages required to do what you need. In some cases, the message handler in the object will actually send a message to one of its child objects.

A television contains a power supply, which is completely hidden. In order for the television to work, the power supply must switched on (which is also none of our business). The user of the television would not ask "How do I switch on the power supply?" Instead, they would say "How do I turn on the television?" When its "public" power button is pressed, the television sends a message to its child object, the power supply, switching on the power. As the user of the television object, you should not even know about the power supply.

While an object should only know about its child objects, it does need to know something about the world that it lives in. An object often requires services from other objects. This object could be its parent, or some other ancestral object. When an object requires services of another object, the object is said to have a dependency on that object. A television has at least two dependencies: it needs to connect to electricity and it needs to connect to some kind of receiving device (cable, aerial, etc.). If these services are not provided, the television will not work.

When an object has an external dependency, it will ask its parent for help in providing the needed service. Sometimes a parent object can directly provide the service required. More often, the parent object will provide a link between the object and another of its child objects. A room object might contain three objects, the television, a electrical wall socket, and a television cable connector. A television will ask its parent, the room, to provide it with the location of the electrical and cable outlets. It can then connect to these outlets. In object-oriented terms, the television asks its parent for the object_id of the objects it depends on. As long as the room can provide these object_ids, the needed connections can be made. Sometimes the parent will be unable to satisfy a dependency and will have to ask its parent for help. This is delegation. As long as one of the television object's ancestors can provide us with an ID, the television will be able to satisfy its dependencies (our television object has a very long connecting cable).

The more external dependencies an object has, the less-portable the object is. A television only has two dependencies. Because of this, our television object can be used in any of many room objects. The objects in the television have a much greater number of dependencies. Some of the objects may have been designed to specifically work for this television, and might work in no other. We are not as concerned about the portability of these objects.

View objects are very similar to a television object. They have a limited number of very-generalized external dependencies. They need to be able to connect to a menuBar object, an error object, and a help object. Views are very portable. They can be added to and removed from a program with little difficulty. The objects inside a view will be far less-portable. The DEOs expect other DEOs and very specific DDOs to exist inside the view.

The difference in portability between a view object and its component objects represents a good fit for data-entry-programming needs. We want our views to be quite independent. We are less concerned about object independence within a view. With extra programming effort, one could design the objects within a view to be quite independent. In most cases, this is simply not needed and therefore not worth the extra effort.


Delegation of Messages

When an object receives a message, it attempts to "understand" the message. The message will be understood if the object's class or any of its superclasses understands the message. If the object does not understand the message, the message will be sent on to the (receiving) object’s parent object. The parent object attempts to resolve the message in the same way, by checking its own class hierarchy. If the parent does not understand the message, it will send the message on to its parent. This is repeated until either the message is resolved or the desktop object receives the message (and fails to understand it) and an error is generated.

The process of sending a message on to the parent object is called delegation.

Look at the above object diagram. Assume that Objects E and C both understand the message jump. Assume that only Object A understands the message hop. If we send the message jump to either Object C or E, the message will be understood by that object. In other words, the class hierarchy that Objects E and C are based on was able to resolve the message.

If we send the message hop to Object E, that object will not understand the message. The message will get resolved through delegation. Object E would delegate the message to Object D, which would then delegate the message to Object A.

Object E did not resolve the message; Object A did. It is as if the message were sent directly to Object A. Finally, note that sending the message jump to Object B (or D) will result in an error. Jump will get sent to B, delegated to A, delegated to the desktop, where an error will be reported. If you understand these examples, you understand delegation of messages.

There are three types of messages: send, get and set. All three of these message types perform message delegation. The Get message type supports an additional expression style syntax which can be used to send messages. For example, these two lines of code are functionally the same:

get function_name to variable

move (function_name(self)) to variable

Messages delegation is supported and is the same for both formats. We recommend that you use the get syntax with function methods.


Delegation of Access Methods

Object-access expressions delegate! This is a powerful and useful features. When a message is sent to an object with the standard object access method, the access expression is evaluated using delegation (if required).

Send hello of B

Consider the above statement. This statement will be executed in two steps.

  1. The object ID of object B must be resolved. The runtime will search for a child object named B. If found, it will use that object id. If no object is found, delegation will occur and the search for an object named B will continue. This occurs until an object named B is found or delegation reaches the desktop object at which point an error will result.

  2. If the ID is found the message hello will be sent to the object_id evaluated in step 1.

The above statement could have been coded as:

Move (B(self)) to hoID

Send Hello of hoID

In this example we are temporarily using an alternate syntax for the object name that more accurately shows how DataFlex resolves message names. When you see the syntax (object_name(self)) you are creating an expression asking DataFlex to find an object named object_name that is a child of self. Self is the current object, which is the object that is sending the message. Therefore we are asking DataFlex to find the object and return its object handle (hoID). That handle is then used to receive the message.

Using the diagram on the previous page, what would happen if Object D sent this message? Since Object D does not have a child object named B, we might expect an error to occur. This would be an incorrect assumption. If Object D cannot resolve the access-method expression by finding a child object named B, it will ask its parent object, A, to resolve the expression (B(self)). Self is now Object A. Object A will attempt to find a child object named B, succeed and return that object's ID in the expression. The message hello will now be sent to that ID. The message Hello was not delegated; the object-access method was. This means that any object can send a message to a sibling object by using the (object_name(self)) access method.

What would happen if Object E sent this message? Object E will try to find a child named B. It will fail and delegate to Object D. Object D will try to find a child named B, fail and delegate to Object A. Object A will find a child named B and return its object_id. The message hello will now be sent to that ID. This means that an object can send a message to any sibling of any of its ancestral objects.

By using the (object_name(self)) access method and its implicit delegation, an object may communicate with any child object and any object that its parent (or its grandparent, etc.) can communicate with.

Refer back to the object diagram to see how delegation of access methods allows objects to communicate. Object A can send a message to itself and its children (B and D). Object B can send a message to itself, its child (C) and, through delegation, Objects A and D. Object C can send messages to itself and, through delegation, Objects B, D and A. Object E can send a message to itself and, through delegation, Objects D, B and A. Objects C and E can never directly communicate.

Does this represent a breach of encapsulation? Objects are supposed to be able only to send messages to themselves, to their immediate children, and to their parents (through delegation). The (object_name(self)) expression is evaluated through delegation. This is proper. A message is then sent to that object_id. It is valid to send a message to any object as long as the ID of that object was obtained through a proper encapsulated technique.

Although this type of access method does not breach encapsulation, it does create a dependency between objects. If, for example, you have an DEO is using (Vndr_DD(self)) as its DDO server, that DEO must be able to find an object named vndr_DD somewhere within its access-delegation path. The entry-form object depends on vndr_DD. If vndr_DD cannot be found by the entry form, it will not operate properly.

In Visual DataFlex referring to an object using an expression syntax or simply with it name accomplishes the same thing. These two code samples are the same

Send Hello of (B(self))

Send Hello of B

We used the expression syntax to show how an object access method is evaluated. When writing programs we will always uses the simpler direct syntax.


Object-Access Methods and Encapsulation

All objects should be accessed in one of the following ways:

Send|Get|Set Message [of self]

This sends the message to itself (sending and receiving object are the same). The of self is not necessary. The compiler will do this for you. Most messages are sent within an object. You will use this method most-often.

Delegate Send|Get|Set Message

This sends the message to the object's parent.

Send|Get|Set Message of object_name

This will find an object_id for the access expression and send a message to it. When required, the access method is delegated.

Move object_name to hoId

Send|Get|Set Message of hoId

If you are sending multiple messages to an object inside a function or procedure, you can save some time if you evaluate the object_id once and send all messages to this Id (hoId). This saves the runtime from having to continually re-evaluate the same expression. The speed difference is going to be quite small. If a message is sent to an object only one time in a function or procedure, use the previous access method. If multiple messages are sent to an object, use this method.

For example:

Procedure doSomething

Handle hoID

Move oMyObj to hoID

Send request_save of hoID

Send request_Clear of hoID

Send Activate of hoID

End_Procedure

You can use these access rules as a means of confirming that your program is properly encapsulated. If you cannot access an object using one of these methods, you should not be accessing that object.

The question that usually arises at this point is, "How do I send a message to a grandchild object?" The answer is, "Normally, You don't"! (The exception is discussed in the next paragraph.) If you need to access a grandchild object, you should send a message to the child object, which should then send a message to its child. If you insist on breaking this rule, you may do so with an expression (e.g., Send Message of (oGrandChildName(oChildName)) ). Treat this kind of code the same way you would a goto statement. Place several comments around it stating that you know this is wrong, apologizing for it, and promising to come back and correct it. If you are maintaining someone else's code and you encounter this kind of code, proceed cautiously.

There is a very useful exception to this rule. When building visual applications visual objects are often nested inside of other visual container objects. When this type of object nesting occurs, it is done to make a panel or dialog look right and it is not done for purposes of encapsulation. When these visual objects need to communicate with each other access can be complicated. Object Neighborhoods are used to remove this complication.


Object Neighborhoods

The purpose of Object Neighborhoods is to make it easier for objects to communicate with each other. Objects within a neighborhood can talk to other objects within the neighborhood by the first name (their simple object name) without needing to worry about where those objects are located.

Object Neighborhoods make it easier to manage visual objects within views. In a view, visual controls are often nested inside containers for appearance purposes only. As you design an application you may decide that it looks better to place several objects within dbGroups and that it then looks better to place those groups within a dbContainder3d. The Studio makes it very easy to make these kinds of changes. However, every time you move these objects around and put them in different containers you change the way those objects are referenced from other objects within the view. This creates the need to generate complex object reference statements and it may require code change every time you move objects around. This results in code that looks like this:

Get SortOrder of (oSortOrder(oSortParamsGroup(oSetUpContainer(;

oTabPageReport(oTabDialog))))) to iOrder

And, if you decide to move your oSortOrder object somewhere else in the view, you need to start all over.

With neighborhoods all objects in the view are registered with the view and all objects within the view are visible to all other objects within the view. The same object placed anywhere within the view is now coded as:

Get SortOrder of oSortOrder to iOrder

Object neighborhoods "flatten" the object structure of visual controls making it possible for all visible controls to see each other. Objects will still properly encapsulate (hide) non-visual and class-based objects that are clearly not meant to be public. This allows easy access to the objects you want easy access to while still writing properly encapsulated applications.

Views (dbView, View, ReportView, etc.) and panels (Panel, ModalPanel, dbModal, etc.) are defined as public neighborhoods. All objects within a neighborhood can communicate directly with any other object within the neighborhood by name. in TabDialog-views where each tab page (dbTabView) is treated as a separate view, each tab page is defined as a neighborhood.

If you use neighborhoods, you want to make sure that each object within the neighborhood has a unique name. While this restriction only applies if you plan on sending messages to object that has a duplicate name, it is good practice to uniquely name all objects within a neighborhood.

Note that object neighborhoods are used primarily to provide simpler access to visual object. Non-visual objects such as Web Objects do not use object neighborhoods and, when properly coded, do not need to use neighborhoods.


Dependencies between Objects

Dependencies are not bad. In fact, they often represent a very-realistic view of the world. Imagine that you had a room containing a person and a chair. When you send the person the message "sit," you want the person to sit on a chair. The person and a chair are both encapsulated by the room. They are sibling objects. When a person sits on the chair, he is not encapsulated by the chair (you do not send messages to the person via the chair). Before a person can sit down, they must have a chair they can use. There is a dependency between the person and the chair. From an OO perspective, the person object will ask its parent object, the room, to provide a chair object that it can use. If the chair is not available, the person will not be able to sit. You have a dependency between objects.

If our person must send a message to the chair, it could do it by sending the message:

Send Message of (oMyChair(Parent(self)))

(This is an example of a sibling sending a message to a second sibling.) We can let delegation do some of our work for us and simplify the expression to:

Send Message of oMyChair

Often, you will want an object to remember the object_id of the object that it depends on. Once an association has been established between objects, you may wish the objects to communicate directly. The person object could do this by defining a property named phoChair. When the person enters the room, it could ask the room object for its chair object and place this object_id in the phoChair property. When the person needs to send a message to the chair, it can now send the message directly to the chair. This will make continued communication between these objects faster.

Within a view, there are dependencies between DDOs. There are also dependencies between DEOs and DDOs. We are provided with a method of establishing these dependencies when we create each DDO and DEO. The dependency between two DDOs is referred to as an updating (set ddo_server ) dependency. The dependency between a DEO and a DDO is referred to as a using (set server)dependency.


The Updating Dependency

When Data Dictionary objects are created, dependencies between DDOs often must be established. When a child-file DDO is created, it must establish a dependency to its parent-file(s) DDO(s). It can do this with the DDO_server property.

Object Parent_DD is a Parent_DataDictionary

End Object

Object Child_DD is a Child_DataDictionary

set ddo_server to Parent_DD

End_Object

When we refer to child and parent, we are talking about the relationship between the objects’ data files, not between the objects (the objects are siblings). Once an updating dependency is established, the DDOs "know" about each other and can communicate directly. Since child-file DDOs are expected to update parent-file DDOs, the parent-file DDOs must be created first. Always create your DDOs starting with those for the parent-most files.


The Using Dependency

Data-entry objects require the services of a DDO for access to the database. This dependency should be specified when the DEO is created. It does this with the server property.

Object oMyDEO is a dbForm

set server to Child_DD

Once the object has been created, the DEO and DDO "know" about each other. The DEO keeps track of which DDO it uses by maintaining a property named server. This property contains the object_id of the DDO it uses. DDOs keep track of all DEOs using their services through internal properties. Once the using association is established, the objects can communicate directly. Since the DEO is expected to be using a DDO, the DDO must be created first.

Not all components use use the server property to establish a Using Dependency. Business Process objects and Web Service objects, for example, do not. Instead these components, communicate with their DDOs explicitly by creating methods that send messages to the DDOs.


Putting together a View

The following figure represents a view object structure. A view will contain a network of object dependencies. DDOs will rely on other DDOs. This is the updating dependency (set DDO_Server). DEOs rely on DDOs. This is the using dependency (Set Server).

Any DDO may establish an updating dependency with any other DDO that is defined in the same view. This connection is made through one level of delegation. Any DEO may establish a using dependency with any DDO. This can involve multiple levels of delegation. If a DDO was placed inside another DDO, the DEOs would no longer be able to communicate directly with the nested DDO.

If a DEO does not have its own server property, it will use the same DDO that its parent does. In this example, DEO_4 will use the same DDO as its parent DEO_2. They would both be using DSO_B. Note that a server is defined at the outermost view level. This means that DEO_1 will use DDO_A as its server. What DDO will DEO_5 use? Hopefully, you answered DDO_A.

The database behaviors of DEOs are determined by the DDO that they are using. The nesting of DEOs has no effect on clearing, finding, saving or delete behaviors. The exception to this is when a DEO is implicitly using a parent DEO's server (e.g., DEO_5). You will nest DEOs based on visual, program-flow and navigational needs.

Although it is easy to connect your objects with updating and using dependencies, it is critical that these links are established properly. If DDOs are not properly connected to each other, they will not act in a coordinated fashion. If a DEO is not connected to a DDO, you will not get the behavior you desire. If, for example, the main view did not use any DDO, it would have no server. If this happened, DEO_1 and DEO_5 would also have no server. If these connections are not made, your program will compile and run, but the view’s behaviors will be incorrect. Fortunately, the Studio creates these connections for you and, as discussed above, it is very easy to inspect view code and verify that your connections are correct.

When you create an application with the Studio, all of these connections are made for you automatically. In addition, a properly constructed data-dictionary subclass knows who its expected parent and child DDOs should be. If for some reason, these servers are not provided or are not properly connected, the DDO will detect the error and generate a runtime error message.

Before you leave this section, make sure that you understand how the delegation of access methods allows data-server and data-entry objects to communicate with each other. This is a central part of the Framework methodology.


Classes and Objects

Standard object theory says that classes contain the methods and that objects are instances of these classes. In Visual DataFlex terms, this would imply that classes should contain functions, procedures, and property definitions. It would also imply that objects should only contain data. In other words, an object should be an instance of a class, which is made unique by the values of its properties.

What can your program do with an object? It can create it, send messages to it, and destroy it. Sometimes sending a message to an object will change a piece of its data (e.g., Set value to "John"). Sometimes sending a message will cause an action to occur (e.g., Send RingtheBell). In all cases, messages sent to an object are handled by the object's class. The code that makes a message do something is contained in the class, not the object. The data that is acted upon or changed is in the object, not the class. Message inheritance takes place at the class level. When a message is sent to an object, the object refers the message to its class definition. If the class can resolve the message, it will; if it cannot, it will forward send the message up the class tree. Inheritance is a class concept, not an object concept.

Classes contain procedures, functions, and property definitions. Objects contain data. This is a fundamental principle of object-oriented programming. The above statement might appear to be inconsistent with the reality of DataFlex. Nearly every sample program included here contains objects with functions and procedures. Is DataFlex breaking the rules? No, it is taking a shortcut that makes the product easier to use (this is a human-engineering shortcut).

When you place a function or a procedure (or define a property) in an object, you are creating an "object/class". DataFlex cannot actually create an object with functions or procedures not defined in the object’s declared class. Instead, DataFlex will define a new "secret" subclass that contains all of the object's functions, procedures, and property definitions. It then creates the actual object as an instance of this new subclass. This shortcut allows you to create a subclass and an object in one piece of code. For example, the following two samples would yield the same result:

// An example of defining a new subclass

// and creating an instance of that class:

Class cBigBell is a cObject

Procedure RingTheBell

Send Bell

Send Bell

Send Bell

end_procedure

end_class

Object oMyBell is a cBigBell

end_object

// An example of creating an object/class:

Object oMyBell is a cObject

Procedure RingTheBell

Send Bell

Send Bell

Send Bell

end_procedure

end_object

Shortcuts usually have a hidden cost. The object/class shortcut is no exception. When you create an object/class, DataFlex will define a new subclass and define an instance of that class. That one object instance is the only object that you can base on that class. In the first example above, we could create as many instances of the cBigBell class as we desire. If we were using the object/class method, we would have to create additional object/classes, each containing the same procedure. This is wasteful and a bad programming practice. Subclasses are reusable (you can create multiple instances of it). Object/classes are not reusable (one hidden subclass, one object).

Creating object/classes has advantages. Very often you will need to customize a class in order to create a single instance of this class. The special entering or exiting procedures you code in an entry form might only ever be used to create a single object instance. Not only is it easier to code this into an object/class, it is probably easier to read. This is especially true when functions and procedures are created to handle the interaction between objects. It is much easier to see what is happening if all your code is together.

Some developers have chosen not to place any functions, procedures, or property definitions inside objects. Instead they create a lot of subclasses. This is the purist approach. Other developers have chosen to create object/classes. They feel that the ease of use outweighs the disadvantages. The Framework methodology supports either style. The samples shown here use the object/class method because it is more consistent with the style that has predominated to date.


Creating Classes from Objects

Even if you are going to use object/classes, you will still need to know how to create classes. You are going find that there will be times when you are placing the same function or procedure inside multiple objects. When this happens, you should consider creating a subclass. Creating a subclass is easy. Often you will find that you have already created an object with functions and procedures that you would like to convert into a subclass. This is quite easy to do.

We will start with a simple example. Let's assume that you wanted all your lists to ring a bell when an item has been selected (users press ENTER), and two bells when they cancel the list (users press ESC). After careful experimentation, you found the following list object did what you wanted.

Object oMyList is a dbModalPanel

// Augment to ring bell once

Procedure OK

Send Bell

Forward Send OK

end_procedure

// Augment to ring bell twice

Procedure Cancel

Send Bell

Send Bell

Forward Send Cancel

end_procedure

:

: add additional objects here

:

end_object

You now need to make this work the same way in all thirty of your list objects. You can choose to cut and paste the OK and Cancel procedures into all thirty objects or you can make a subclass and base the objects on this class. This is the better solution. A subclass is made as follows:

Class cBellDbModalPanel is a dbModalPanel

// Augment to ring bell once

Procedure OK

Send Bell

Forward Send OK

end_procedure

// Augment to ring bell twice

Procedure Cancel

Send Bell

Send Bell

Forward Send Cancel

end_procedure

end_class

Your object and the thirty other objects would now look like:

Object oMyList is a cBellDbModalPanel

:

: add additional objects here

:

end_object

One very important rule about creating a subclass from an object is that all code in the class must be placed inside a function or a procedure. As a rule, any object code that was not placed inside a function or procedure should get placed inside of your object constructor, the procedure construct_object .

For example, the code shown below takes the container class and changes three properties just for one object:

Object oMyContainer is a Container

set scope_state to true

set attach_parent_state to true

set ring_state to true

:

When you set these properties in a class definition, you should not do the following:

// This is wrong!

Class cViewContainer is a Container

set scope_state to true

set attach_parent_state to true

set ring_state to true

end_class

This will compile and it might even run. It will definitely not do what you want. All code in a class must be inside a function or a procedure. Place the object code that was not inside a function or a procedure inside construct_object. Construct_object is called when the object command is executed. Here is how to set these properties correctly in a class definition:

class cViewContainer is a Container

procedure construct_object

forward send construct_object

set scope_state to true

set attach_parent_state to true

set ring_state to true

end_procedure

end_class

There is a somewhat similar programming error committed with the end_construct_object procedure. Say you set the properties discussed above in Procedure end_construct_object, as:

class cViewContainer is a Container

procedure end_construct_object

forward send end_construct_object

set scope_state to true

set attach_parent_state to true

set ring_state to true

end_procedure

end_class

This would actually work, but it would make it impossible to set the properties to other values in instances of the new class. Anything you set them to would be cancelled by the settings in end_construct_object. And no error would be reported at compile or run time. In a subclass of this class, the property-setting code would not even be visible—it would be incorporated with a forward send end_construct_object.


Properties in Classes and Objects

Properties are the variables of object-oriented programming. In procedural DataFlex, you cannot write a very sophisticated program without creating variables. In object-oriented DataFlex, you cannot write very sophisticated programs without defining new properties. Properties can be defined within a class or defined within an object.

It is easy to define properties inside a class. Properties must be defined inside your construct_object procedure. If we had a subclass that needed two properties, we would define them as follows:

Class Vndr_Data_dictionary1 is a Vndr_DataDictionary

procedure construct_object

forward send construct_object

property boolean pbActiveOnly true

property string psValidStat ''

end_procedure

end_class

// here's how we would use this class

Object Vndr_DD is a Vndr_DataDictionary1

set psValidStat to "A"

Procedure OnConstrain

String sValidStat

Boolean bActiveOnly

Get psValidStat to sValidStat

Get pbActiveOnly to bActiveOnly

constrain Vndr.status eq sValidStat

if (bActiveOnly) ;

constrain Vndr.Active_stat eq "Y"

end_Procedure

end_object

If you only need to define properties within a single object, you can define the properties directly in the object. Defining a property inside an object has the same effect as placing a procedure or function inside an object; it creates an object class. The following example will create the same two properties directly inside an object class. Properties in an object should be placed near the top of the object and must not be placed inside a function or procedure.

Object Vndr_DD is a Vndr_DataDictionary

//define new properties

Property boolean pbActiveOnly True

Property string psValid_Stat 'A'

Procedure OnConstrain

String sValidStat

Boolean bActiveOnly

Get psValidStat to sValidStat

Get pbActiveOnly to bActiveOnly

constrain Vndr.status eq sValidStat

if (bActiveOnly) ;

constrain Vndr.Active_stat eq "Y"

end_Procedure

end_object

Do not use global variables to store data that is local to objects; use properties.

Delegation and Properties

It is possible to get and set the value of any property through delegation. This means that a property that is defined in an object can be accessed by any of the object's children, grandchildren, great-grandchildren, etc. This is a very powerful concept. You can use properties to allow objects to communicate. A property value may be set in one object and retrieved in another. The following segment shows how you might use a property and delegation to allow you to create a list that will display either all records or active records.

Object oVndrLookupList is a dbModalPanel

// define property which determines if filter is used

Property Boolean pbActiveOnly False

// Create DDO with conditional constraint

Object Vndr_DD is a Vndr_DataDictionary

Procedure OnConstrain

Boolean bActiveOnly

// get value of property through delegation

get pbActiveOnly to bActiveOnly

If (bActiveOnly) ;

constrain Vndr.Stat eq 'A'

end_Procedure

end_object

// Create the DEO with Alt+T as filter toggle

Object C_List is a dbList

set server to Vndr_DD

on_key key_alt+key_t Send Flip_Filter

begin_row

entry_item Vndr.NUMBER

entry_item Vndr.CUSTOMER

entry_item Vndr.Stat

end_row

// Flip the constraint

Procedure Flip_Filter

integer bActiveOnly

// get and set this property through delegation

Get pbActiveOnly to bActiveOnly // get property

Set pbActiveOnly to (not(bActiveOnly)) // flip its value

// Rebuild the constraints w/ the new value

Send rebuild_constraints of Vndr_DD

Send beginning_of_data // redisplay the list

end_procedure

end_object

end_object


Basic and High-Level Classes

The DataFlex runtime has a series of built-in low-level classes. These are referred to as the basic classes. Examples of the basic classes are Form, Grid, Edit , and Array. These classes are then built upon through a hierarchy of subclasses eventually resulting in high-level classes. Examples of the high-level classes are dbForm, dbGrid, and DataDictionary. Any high-level class is part of a class hierarchy. For example, the hierarchy of the high-level class dbForm is:

While this is an accurate representation of the class structure, it verges on information overload. As application builders, we are going to look at our classes from a different perspective.

You write your programs using tools. There are two types of tools, basic and high-level. The basic classes you are most likely to use are Array, Form, Edit, Radio, CheckBox , Grid, List , ComboForm , SpinForm , ModalPanel , Group , Panel , Container3d , Button , and Edit. The high-level classes you will use are dbContainer3d , dbGroup , dbView , dbForm, dbComboForm , dbSpinForm, dbEdit, dbGrid, dbList, dbModalPanel, and DataDictionary. All of these classes are task-specific. When writing an application, find the tool that is best-suited for the job and use it.

The Basic Classes

You can actually write some pretty-sophisticated applications without using any basic class directly. At some point, however, you may need to use them. Then, you would need to know how.

Sometimes you will not need any of the high-level data-entry classes in your program. When you write reports, you will need selection panels to enable users to choose report printing selections (date ranges, output device, etc.). Trying to use the high-level classes to do this is like trying to split a toothpick with a power saw. You will find that the Container3d, Group, form, button, and radio classes are ideally suited for this kind of task.

Sometimes you will find that you need to mix some base-class objects in among your DEOs. You might want a view to contain a Close button. This can be easily done, if you know how to use the base classes.

The High-Level Classes

Since you spend most of your time writing data-entry programs, you will spend most of your time using the high-level tools. It is easy to keep track of these tools. Each does a specific job. If your users need to enter text field data, you will need a text-editor DEO (dbEdit). If they need a data-entry form, you will need a form DEO (dbForm). If you use the standard DataFlex high-level tools, you use one set of tools (dbForm, dbGrid, etc.). You or your company might decide that you need to extend these tools one more level (dbForm_xyz, dbGrid_xyz, etc.). This gives your programs a unique look and feel that is consistent throughout your programs.

Tool building and application building are two separate processes. Before you can build your application, you must have a set of tools that will do the job. Depending on your needs, you will either purchase your tools, build your own tools, or purchase tools and extend them. Once you’ve got the tools you need, you can start application building. Hopefully, you will spend most of your time application building.

When you create classes, you are building tools. When you create objects, you are building applications.

From an OO-purist standpoint, this statement is not quite true. When you are creating an object, you will find that this object will require some custom functions and procedures. Whether those functions and procedures get placed in an object or a subclass, technically you are creating a subclass. Should this subclass be thought of as a reusable tool? Usually it should not. The subclass tends to be so specialized that it will only ever be used by a single object. If you choose to place all functions and procedures in subclasses, you will find that you have a high-level tool class, many subclasses descended one level from this class, and a single object for each of these subclasses. The following diagram shows some entry-form objects used by the XYZ Company. This company has extended the class one more level.

In this example, the dbForm_xyz class is the reusable form data-entry tool. There will be many objects or subclasses based on this class. When application developers need to create an entry form, they will start with this class. This class has to be a good, generalized, robust tool.

The subclasses based on dbForm_xyz need not all be so reusable. They perform very specific tasks and tend to be used by only one object or application. It would make sense to combine these subclasses and their objects into a single entity. DataFlex allows you to do this by placing the subclass code inside the object. These are object/classes. We could restate our earlier rule to say:

When you create classes you are building tools. When you create object/classes, you are building applications.

Using object/classes, our XYZ Company object structure changes as follows.

All of the objects (actually object/classes) are based on the dbForm_xyz class. Tool building now resides in one column and application building resides in another. You will notice that all of the samples provided show how to build applications. Most DataFlex programmers are application builders. If you want to see examples of tool building, look at the subclasses in the .pkg files.


Ownership of Classes

In the above sample, you might question why the XYZ Company chose to create a new subclass based on dbForm . Why didn’t they just modify the dbForm class? After all, the source is provided. The answer to this question is that they do not "own" the classes.

You do not want to change classes that you do not own. The term ownership is not used in a legal sense. It refers to the fact that you are not responsible for the class. If someone else is in charge of writing and maintaining class code, you do not own it. As long as modifications to superclasses are documented, there will be minimal problems in making whatever changes would be needed in the subclasses. The XYZ Company will then have minimal problems making whatever modifications they need to make in their classes.

Let's use some examples. In the next release, Data Access Corporation makes major internal changes in their dbGrid class. The new class’s message interface is unchanged, but the speed of the grid is stunningly faster. No changes will be required in the XYZ Company's dbGrid_xyz class to support this. If XYZ Company had made their changes directly to the DataFlex dbGrid class, they would both have to spend significant time updating this. They would probably have to read the new code and add their enhancements all over again.

If you create your own subclasses based on the DataFlex classes (and you are encouraged to do this), you should plan on extending the subclass tree down one more level and placing all of your changes at that level. This will put you in the strongest possible upgrade path.


Class and Object Packages

The software tools provided consist of class packages, and object packages. Although class and object packages can have the same file extension (.pkg) and are added to a program the same way (use package_name), they serve very different purposes.


Package Filename Extensions

By convention, Visual DataFlex uses special filename extensions for package files indicate the nature of their contents as listed below:

Package Contents | Filename Extension
Classes | .pkg
Data-dictionary Subclasses | .dd
Data-Entry Views | .vw
Lookup List Views | .sl
Report Views | .rw
Business Process Objects | .bp
Web Objects | .wo

(Programs—collections of views, usually with a menu bar—usually use the .src filename extension.)

The Studio generates source-code files using these extensions. You can follow these conventions, add your own set of extensions to this list, or use .pkg for all packages. The use command will work with any file extension if the file extension is included:

use Dfallent.pkg

use Confirm.pkg

use Vendor.dd

use CustEnt.vw

The use command allows you to omit the file extension if and only if the package has a file extension of .pkg. However, you are encouraged to always provide the extension.


Class Packages

A class package includes class definitions. Your program will use the class definitions to create the objects the program needs. These are your reusable tools. Usually, you can create an unlimited number of objects based on any subclass.

Most of the class packages included in Visual DataFlex are the high-level classes. These all get pre-compiled into the dfallent package (dfallent.pkg). These are the primary tools you will use for application development.

You should avoid changing any of the package classes provided. If these classes do not behave exactly as you wish (and we assume that they will not), you can attain the behaviors you want by creating a series of subclasses based on the classes provided here (e.g., cMydbForm is a dbForm). Simply add your classes as a layer over the DataFlex classes, and you are ready to go.


The OO Philosophy of Program Development

Object-oriented programming is supposed to enable us to rapidly develop and maintain applications. It will, if we employ the correct strategies. Listed below are a few suggestions that hopefully will aid you in the development process.

  • Know when to leave well enough alone

  • Prototype

  • Know what behaviors you want


Know When to Leave Well-Enough Alone

You have no idea how much time, money, and frustration you will save yourself if you follow this rule.

The standard mode of operation for most programmers has been to stick with a part of an application until it is perfect. This explains why there are so many applications that are 60% complete and hopelessly behind schedule. Of course the 60% that is complete looks great. This type of approach never worked very well with non object oriented programs and, not surprisingly, it doesn't work in an object-oriented environment.

As you are writing your applications, you will find that there are places where the program will not behave exactly as you want. Often you will find that you need to use a class in a way that it was not designed to be used. One of the great features of object-oriented programming is that you can create subclasses which will allow you to customize your class to make it do anything you want. This is also the danger of OOP. Tool building can be a time-consuming affair. It also requires that you have far more knowledge of the classes that you are working with. When you write a program using high-level classes, you will find that you can assemble applications quite rapidly if you use the classes the way they were designed to be used. As soon as you start customizing the classes or trying to use them in unorthodox ways, you will find that your application productivity drops significantly.

Before you dive in and start customizing your program or your tools, ask yourself the following questions. Is this custom feature essential to the application? In other words, could you deliver it or sell it without the feature? Is there time in your development schedule for tool building? If your application will work without custom changes, the best thing to do is to maintain a wish list. As such items come along, add them to the list. When your application is finished, you might find that you are ahead of schedule. This can happen. Once you have installed the program, you will start receiving real feedback. It may turn out that your user really did not care about some enhancements that you felt were essential. They will probably give you plenty of additional wishes for your list. At this point, you are ready to start your next revision. You can prioritize your wish list, estimate the cost of each addition, and decide whether a particular feature should be implemented.

Building an application is often compared to building or remodeling a house. Just like a builder, you have to have a cost-effective method for handling change orders. As an OO-builder, you have a big advantage over a home builder. The home builder must often handle change orders while they are doing the original work (it is too expensive to go back and make changes in a completed building). You can build your entire application and add the changes during the next revision. This provides a very powerful way of controlling costs and delivering product on time. Use it!


Prototype

One of the goals of object-oriented programming is to close the time gap between application design and application testing. The sooner you can get a prototype of an application, the better. The longer an application's "gestation" period, the greater the risk that the application will be unacceptable. If you deliver an application that took a long time to create, you will often find that the original application specifications are wrong, the needs of the customer have changed, and/or that your customer's management has changed (placing you in an uncomfortable political situation). While none of these is your fault, you will suffer the consequences nonetheless.

Object-oriented programming technique is ideally suited for creating and delivering applications as a series of revisions or prototypes. You can use the Studio to create and test entry views almost immediately. Feedback you receive here will allow you to make sure that your view’s appearance and navigation behaviors are proper. If you follow the "leave well-enough alone" philosophy, you will be able to create working views, assemble your application, and deliver your "first-pass" version. Then, based on (enthusiastic) user feedback and lists of desired enhancements, you can deliver your more-polished application.

This development philosophy ensures that you continually progress in the right direction and helps you to avoid getting bogged down making minor (and often inconsequential) changes.


Know What Behaviors You Want

A program can be defined by its behaviors. It’s up to you to decide what these behaviors should be. There is a tendency to place objects together and hope that the objects will "just do the right thing." Often they will. When they do not, you need to know what the "right" behaviors are. A data-entry program's behaviors can be divided into two types, data-entry and navigational. In a large event-driven application, these can get complicated. Fortunately, the two behaviors do not interact much and can therefore be treated separately.

Navigation Behaviors

Event-driven programming makes controlling navigation a far greater challenge. You have two types of navigation devices, the mouse and the keyboard. With a mouse, users can navigate to just about any item in just about any object from just about any object. In most cases, you want the mouse to be free to roam the desktop (non-modal navigation). In some limited cases, you will want to restrict the mouse's movements (modal navigation). The keyboard is a far more-limited and less-intuitive navigational device. You are provided with a limited number of navigation keys (Next control, Previous control, etc.). It is your job to make sure that they act sensibly. You control this with object placement (object order and nesting), setting navigation properties (for example, ring_state and scope_state), and augmenting navigation-message handlers (for example, switch and switch_back).

The high-level classes you will be using are designed to make the navigation between objects as simple and automatic as possible. A large number of conditions are handled properly without any programmer intervention required. More-complicated views will require customization. Consider, as an example, a check-writing view consisting of a check-header entry-form object and a check-detail grid object. We can define the following navigational needs and conditions. These may not agree with what you would want to see in your application. This is not important. It is important that you can define what your requirements are.

We will define our navigation needs for the check view as:

  1. The mouse should support moving back and forth between the header object and the grid object. The switch and switch back keys will permit navigation between these objects with the keyboard.

  2. Before entering the detail grid, we must make sure that the current check header record is valid and saved. If it is not, do not allow entry into the grid.

  3. When a row-change navigation occurs or when grid-to-header navigation occurs, the grid record should be saved. If the save fails, do not allow the user to navigate out of the row.

  4. Users should be able to navigate out of either the grid or the header object into a different view without any kind of save or validation.

Once you have defined your navigational needs, you can now determine which needs will require custom code. Needs 1 and 4 are handled automatically. Needs 2 and 3 will require customization. It will take a while to be able to think like an event-driven programmer. For example, you might try to solve 2 by sending a request_save in the exiting procedure of the header object. It would make sense that the header object should save its data before the detail object is entered. This will not work all the time. The header object can be exited on the way to any other object, and the detail object can be entered from any object (it's that darn mouse again). The proper approach is to perform the save in the grid's entering procedure. The grid object will send a message to the header object asking it to save itself. The header object should return a value letting the grid object know if the record was saved. If it was not, the grid will not allow entering to proceed. This may seem convoluted, but this is what is required to make event-driven programs work properly.

Data-Entry Behaviors

Data-entry objects perform four data-entry tasks: save, delete, find, and clear. That's it. You must make sure that all these operations behave properly. Before you can do that, you need to know what these behaviors should be. This will determine how you should connect your DSOs (with the ddo_server property), how set your finding filters (inside of OnConstrain), and where in the Data Dictionary structure you connect your data-entry objects (with set server).

If you know what behaviors you want and expect from your application, it becomes much easier to test your program. You simply test that navigation works the way you desire and that for each object the four data-entry behaviors are proper. If navigation is improper, you will want to make adjustments in your DEOs (nesting, property settings, message handlers). If data-entry behaviors are incorrect, you will want to adjust your DDOs (the DDO_Server dependencies, constraints, message handlers) and the DEO-to-DDO connections (the Server dependencies).


When to Create Classes

When no existing class provides functionality close to what you need, or when multiple custom objects with similar definitions will be used in one or more applications, it is beneficial to create a class. There are three categories of class creation: generalized, application-specific and program-specific.

Generalized classes, are classes designed to be used in a variety of applications, without requiring the use of modules or classes oriented toward a specific application. All of the data-entry classes (lists, grids, entry forms) are examples of generalized classes.

Application specific classes, are classes designed to allow the creation of one or more specific objects in programs designed for a particular application. Data-server subclasses for each data file are an example of an application-specific class.

Program specific classes, are classes that are created and used in a single program (a single source-code module). The creation of program-specific classes is often a matter of programming style and preferences. Some programmers prefer to never create object/classes. An object/class is a class created internally by DataFlex when one or more functions, procedures, or property definitions not contained in the superclass is created in an object. Instead, these programmers create a subclass for any such object, moving all the customizations into the subclass. They then base their object on this sub-class. Others are content to let DataFlex handle this, at least up to a point.

How do you know if a class should be generalized, application-specific, program-specific? The answer is, you can't always tell before creating and using the class. You may find that what you thought was an application-specific class can be reused in other applications. You may find that what you considered to be a program-specific class will need to be re-used in another program. You may discover that a generalized class is only ever used once, or never used at all. The truth of the matter is, there is really only one kind of class, and it is re-usable.

While you may not be able to categorize a class type, you will find that the type of programming required for each class type is substantially different. The more-generalized the class, the more generalized the code must be. In other words, such a class must be able to operate properly in many different conditions. For this reason, the design and creation of generalized classes takes more time. The data-entry classes are good examples of this generalized type of design.

When a dbForm processes the request_save message, it must check to see if it is directly using a DSO, if it using its parent’s DSO, or if it is not using any DSO at all. It must be able to handle each case. If you are creating a generalized subclass based on dbForm, you must make sure that it can also handle all of these conditions or specifically document any new limitations. Either way, this will take time.

If you were creating a dbForm subclass that is only going to be used in one specific module, you would not need to handle all possible variations because you could know the variations will not occur. If you build every class to be generalized and reusable, you will over-build, and the development process will be too slow. If you build every class as if it were program-specific, you will under-build, and you will find that none of your classes is reusable. Because you will not be able to re-use your classes, you will again find that your development process will be too slow.

The best solution is to know what type of class you are building and design accordingly. Another way to say this is to know when you are building tools, and when you are building applications. This approach should yield the most-efficient long-term development strategy. Keep in mind that it is not hard to convert a specific class into a more-generalized class. In fact, it is not that hard to convert an object/class into a separate subclass and object.

A class can be defined within the main source module of a program, or in a separate source file called a package that allows the class definition to be re-used by multiple modules. When creating a package, the source is stored in a file with the extension .PKG and can be included for use in a source module with the use command. Generalized and application-specific classes will almost always get placed in their own package files and included in source modules with a use command. Program-specific classes will often be coded directly in the module that uses them.