Using the wwBusiness Object Wizard

To facilitate the process of creating specific subclasses for your business objects Web Connection provides the Business Object Wizard which connects a table to a business object. Note that although a table is the most common scenario, tables are not required for the business object.

To start the Wizard do:

DO Console WITH "BUSOBJECT"

*** For Client Tools
* DO wwclient_Console with "BUSOBJECT"

which brings up the Wizard form:

Base Class
You can specify a base class for the new class that is to be created. By default this will be wwBusinessObject in wwBusinessObject.prg and that's the recommended base class. You can also use wwBusiness.vcs and wwBusiness or choose any base class you subclassed yourself (an application base business object.)

Name of the class
Pretty obvious - this will become the business object class name.

Class library
Where to store the new business object. This can either be a PRG files for subclassing from wwBusinessObject or a VCX class when subclassing from wwBusiness.

Base Table
The Web Connection business objects tend to link to tables - so pick a table that you want as your base table. You may not actually use the table later on but you need to specify one here. If you're using SQL Server data only just type in the name of the file, otherwise go ahead and pick the file. For Fox tables I suggest that you use relative paths such as .\wwstore\wws_items.dbf instead of fully hardcoded paths. Relative paths port much easier if you move the application. For Sql Server you should specify just the table name.

ID Table
For Fox data only you can specify the name of the ID table. If the table doesn't exist one will be created. For SQL Server the table name always will be wws_id and it will be created if it doesn't exist. The Wizard will add a record to this table with the name of the base table.

PK Field
Specify the field that will be used as the primary key field. Remember this field must be a numeric (Integer preferrably) field in order to be able to accept the PK generation features. If you choose a different type you'll have to override the NewId() method to handle the ID generation.

For Fox data you can specify a field name here and the Wizard will add that field to the ID table. For SQL data you have to manually add the field to the table.

Fox/SQL Data options
Choose whether you want to work with Fox or SQL Server data or both. This option controls the ID table updates.

CreateTable() and Reindex() generation
If these options are checked these methods are automatically generated for you based on the structure of the table that the business object is bound to. Note, this will overwrite any code in those methods and generates code only for the Fox tables, not SQL Server tables (nDataMode=0). If you have custom code here you can use the Table and Index Structure Exporter utility and paste the generation code manually.

SQL Connection
If you want to use SQL Server provide a connection string that will allow you to connect to the SQL database you want to connect to. This value will also become the default connection string used by the object (which you can of course override with code at any time).

Note that connection strings default to server=.;integrated security=yes; and you can optionally omit those values unless you want to use something different. To connect to a DB named WebStore on the local server with integrated security it's enough to specify: database=webstore.

Generate Class
Determines whether a class is generated. You can turn this off if all you want to do is generate the ID table entry.

Generate ID Table Entry
Determines whether the ID table is created and/or updated. The value of the counter is set to the highest PK + 1.

Note that you can run the Wizard multiple times without worrying about overwriting code. The Wizard only writes property values so existing class code is not affected. The exception to this are the CreateTable() and Reindex() options which will overwrite the respective method code with newly generated code.

A typical multiple use scenerio will be:

  • Create object for fox data
  • Come back to create SQL entries - run only the ID generation
  • Come back to update the ID table entry
  • Come back to update the CreateTable() and Reindex() methods when structure changes occur.

The Wizard remembers your last settings with a config stored in your temp directory.

What's generated

The Wizard generates a business object class that is a subclass from wwBusinessObject (or whatever class you chose in the Wizard). The class is mostly empty except for the following generated methods (if you chose Fox data):

Empty Overloaded Methods

  • Load()
  • Save()
  • Validate()

These are there simply as placeholders and can be removed if you don't need them. But these methods are often overridden to provide custom load, save or validation before save behavior.

FoxPro File Administration Helpers

  • CreateTable()
  • Reindex()

These methods provide FoxPro file creation and reindexing functionality based on the table's original structure.

Important: The structure is not updated when the tables are changed. You have to manually update these methods or regenerate them. You can use tools\crt_dbf.prg to generate new structure and reindex commands from a table or cursor.

Here's the actual class output generated by the Wizard:

DO wwBusinessObject
SET PROCEDURE TO ccustomer.prg ADDITIVE



*************************************************************
DEFINE CLASS cCustomer AS wwBusinessObject
*************************************************************

cAlias = "customers"
cDataPath = "c:\webconnection\fox\samples\wwdemo\"
cFileName = "customers"

cPkField = "pk"
cIdTable = "c:\webconnection\fox\samples\wwdemo\cust_id"

*** Sql Connection String
cConnectString = "database=test"

*** 0 - fox 2 - Sql 4-Web
nDataMode = 0

#IF .F.
LOCAL THIS as ccustomer of ccustomer.prg
#ENDIF


************************************************************************
*  Validate
****************************************
***  Function: Add custom validation Logic here or remove if you
***            have none. Call explicitly or with `lAutoValidate = .t.`
***    Return: .T. for no errors, .F. if you do
************************************************************************
FUNCTION Validate()

*!*	THIS.ValidationErrors.Clear()

*!* *** Put your custom validation logic here
*!*	IF LEN(this.oData.Author) < 6
*!*	   this.AddValidationerror("Author name has to be at least 6 characters")
*!*	ENDIF

*!*	IF THIS.ovalidationerrors.COUNT > 0
*!*	    THIS.SetError(THIS.ovalidationerrors.ToString())
*!*	    RETURN .F.
*!*	ENDIF

RETURN .T.
ENDFUNC
*   Validate


************************************************************************
*  Load
****************************************
***  Function: Use this to override default Load behavior for adding
***            things like child objects/collection, or updating loaded
***            values on load.
***    Assume: Make sure to call `DODEFAULT()`.
***      Pass:
***    Return:
************************************************************************
FUNCTION Load(lvId)
RETURN DODEFAULT(lvId)
ENDFUNC
*   Load

************************************************************************
*  Save
****************************************
***  Function: Use this optionally override save behavior. Often used
***            to update values like timestamps or fixup values before
***            saving. Make sure to call `DODEFAULT()`
***    Assume: Make sure to call `DODEFAULT()`.
***    Return: .T. or .F.
************************************************************************
FUNCTION Save()
RETURN DODEFAULT()
ENDFUNC
*   Save

*************************************************************
FUNCTION CreateTable(lcFileName)
*************************************************************
* ...
ENDFUNC
* CreateTable

*************************************************************
FUNCTION Reindex()
*************************************************************
* ...
ENDFUNC
* Reindex

ENDDEFINE
* ccustomer

The class is basically empty except the admin for the methods and the idea is that you add your own custom methods for custom queries and augmenting the CRUD operations like Load() and Save() or OnValidate()

Fixing up File Location Paths

You may notice that the Wizard creates hard coded paths for your business object class. Generally this is not a good idea because hardcoded paths are not portable if the project gets moved:

cAlias = "customers"
cDataPath = "c:\webconnection\fox\samples\wwdemo\"
cFileName = "customers"

cPkField = "pk"
cIdTable = "c:\webconnection\fox\samples\wwdemo\cust_id"

It's highly recommended that you do one of two things:

  • Use Application Relative Paths
  • Make sure SET PATH includes the data paths

Using Relative paths looks like this:

cAlias = "customers"
cDataPath = ".\samples\wwdemo\"        && relative to app start path
cFileName = "customers"

cPkField = "pk"
cIdTable = ".\samples\wwdemo\cust_id"  && relative to app start

Using SET PATH looks like this:

Somewhere during application startup:

SET PATH TO '.\samples\wwdemo` ADDITIVE

Then in the business object:

cAlias = "customers"
cDataPath = ""          && no path - it's in SET PATH              
cFileName = "customers"

cPkField = "pk"
cIdTable = "cust_id"    && no Path

Adding Your own Methods

A business object becomes a business object only once you start adding functionality:

  • Query methods
  • Business Operations and Calculations
  • Validations
  • Custom Insertions/Data Manipulation

Here are some examples:

Query Methods

This typically takes the form of building up a query string from various input parameters and calling the Query() method:

PROCEDURE GetWeblogEntries
LPARAMETERS lnCount, lcFields, lcFilter, lcCursor

IF EMPTY(lcCursor)
    lcCursor = "TEntries"
ENDIF

IF !EMPTY(lnCount)
    lcSql = "SELECT TOP " + TRANSFORM(lnCount) + " "
ELSE
    lcSql = "SELECT "
ENDIF

IF EMPTY(lcFields)
    lcFields = "title,body,active,entered,bodymode,pk "
ENDIF

lcSql = lcSql + lcFields + " FROM " + ;
    THIS.cfilename

IF !EMPTY(lcFilter)
    lcSql = lcSql + " Where " + lcFilter
ENDIF

lcSql = lcSql + " ORDER BY ENTERED DESC "

RETURN THIS.QUERY(lcSql,"TEntries")
ENDPROC

Although the Query() or Execute() methods are recommended for command execution in order to provide some abstraction and the possibility of switching to a SQL backend, you can also just run raw FoxPro query commands here - as long as you return the _TALLY or Reccount() from query results for consistency.

THe code above could just do:

SELECT title,body,active,entered,bodymode,pk ;
    FROM blog_entries ;
    WHERE entered > DATE() - lnDays
    INTO CURSOR TEntries
    
RETURN _TALLY    

The main goal is not about the mechanics, but abstracting your business logic including queries into reusable methods that are:

  • Easy to find
  • Easy to debug
  • Easy to fix in one place if used often

Another common task is to calculate some values related to a business entity. The following calculates the Weblog stats displayed on the home page of the Web Connection Weblog and returns an object. As such this is a custom function that returns some specific data related to Weblog entries:

FUNCTION CalculateWeblogStats
LOCAL loStats, lnResult

loStats = CREATEOBJECT("EMPTY")

ADDPROPERTY(loStats,"Posts",0)
ADDPROPERTY(loStats,"Comments",0)
ADDPROPERTY(loStats,"Trackbacks",0)

lnResult = THIS.QUERY("select COUNT(pk) as Posts from " + THIS.calias + " where active","TStats")
IF lnResult > 0
    loStats.Posts = TStats.Posts
ENDIF

lnResult = THIS.QUERY("select COUNT(pk) as Comments from blog_comments where active","TStats")
IF lnResult > 0
    loStats.Comments = TStats.Comments
ENDIF

RETURN loStats
ENDPROC

Note that helper functions can be very simple and still be useful and they don't necessarily have to access the database to live in the business object. For example this method simply returns formatting for a GravatarLink() for the active Weblog Comment:

PROCEDURE GravatarImagelink
LPARAMETERS lcEmail, lnSize
IF EMPTY(lnSize) 
   lnSize = 85
ENDIF
RETURN GravatarLink(lcEmail,lnSize)
ENDPROC

Updating Values on Save

Another common operation is an override for the Save() method to update values before they are written to the database. Here the Updated field is updated before writing:

PROCEDURE Save

IF EMPTY(THIS.oData.Abstract)
    THIS.oData.Abstract = TextAbstract(THIS.oData.Body,200)
ENDIF

THIS.oData.UPDATED = DATETIME()

return DODEFAULT()
ENDPROC

Note that you **have to call DODEFAULT() to ensure the base save behavior is firing.

If you've customized your .oData object on load to hold additional data than what's in the mapped table, you'll need to make sure you also write that data back to the database.

Validation

Validation is important for most business objects and you can override the Validate() method to provide custom validation logic that checks for certain conditions and then adds any validation errors into the ValidationErrors collection:

PROCEDURE Validate

THIS.oValidationErrors.Clear()

IF LEN(this.oData.Author)
   this.AddValidationerror("You have to specify a name","Author")
ENDIF

IF LEN(THIS.oData.Body) < 20
    THIS.Addvalidationerror("Body needs to be longer than 20 characters.","ody")
ENDIF
IF EMPTY(THIS.oData.Title)
    THIS.Addvalidationerror("The title cannot be left blank","Title")
ENDIF

*** If we have errors added - return .F.
IF THIS.oValidationErrors.Count > 0
    THIS.SetError(THIS.oValidationErrors.ToString())
    RETURN .F.
ENDIF

RETURN .T.
ENDPROC

The Validate() method can either be called explicitly via code, or you can set loBus.lAutoValidate = .T. to automatically validate when Save() is called. If validation fails, the Save() will fail before the database is updated.


© West Wind Technologies, 2023 • Updated: 06/14/20
Comment or report problem with topic