Está en la página 1de 266

PowerBuilder Online Courses

Advanced PowerBuilder 7
 MDI Applications
 Review of Course - I
 Introduction
 MDI Concepts
 MDI Basics
 Frame
 Client Area
 Sheets
 MDI_1 Control
 Moving Sheets
 Converting to a MDI Application
 Opening an Instance of a Window
 Creating a New User Interface
 Resizing the Controls Automatically
 Arranging Sheets
 Displaying About Window
 Displaying a Background Logo
 Minimizing Application When Idle
 Customizing Toolbars
 Scoping Variables
 Local Variables
 Global Variables
 Shared Variables
 Instance Variables
 Communication between Objects
 Global Variables
 Referencing Window Variables Directly
 Opening Window with a Parameter
 Structures
 The Message Object
 Calling Functions
 Triggering or Posting Events
 Displaying Popup Menu on the DataWindow
 Summary
 Review Questions & Answers
 Exercises
 Advanced DataWindows – Part I
 DataWindow Buffers
 Adding Rows
 Deleting Rows
 Filtering Rows
 Modifying Rows
 Edit Control Buffer in the DataWindow Control
 DataWindow - Data Validation
 DataWindow Events
 RetrieveStart Event
 RetrieveRow Event
 RetrieveEnd Event
 PrintStart Event
 PrintPage Event
 PrintEnd Event
 UpdateStart Event
 UpdateEnd Event
 EditChanged Event
 ItemChanged Event
 ItemError Event
 ItemFocusChanged Event
 DbError Event
 Error Event
 SQLPreview Event
 Other Non-Mapped, but Important DataWindow Events
 Removing Unwanted Rows
 Data Retrieval Cancellation
 Appending the New Result Set to the DataWindow
 Appending a Row Automatically
 TAB Key Behavior to the ENTER Key in a DataWindow
 Resetting Update Status of All Rows
 Powering up the UP Arrow Key in the DataWindow
 DataWindow Update Properties
 Multiple Table DataWindow Update
 Parent-Child Tables Data-Entry
 INSERT Trigger On trans Table
 DELETE Trigger On trans Table
 UPDATE Trigger On trans Table
 An Introduction to Triggers
 DropDown/Child DataWindows
 Modifying DDDW Dynamically
 Dynamic DataWindows
 DataWindow Object—Dynamic Assignment
 Modifying Data Source Definitions
 GetSqlPreview() & SetSqlPreview()
 GetSQLSelect() and SetSQLSelect()
 Modify() Function
 The DW Syntax Tool
 On Fly DataWindow Creation
 Summary
 Review Questions & Answers
 Exercises:
 Object Orientation—PB Implementation
 In This Session You Will Learn:
 What Are Objects?
 Objects Basics
 Class
 Instances
 Abstract Class
 Concrete Class
 Messages
 Methods
 SubClass
 SuperClass
 Inheritance
 Base Classes
 Inheriting the Code
 Inheriting the Interface
 Multiple Inheritance
 Benefits of Inheritance
 Encapsulation
 Benefits of Encapsulation
 Polymorphism
 Overloading
 Inclusional Polymorphism OR Overriding
 Operational Polymorphism
 Delegation - Aggregate Relationship
 Inheritance in PowerBuilder
 Inheriting a Window
 Scripts
 Viewing Ancestor Script
 Extending Ancestor Script
 Overriding Ancestor Script
 Calling Ancestor Scripts
 Inheriting Menu Objects
 Multiple Inheritance
 Encapsulation in PowerBuilder
 Variables & Scoping
 Name Spaces
 Access Levels - Variables
 Access Levels - Functions
 Choosing Between Events and Functions
 Creating an Instance
 Destroying an Instance
 Garbage Collection
 Polymorphism in PowerBuilder
 Sending Messages
 Function Overloading
 Function Overloading Using Inheritance
 Function Overloading Without Using Inheritance
 Function Overriding
 PowerBuilder's Class Hierarchy
 The Object Browser
 Summary
 User Objects
 Introduction
 Visual User Objects
 Class (Non-Visual User Objects)
 Creating a Standard User Object
 Creating a CommandButton User Object
 Creating a DataWindow User Object
 Context Sensitive Help For DataWindow Controls
 Custom User Objects
 Painting the Custom User Object
 Creating Functions For the Custom User Object
 Global Function f_copyrecord
 User Object Level Function uf_initialize
 Code For The DataWindow Control
 Code For the CommandButton
 External User Objects
 Standard Class
 Custom Class
 Notes on User Object Programming
 User Objects On Windows
 Resizing User Objects
 Time Related Existence
 Placing Non-Visual User Objects
 PARENT and THIS Pronouns
 User Object Events
 Summary
 Exercises
 Embedded & Dynamic SQL
 Introduction
 Embedded SQL - PowerBuilder Implementation
 Using Host Variables
 SELECT INTO
 The UPDATE Statement
 The INSERT Statement
 The DELETE Statement
 Cursors
 Declaring And Executing a Cursor
 Creating a Cursor
 Executing the Cursor
 Isolation Levels
 Dirty Reads
 Non-Repeatable Reads
 Phantom Rows
 The FETCH Statement
 Cursors and Transaction Objects
 Scrollable Cursors
 Updating Through a Cursor
 Deleting Through a Cursor
 Stored Procedures
 SQL Anywhere Stored Procedures
 SQL Server Stored Procedures
 Using a Stored Procedure In PowerScript
 Declaring a Stored Procedure
 Executing a Stored Procedure
 Executing a Remote Stored Procedure
 Retrieving Multiple Result Sets
 Retrieving the Value Of OUTPUT Variable
 Dynamic SQL
 Dynamic Staging Area
 Dynamic Description Area
 Format 1
 Format 2
 Format 3
 Format 4
 Handling the Results
 Summary
 External Functions
 Introduction
 Declaring External Functions
 Datatypes For External Function Arguments
 External Function Arguments
 Calling an External Function
 Calling an External Subroutine
 Multimedia and PowerBuilder
 Frequently Used MS-Windows APIs
 Copying the Specified File
 Finding the Current Directory
 Setting the Current Directory
 Creating a Directory
 Finding the Computer Name
 Setting the Computer Name
 Reading the Value of an Environment Variable
 Finding the Volume Information
 Writing to the Windows NT Event Log
 Displaying a URL in the Default Browser
 Finding the System Directory
 Finding Where the EXE is Running From
 Copying the Active Window to the Clipboard
 Copying the Full Screen to the Clipboard
 Browse For the Folder
 Closing the Current Session And Display Login Screen
 Call-Back External Functions
 Stored Procedures - External Functions
 Cross Platform Issues
 Summary
 Debugging, Tracing & Profiling
 Running Your Application
 Invoking the Debug Painter
 Views in the Debug Painter
 Call Stack View
 Breakpoints View
 Objects In Memory View
 Source View
 Source Browser View
 Source History View
 Variables View
 Watch View
 Breakpoints
 Conditional Breakpoints
 Breakpoint On a Counter Value
 Both Condition & Occurrence Breakpoints
 Breakpoint On a Variable Change
 Breakpoints in Embedded SQL
 Editing Breakpoints
 Enabling and Disabling Breakpoints
 Flow In the Debug Window
 Step-in
 Step-over
 Step-out
 Running to the Cursor
 Changing Variable Values
 Just-in-time Debugging
 Debugging Inherited Objects
 Solutions To Common Debugging Problems
 MessageBox()
 KeyDown(), GetObjectAtPointer()
 Messages
 Closing the Debug Window
 Remote Debugging
 Tracing PowerBuilder's Internal Execution
 Logging SQL Statement Execution
 Tracing ODBC Driver Manager Calls
 Tracing and Performance Analysis
 Collecting Trace Data - An Easy Way
 Performance Analysis - An Easy Way
 Class View
 Routine View
 Trace View
 Collecting Trace Data using Tracing API
 Performance Modelling Objects
 Trace Modelling Objects
 Summary
 Dynamic Data Exchange
 Introduction
 DDE Concepts
 The Registration Database
 Application
 Topic
 Item
 Links
 Hot Link
 Cold Link
 DDE Events In PowerBuilder
 Using DDE With PowerBuilder
 Importing Excel Spreadsheet Using DDE
 Importing a Spreadsheet Using Clipboard
 Using Excel Names
 Hot Link With Excel
 Mail Merge
 Mail Merge Setup In MS-Word
 Running the Example
 PowerBuilder As a DDE Server
 The RemoteExec Event
 The RemoteSend Event
 The RemoteRequest Event
 Summary
 OLE — Part I
 Introduction
 What Is Linking And Embedding?
 Linking
 One-Way Links
 Two-Way Links
 Adaptable Links
 Embedding
 Linking Or Embedding
 Traditional Model, Component Object Model
 Traditional Model
 Component Object Model (COM)
 OLE Features
 Visual Editing
 Drag And Drop
 Optimized Object Storage
 Nested Object Support
 Automation
 Version Management
 Objects Defined
 Why Implement OLE?
 DDE Versus OLE
 Which One To Choose, DDE Or OLE ?
 Deciding Where To Use DDE Or OLE
 Registration Database
 The Advanced Interface Of REGEDIT
 What Can We Do With RegEdit?
 OLE - PowerBuilder Implementation
 OLE Control
 OLE Control Attributes
 Contents
 Display Type
 Activation
 Link Update
 OLE Control Attributes
 Opening a File In a OLE Control
 Opening a Sub-Storage
 Insert Class In the OLE Control
 Inserting an Existing File In the OLE Control
 Inserting an Object In the OLE Control
 Saving the Object From the OLE Control
 Saving the OLE Control as Another File
 Linking the OLE Control's Object To a File
 Getting Image From the Database To OLE Control
 Updating the Database BLOB Column From OLE Control
 Copying the OLE Control
 Cutting the OLE Control Object
 Pasting an Object In the OLE Control
 PasteSpecial()
 PasteLink()
 Editing/Printing the Object In the OLE Control
 In-Place Editing & OLE Server Menus
 OLE Columns In the DataWindow
 OLE Presentation Style DataWindow
 OLE Control In the DataWindow
 Summary

MDI Applications
MDI is an acronym for 'Multiple Document Interface'. MDI is everywhere. You and your
users probably use them all the time and may not even realize it. PowerBuilder
environment is a MDI application, and so are most MS-Windows packages.

Basically, using an MDI application means that you can open several windows at the
same time and more over open copies of the same window. You can move, resize,
minimize, maximize, tile and cascade any or all these windows, allowing you to see
exactly the information you need, and require.

Due to the added functionality and demand of today's user, you are forced to produce
these types of applications, reproducing options that every other MS-Windows
application has to offer.

After this session you will:

 Have clear understanding of MDI applications.


 Understand Inter Object communication in an MDI environment.
 Know about how to convert 'Product Management System' a SDI (Single
Document Interface) to MDI application

Estimated Session Time:

180+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/ Professional/ Enterprise version)


installed on your computer.
 You should complete exercises given till 'PowerScript - Database Operations'
session.

Review of Course - I
This section is meant for students who know PowerBuilder to an extent and would like to
start with the 'Advanced PowerBuilder' course. If you are from course 1, you may be
interested in the diagram given at the end of this topic. It gives an overview of the
programs done in course 1.
In 'Introduction to PowerBuilder' course, students were introduced to painters and at the
end of each session, they were given exercises to complete. (S)He also learned
PowerScript basics and developed few windows, menus, DataWindows and other objects.
(S)He started developing an application called 'Product Management System', which
handles issues, receipts and returns of products. As part their project, they created a
database named product.db (product.db.zip) along with the log product.log
(product.log.zip). (S)he created a PowerBuilder library, product.pbl (product.pbl.zip), and
all objects from the previous course is stored in that PBL.

Upon downloading the files, you need to uncompress and place them in '\workdir'
directory. These files are compressed using WinZip 6.3 and so, you may need 6.3 or later
version to uncompress them. If don't have the software to uncompress, download a copy
of WinZip from http://www.winzip.com (site not managed by us). All the files are based
on Windows 95, they should work both on Windows 95 and Windows NT. The database
files are cross-platform compatible. PowerBuilder library should work fine on any MS-
Windows platform, without changes.

If you haven't placed the uncompressed files on the 'C' drive or if you have placed them
in any other directory, you need to edit w_login window's open and close events and
remove the hard code of 'c:\workdir' from the script. You need to configure ODBC from
the database painter and create a database profile product for the product.db. Before you
proceed to the next section, you may want to run the application and test the database
connectivity and also understand the functionality of the project. When you run the
application, provide the following parameters to log onto the application.

Prompt Value you need to provide

Login Name DBA

Password SQL

DBMS ODBC

Database product

The following chart gives you an overview of events/functions that have code in the
'Product Management System' application at this stage.
Home Previous Lesson: MDI Applications
Next Lesson: Introduction

Introduction
The "Product Management System" application, we've been developing until now, has
been based around single window. Whilst this works well, it would be useful for the user
to be able to see information about another products while entering new data. As it
stands, the user would have to cancel the new item number, query on the product_no,
switch to data entry mode and begin to enter afresh the new product number along with
its associated information.

This is all because the user can't open the same window more than once in the same
application. If you try to open a window that is already open, PowerBuilder simply
activates the opened window - it won't open another instance of the window.

To allow PowerBuilder to open several copies of the given window at the same time, in
the same application, we have to convert our application to use Multiple Document
Interface. We've already done some of the work required for this conversion in Menu
Painter session, when we painted some menus and we'll be showing you how to
implement these menus when we convert our application.

MDI Concepts
A Multiple Document Interface is an application style that allows users to open multiple
windows (called sheets) in a single window (called MDI Frame) and to move freely
among these sheets. The PowerBuilder development environment supports a Multiple
Document Interface, as you can open several painters at the same time and swap between
them as required:

In the above picture, we have the file editor, library and database painters open and
moreover, you can see from the Window menu option, in what order the painters were
opened and which is currently selected. You can move between the windows by simply
clicking on the window you want or by selecting the appropriate entry from the Window
menu. When you move the focus from one window (sheet) to another, menu bar and the
toolbar change, to reflect the options available for the active sheet (window).
MDI Basics
In a MDI application, there is a main window (not to be confused with PowerBuilder
window, of type Main), and other windows or sheets opened within it. This main window
can have one of two PowerBuilder window styles: MDI Frame or MDI Frame with
MicroHelp.

Typically, PowerBuilder windows of type 'Main' are opened in MDI Frame as


sheets.

Generally, MDI window has a status bar for displaying MicroHelp and menu bar. This
menu bar is displayed until a sheet is opened in the window. If the opened sheet has an
associated menu, PowerBuilder replaces the menu associated with the MDI window with
the sheet's menu. When a sheet is active, the rest of the sheets are deactivated and the title
bars of those sheets are grayed out. If there isn't a menu attached to a sheet, the menu
associated with the MDI frame remains displayed and is active for that sheet.

The MDI frame window has three parts:

 Frame
 Client area
 Sheets

Frame
The Frame Area consists of menu bar, window title and the status bar to display
MicroHelp. If you paint anything other than new sheets on the MDI Frame window, that
area will also be included in the frame area. The toolbar attached to the menu is also part
of the frame area.

Client Area
The area between an MDI Frame title bar and the MicroHelp status bar is called the
Client Area and it is here that the sheets are opened. The Client area is automatically
resized to take into account menus, toolbars and help status bars that are attached to any
of the sheets.

However, if you paint any object in the frame window, this automatic resizing is canceled
and resizing the client area has to be done programmatically.

Sheets
OpenSheet() and OpenSheetWithParm() are the two functions provided by PowerBuilder
to open sheets in the client area. Any type of PowerBuilder window can be opened as a
sheet in a MDI Frame window except, of course, another MDI Frame window. In other
words, you can't embed MDI Frames within each other.

The standard sizing, minimizing and maximizing features of windows are automatically
defined by PowerBuilder when using the OpenSheet() or OpenSheetWithParm()
functions. As an example, suppose you define a window of type Main, as non-resizable
without control menu, maximize and minimize icons. When you open this window with
Open(), all the properties you defined are retained. However, when you open the same
window with OpenSheet() or OpenSheetWithParm(), it is opened with the standard
features of an MDI sheet, which include the resizable properties and control menu,
maximize and minimize icons. Even if a response window is opened using OpenSheet()
or OpenSheetWithParm(), PowerBuilder automatically enables all the standard features.

When you close a MDI Frame, PowerBuilder first closes all the sheets that are
opened within the "MDI Frame" window and then closes the MDI Frame.

MDI_1 Control
Whenever a MDI Frame is painted and saved, PowerBuilder automatically creates a
control called MDI_1 which refers to the Client area. Use this name in your scripts to
specifically refer to the Client area, but note that unlike other controls, MDI_1 has no
events associated with it.

Note that MDI_1 is not a reserved word, so, there is nothing that can stop you from
calling a control as MDI_1. However, this isn't recommended, as it obviously creates
confusion in the script.
If you add an object to the MDI frame, the size of the client area will be affected.
Resizing of the client area has to be taken care by you, by writing script for the MDI
Frame resize event.

If you don't resize the client area in your script, users will be able to open sheets, but they
won't be visible.

When resizing the client area, you have to take the space occupied by the toolbars,
objects and the status bar into account. The WorkspaceWidth() and WorkspaceHeight()
functions help in determining the exact size of the MDI Frame.

Moving Sheets
Sheets opened in the MDI Frame are not visible if they are moved outside of the Frame
area; the sheet area outside the Frame is clipped from view. You can, however, set
vertical and horizontal scrollbars for an MDI Frame and they allow the user to see any
sheet area that is by default outside of the MDI Frame. If scrollbars are not set, the user
has to move the sheet by clicking and dragging it.

Converting to a MDI Application


The first and most obvious thing to remember when converting single window
application to MDI version is that you have to create a MDI Frame window. Invoke the
window painter and create a new window. In the window Properties sheet, select the
Window Type as MDI Frame with Microhelp. Provide "Product Management System"
for the Title prompt and m_mdi_menu for the Menu prompt. We hope that you painted
m_mdi_menu in the Menu Painter session, otherwise, revisit that session.

For a window of type MDI Frame or MDI Frame with MicroHelp you must provide
the menu name at painting time, otherwise, you can't save the window.

We could paint objects on the window, but as we said before, if you do that you have to
write script for the resize event. For now, let's continue with the standard MDI Frame.

After signing into our application, we now want to open MDI window rather than
w_product_master. We also want to allow the user to open w_product_master as a
sheet within the MDI Frame, when he selects 'Module > Product Master Maint.'
option from the menu. Replace the application object's Open event script with:

open(w_product_master)

to:

// Object: Application Object


// Event: Open
open(w_mdi_frame)

Now run the application. You will see an empty window along with a menu and a
toolbar:

Clicking right mouse button on the toolbar will display the right-click menu allowing you
to position it, close it or show the associated text.

This menu is similar to the one displayed in the development environment, which allows
you to re-position the Painterbars. PowerBuilder provides this default behavior for no
additional code.

Since, you have not coded anything for the menu option events, you can't open
w_product_master window from the menu at this point. Double-click on the control
menu to close the application.

Opening an Instance of a Window


To open an instance of a window as a sheet, we use the OpenSheet() function and a local
variable declaration. If we open more than one instance of a window, the data in each
instance is independent of the other.

We want to allow the user to open an instance of w_product_master whenever (s)he


selects 'Product Master Maint.' option from the Module menu, so we have to write script
for this menu item. Open m_mdi_menu from the menu painter, go to the script for
'Module > Product Master Maint.' If you look at events for a menu item, you'll see that
there are two events: Clicked and Selected events.
The Selected event occurs whenever a user scrolls through the menu options using
the arrow keys or the mouse pointer. The Clicked event occurs when the user clicks
on the option or presses the Enter key when the option is highlighted. This is the
event for which we want to write code.

// Object: 'Module/Product Master Maint.' menu option


// in m_mdi_menu menu.
// Event: Clicked
w_product_master l_sheet1
OpenSheet( l_sheet1, ParentWindow, 4, Cascaded!)

The first line declares l_sheet1 as a local variable based on w_product_master. The
second line calls the OpenSheet() function with the following parameters:

l_sheet1 is the name of the window which is to be opened.

ParentWindow is the name of the MDI Frame in which the window is to be opened.
When you code the script for a menu item, this parameter is always ParentWindow. You
can hard code the MDI Frame window name into the script, but, by using ParentWindow,
maintenance required is reduced in cases where name of the MDI frame window is
changed. This also gives us a chance to attach the menu to any MDI Frame window.

“4” is the menu option under which the list of opened sheets is to be displayed.
The standard place to display opened windows is the Window menu option, the second
option from the right. PowerBuilder allows you to specify this as the default when a zero
is used. We know that, m_mdi_menu doesn't have a Window option. So, the open sheet's
names would be displayed under the Module option for now. When we assign menus to
other sheets, the problem would be rectified. The reason we didn't paint Window menu
option for the m_mdi_menu menu is that, the options under the Window menu option
doesn't apply unless at least a sheet is opened in the MDI frame window. When a sheet is
opened, the menu attached to that sheet has the Window menu option and the opened
window names listing will automatically move to Window menu option from Module
menu option.

Cascaded! argument indicates how the opened sheets are to be arranged in the frame.
PowerBuilder supports three basic formats: Cascaded!, Layered! and Original!.

We declared w_product_master as a local variable and then used this in the


OpenSheet() function, as it allows us to open more than one instance of
w_product_master window. If we had used the following code and then try to open
another instance of the window it would only activate the sheet.

OpenSheet( w_product_master, ParentWindow, 4, Cascaded!)

Try running the application to see how the menu option allows you to open multiple
instances of w_product_master:
You can see from the Window menu that we have opened three instances of the same
window and the sheets are displayed in a cascaded fashion.

At this time, open w_product_master, and set the menu name to m_sheet_menu in the
Window Properties sheet. Save it and close the window.

Now, run the application. You will observe that PowerBuilder displays a new menu when
you open an instance of w_product_master by selecting 'Module > Product Master
Maint.' from the menu.

Now try to open another instance of w_product_master from the new menu, you can't do
that. That is because, you wrote the script for m_mdi_menu menu, and NOT for
m_sheet_menu. So, copy the script from 'Module > Product Master Maint.' in
m_mdi_menu to 'Module > Product Master Maint.' in m_sheet_menu.

At this point, you can see all the CommandButtons painted in w_product_master. Even if
you open ten instances of the same window, you can still see them in all windows. Don't
you think that we can get rid of all the CommandButtons and push the functionality into
the menu options. If you do it, user will be selecting the menu options, instead of clicking
on CommandButtons. Let's learn more about this user interface.

Creating a New User Interface


In the previous session 'PowerScript - Database Operations', you have learned about
events, user events, triggering events and so on. What we are going to do now is, define
user-defined events first. Then, we move the code from CommandButtons to user-defined
events. As a last step, we write code for the menu items, to trigger the user-defined
events.
Open the w_product_master window. Go to the Script editor view and select '(New
Event)' option from the second DDLB from left. Define the following events. Remember
that after this, all the new events are available at the window level, i.e.,
w_product_master.

Type the following information for the appropriate prompts. Click OK once done.

Event Name Event ID

ue_close pbm_custom01

ue_retrieve pbm_custom02

ue_query pbm_custom03

ue_add pbm_custom04

ue_delete pbm_custom05

ue_print pbm_custom06

ue_print_preview pbm_custom07

ue_printer_setup pbm_custom08

ue_export pbm_custom09

ue_sort pbm_custom10

ue_filter pbm_custom11

ue_save pbm_custom12

You must declare the user-defined events at the correct window level. If the events are
declared at w_mdi_frame level, they are not available to the w_product_master window!

This completes our step number 1, defining user-defined events. Under step number 2,
let's move the code from CommandButtons to user-defined events.

We will show you for one event, do for the rest to get experience.
 Invoke the script painter for Query CommandButton.
 Select all the code and copy it to the clipboard.
 Go to the one of the scripts for the Window itself by selecting w_product_master
from the left DDLB.
 Choose the ue_query event from the second DDLB from left.
 Paste the code and continue with other events.

Similarly, do for all the CommandButtons. Make sure you paste the code in the
appropriate user event.

Copy Clicked event script To the user-defined event at


from w_product_master

cb_retrieve ue_retrieve

cb_query ue_query

cb_add ue_new

cb_delete ue_delete

cb_print ue_print

cb_print_preview ue_print_preview

cb_printer_setup ue_printer_setup

cb_sort ue_sort

cb_filter ue_filter

cb_export ue_export

cb_save ue_save

For the ue_close event, type the following:

Close(This)
The code we have for cb_close CommandButton is: Close(Parent). Here, we changed it
from Parent to This, because, ue_close is defined at the window level. You can't use
Parent at the window event/function level, since a Window doesn't have any parent.

This completes step 2. To make sure whether you have copied code from all
CommandButtons, invoke the script painter for the window. When you click on the
'Select Script' DropDownListBox (second from left), you should see script icons before
all user-defined events, as shown in the following picture.

Once you confirm, delete all the CommandButtons from w_product_master window.

In step 3, we need to write script for the menu options.

Window l_Sheet
l_Sheet = ParentWindow.GetActiveSheet()
If IsValid( l_Sheet) then
PostEvent( l_Sheet, "user_event_name")
End If

You need to write the above code for the clicked event of the menu items shown in the
following table, in the left side column.

Clicked event of the Menu Item: Replace user_event_name with:

File > Close ue_close

File > Query ue_query

File > Retrieve ue_retrieve

File > Sort ue_sort


File > Filter ue_filter

File > Save ue_save

File > Print ue_print

File > Print Preview ue_print_preview

File > Printer Setup ue_printer_setup

Edit > New Record ue_add

Edit > Delete Record ue_delete

The code for each menu option is exactly the same. You simply replace the
user_event_name in the last line with the relevant user event name, listed in the right side
column in the above table. As an example, for File > Query menu option's code, replace
user_event_name with ue_query, and don't make any spelling/typing mistakes.

The IsValid() function is used to check the validity of the object supplied as an argument.
If you don't add this check to the code, PowerBuilder will generate a 'Null reference error'
and will abruptly halt your application, when you attempt to close down a sheet when
none of the sheets are open.

After checking for a window, the script calls the ue_close event using the PostEvent()
function.

Now is the time to run the application and test if all the coded options are working
properly or not. We are sure that, you will be finding limitation in the application. That is,
even if you maximize a sheet, DataWindow controls dw_query and dw_product are in the
same size, and the rest of the window area is not used. Don't you think resizing the
DataWindow controls automatically will make the application better?

Resizing the Controls Automatically


We can make w_product_master window more useful, by simply resizing the
DataWindow controls whenever the user resizes the window. To do this, we have to
add some code to the resize event of the w_product_master:

// Object: w_product_master
// Event: Resize
dw_query.x = 10
dw_query.y = 10
dw_query.Width = WorkSpaceWidth() - 20
dw_query.Height = WorkSpaceHeight() - 20
dw_product.x = dw_query.x
dw_product.y = dw_query.y
dw_product.Width = dw_query.Width
dw_product.Height = dw_query.Height

We are using the WorkSpaceWidth() and WorkSpaceHeight() functions to determine the


width and height of the workspace and then to resize the controls, leaving a border of 10
units around the DataWindow controls.

Arranging Sheets
PowerBuilder allows you to arrange sheets in four different ways: cascade, tile,
horizontal tile or layer. The script to do this is just one single line, so write the
following script to the clicked event, for each option under the Window menu item:

// Object: Window/Vertical in m_sheet_menu


// Event: Clicked
ParentWindow.ArrangeSheets( Tile!)
// Object: Window/Horizontal in m_sheet_menu
// Event: Clicked
ParentWindow.ArrangeSheets( TileHorizontal!)
// Object: Window/Layer in m_sheet_menu
// Event: Clicked
ParentWindow.ArrangeSheets( Layer!)
// Object: Window/Cascade in m_sheet_menu
// Event: Clicked
ParentWindow.ArrangeSheets( Cascade!)

Notice that the argument for ArrangeSheet() function is Cascade!, and for OpenSheet()
function it is Cascaded!. The same is true for Layer!, Layered! for OpenSheet(). This can
be confusing!

All four parameters only affect the sheets that are currently open and active. To arrange
minimized icons, you could add a new menu item, say 'Arrange Icons', and use the same
code with the argument Icons!.

We've now seen how to open and close instances of w_product_master window and how
to arrange sheets in the MDI frame. To test this functionality, open multiple instances of
w_product_master and select the options from the menu and see if they work or not.

Displaying About Window


One of the options available from the Help menu is About. When selected, the following
window should be displayed:
Remember that you have painted w_about window in the Window Painter
Exercises. Before we display this window to the user, we need to write some script.
So, open w_about window and write the following script for the Done button's
clicked event:

// Object: cb_done in w_about window


// Event: Clicked
Close(Parent)

This will allow the user to close the window. We can also cause the window to close
down automatically after ten seconds, if it hasn't been closed by the user. To do this,
we have to write two scripts; one for the open event and another for the timer event:

// Object: w_about
// Event: Open
Timer(10)
// Object: w_about
// Event: Timer
Close(This)

In the Open event, we are calling Timer() function. This function triggers the Timer
event at the window, after the time specified in the Timer() function elapses. Don't
get confused, there is a Timer function and a Timer event. The Timer event closes
the window after the specified time elapses. The final stage is to open the window
from the Help > About menu option. Write the following script in the clicked event
for the menu option:

// Object: m_about in m_mdi_menu AND m_sheet_menu


// Event: Clicked
Open( w_about)

Now when you run the application, you can select Help > About to bring up the w_about
window, and if you don't click the Done button, it will close automatically after ten
seconds. Remember, you need to write Open( w_about) to the Help > About in the
m_mdi_menu menu also.
Let's write code for other CommandButtons in w_about window in the later session.

Displaying a Background Logo


This section gives you concepts. Don't write the code given in this section. Displaying a
background logo looks like a simple thing, but it requires some thought. You might think
that, to display a background logo, you could paint it in the MDI Frame window itself.
However, as we've already discussed, this would disable PowerBuilder's automatic
resizing of the client area. When you take into account the fact that a logo is typically
centered, you may not be left much room to resize the sheets.

Instead, you could paint the company logo in a window, opening this window before
opening the login window. However, this doesn't solve the problem, because the Open()
function makes the window independent and when you open another sheet, this window
goes out of the MDI frame window's area. When the logo window is opened with
OpenSheet(), PowerBuilder displays the title bar, minimize and maximize icons. In other
words, the window is given functionality that draws attention to it, rather than fading it
into the background.

A Response window wouldn't work either, because the user can't work with the
application as long as the response window is open.

This only leaves a Child window or a Popup window. Only in these two type of windows
you disable all the attributes, such as border, titlebar, control menu, min/max icons, close
icon, enable property, menu name, etc... If you open a window with OpenSheet()
function, the window name will be displayed under one of the menu bar items, which
allows it to be selected by the user. We don't want this to happen, so we have to make do
with the Open() function.

Create a new window and turn off all its attributes except the visible attribute and make it
a Child window. Paint the company logo on the window.

Save the window as w_logo and in the open event script for w_mdi_frame window, call
this logo window with the Open() function. Try running the application, you'll see that
this does work.

However, there are still couple of problems. Whenever MDI Frame is resized, w_logo
window and the logo within it are not resized accordingly. Also, if you open other sheets
in the MDI Frame, they won't be visible because, the child window will always be on top
of any opened window.

We can solve the first problem by writing script for the resize event of the MDI
Frame, but this won't solve the second. The best solution is to display the logo when
we open the MDI main window and then turn its visible property off when we open
a sheet. We can then check for open sheets, after we run the File > Close menu
option script. If there aren't any, turn the visible property back on. We would also
need to add the following line to the start of the script for 'Module > Product Master
Maint' menu option:

w_logo.visible = FALSE

We can then create a new user event ue_check_for_active_sheets at the MDI


window level and assign it a custom Event ID. We then write the following code for
this event:

Window l_Sheet
l_Sheet = GetActiveSheet( This)
If not IsValid( l_Sheet) Then w_logo.visible = True

and call this event from the File > Close menu script using:

PostEvent( ParentWindow, "ue_check_for_active_sheets")

Minimizing Application When Idle


Sometimes users may leave their desk without logging out. If the application contains
confidential information, there is a possibility that the data could be seen by someone
who shouldn't. You could rely on the Windows screen saver, but it is best to handle this
from within the application.

Do it by disconnecting from the server and minimizing the application if it is idle for
more than five minutes. In a multi-user environment, it is better to disconnect from the
server whenever it is not needed for an extended period of time.

Add the following line to the open event of the application:

Idle( 300) // Write this after Open( w_login)

This calls the idle event after 300 seconds or 5 minutes of inactivity. In the idle event
for the application, write the following script:

// Object: Application
// Event: Idle
w_mdi_frame.WindowState = Minimized!

Test to see if the code works, by running the application and logging onto the database
successfully and leaving it alone for five minutes.

Customizing Toolbars
When you click on any toolbar icon with the right mouse button, PowerBuilder displays a
pop-up menu and allows you to change the toolbar position, show/hide the toolbar text
and even the toolbar itself.

Once the toolbar is hidden by deselecting the Sheetbar option, there is no default
method to display the toolbar again; hence, you need to provide a menu option to
display the toolbar. That's the purpose of the menu option 'Window > Toolbars' in
the m_sheet_menu. Add the following code to the Window > Toolbars menu option:

// Object: Window/Toolbars in "m_sheet_menu" menu.


// Event: Clicked
If ParentWindow.Toolbarvisible = true Then
ParentWindow.Toolbarvisible = False
This.Text = "Show Toolbar"
Else
ParentWindow.Toolbarvisible = True
This.Text = "Hide Toolbar"
end if

This simply checks if the toolbar is visible and toggles between on or off, and changes
the text displayed in the menu option at the same time.

This code has its disadvantages. If you show/hide the toolbar by selecting this option, it
works fine. However, when we try to hide toolbar from the pop-up menu, we expect the
menu item text to change accordingly, but it won't. To solve this problem, add the
following code to the Window menu option:

// Object: Window option in m_sheet_menu menu.


// Event: Clicked
If ParentWindow.Toolbarvisible = true Then
Parent.m_window.m_toolbars.Text = "Hide Toolbar"
Else
Parent.m_window.m_toolbars.Text = "Show Toolbar"
end if

To select Toolbar option from the menu, the user has to click on the Window menu
option. At this point just change the text, so that the user always sees the correct menu
option.

There is always a possibility that the user may press the Alt key to activate the
menu, rather than clicking on it. For it simply add this one line code in the selected
event of the menu bar item:

// Object: window in m_sheet_menu menu.


// Event: Selected
TriggerEvent(This, Clicked!)

Scoping Variables
Variables can be declared at four different places, depending on the scope you wish to
associate with them. A variable's scope indicates its lifetime and the influence it can have
over the rest of the application.

There are four levels of scope:

 Local
 Global
 Shared
 Instance

Local Variables
Do you know what type of variables we were using till now? Right!, Local Variables.
Local variables are declared in the script itself and they are available only in the script
where they were declared. This variable can't be referred in any other events or functions.
For example, if you declare a variable in the Clicked event of a CommandButton, they
are available only in that event. It is not available even if you try to access that variable,
say, from rButtonDown event of the same object. Local variable comes into existence
only when the script for the event/ function starts executing and is destroyed upon the
completion of the script.

Global Variables
Did you know that we used few global variables till now and that too without even
declaring them. They are SQLCA and ERROR object. These two are built-in global
variables. You can declare global variables by selecting 'Declare' option from the left
DDLB in the script painter and selecting 'Global Variables' option from the second
DDLB.

Global variables can be referred from anywhere in the application. They become
available when the application is opened and are destroyed when the application is
closed. Use Global variables only when absolutely necessary and take care while naming
them.

Shared Variables
Shared variables are stored in an object's definition and are available for all instances of
the object. The first instance of the variable is initialized, when an object that has the
shared variable is opened, and exists until the close of the application. The value of the
variable persists even if the object is closed.
One example of how you could use this type of variable is when you want to count the
number of times an object has been opened in an application. For each instance that is
opened, the shared variable is incremented by one and the destruction of the instance has
no effect on the value or on the existence of the variable itself.

 If you open multiple instances of an object, the same value is used by all
instances.
 If the value of the variable is changed in one instance, the change will be reflected
in all other instances of the object.
 The value of the variable persists, even after all instances of the object are closed.

Shared variables can be declared for an application, menu, window and user objects.
Shared variables are declared in the same way as Global variables, but you need to select
'Shared Variables' instead of 'Global Variables' option.

Instance Variables
Instance variables are created when an instance of the object is created and is destroyed
when the object's instance is destroyed. If more than one instance of an object is opened,
the value of the instance variables can be different in each case.

Instance variables can be declared for application, menu, window and user objects. The
procedure for declaring instance is same as global and shared variables. When declared
for windows or user objects, they are available to the controls which are part of the
object. For example, if you declare an instance variable at the window level, you can
refer to it in the script/function of any control that is placed on that Window. .

Communication between Objects


When there are many objects in an application, you will probably want them to
communicate, share variables and pass values among themselves. In a procedural
programming language, it isn't a problem because, you either have one long program or a
main routine with several subroutines and in the subroutine calls you pass the variables as
parameters.

In an event-driven language, the process of communicating between objects is complex


because of the fact that you don't always know what the user is going to do next. To make
it easy, PowerBuilder provided us several different ways to communicate between its
objects. We'll look at each of them, highlighting some of the pros and cons for each
method.
Global Variables
As discussed in the previous section, a global variable can be accessed by any object in
an application. This is the easiest way to pass values between objects, but as we've said
before, they should be used sparingly.

Unless you adhere to a strict set of naming convention, global variables can cause
problems when integrating modules developed by more than one development team.
Another disadvantage is that they can be changed from anywhere in the application and
since they exist throughout the duration of an application, you may not be using the
memory efficiently.

Referencing Window Variables Directly


Instance variables are declared at the object level and are considered as the
attributes of the appropriate object. You can therefore refer an instance variable as
any other attribute of an object.

// Standard window attribute


w_product_master.visible = TRUE
// Instance variable attribute
// w_product_master.InstanceVarName = FALSE

Instance variables can be declared with different access levels (We will be learning that
in later sessions). To refer an instance variable from another object, it must be declared as
Public access. This is the default access level, so worry about it only if you change the
access level. It is a good idea to restrict access to these instance variables by providing
methods which act on them and allow other objects to access them through these methods
only.

Opening Window with a Parameter


For windows, sheets and user objects, you can pass parameters by using

 OpenWithParm()
 OpenSheetWithParm()
 OpenUserObjectWithParm()

functions, rather than just the standard plain old opening functions. If you are
working with PowerBuilder windows, you can also return values by using the
CloseWithReturn() function instead of the Close() function. The syntax for these
functions are as follows:

OpenWithParm(<window name>, <parameter>, {Parent})


The only disadvantage to this method is that you can only pass one parameter. We can
get around this by using structures.

Structures
Structures are basically collections of variables/objects that are defined in the structure
painter. If you know 'C' language, then think it as a structure in 'C' and if you are from
'COBOL' background, then think it as a record with no sub-levels.

To declare a structure, invoke the structure painter by selecting File > New menu option
and double clicking on the icon and define the variable/object name and the datatype.
The object can also be another PowerBuilder structure. When you save a structure, it
becomes a PowerBuilder object.

In functions like OpenWithParm(), you can pass only one parameter. Sometimes you may
need to pass more than one parameter. If that is the case, use Structures. When you pass
the Structure as the parameter, the opened object can access all members in the structure.

The Message Object


Till now we talked about sending parameters to the window while opening it, but how
does the opened window access those parameters? It is possible by the Message object.
When you use functions like OpenWithParm(), the parameter value is stored in a
Message object. A Message object is a built-in global object that can be used as a carrier
for the parameter. You can send one of the following data types through the Message
object:

Data Type Stored in

String Message.StringParm

Numeric Message.DoubleParm

PowerBuilder Object Message.PowerObjectParm

The parameter is stored in the relevant Message object attribute, depending on the
data type of the parameter. You can then refer to the parameter in the calling object
by using the Message. notation. For example:

// DataWindow idw_2_save //Instance variable


idw_2_save = Message.PowerObjectParm
A possible problem associated with the Message object is that, all currently running
applications can generate messages, so there is a chance of the Message object being
overwritten.

A solution to this problem is, to declare an instance or local variable in the called object
and then in the very first statement of the object's open event, assign this variable with the
Message object attribute.

Calling Functions
You can call functions declared in another object by using the following syntax:

<Object Name>.<Function Name>(Parameter1, Parameter2, ...)

Keep in mind that, like instance variables, functions declared at the object level have an
access level, so they must be declared as Public to be available to other objects.

Triggering or Posting Events


Calling the TriggerEvent() or PostEvent() functions is a convenient way to execute the
scripts of other windows or window control events. These functions allow you to write
script in one place and execute them any number of times from other objects.

We've already used some of these methods of communication earlier in the session. We
can't recommend any single method to be the best, as each have their own merits and can
be used in many diverse circumstances. The advice we can give is, try various methods
and find the one with which you feel comfortable.

Displaying Popup Menu on the


DataWindow
You have learned about variable scoping in the previous section. Remember that we
painted m_popup_menu menu in the Menu Painter session's exercises. We can use that
menu as the popup menu for the DataWindow dw_product. Whenever the user clicks
with the right mouse button on the DataWindow, invoke the popup menu at the place
where the mouse pointer was.

To achieve this functionality, declare an instance variable at the w_product_master


window. Open w_product_master and select Declare option from the left DDLB in
the script painter and select 'Instance Variables' option from the second DDLB.
Type the following declaration:

m_popup_menu i_popup_menu
The above statement declares an instant variable i_popup_menu of type m_popup_menu.
This declaration is similar to declaring any traditional datatype variables. For ex: Int I
declares a variable I, of Integer datatype. Close the dialog box by clicking on the OK
CommandButton.

Now, if you try to use the variable i_popup_menu, you will get "Null Point
Reference" error. That is because, we haven't created any instance of the popup
menu. Using an object variable is little different from using a traditional datatype
variable. When we declare an integer variable, we can start using that variable
immediately, i.e., you can assign a value to the variable. In case of object variables,
you have one more step to complete before you actually start using the variable.
That is, creating that variable by using the CREATE statement. Append the
following statement to the Open event for the w_product_master window.

// Object: w_product_master window


// Event: Open
i_popup_menu = CREATE m_popup_menu

CREATE statement creates an instance of a specified object and assigns it to the


variable specified on the left-hand side. With this step, you have declared and
created a menu instance variable. Now, let's display the popup menu. Type the
following command:

// Object: dw_product in w_product_master window


// Event: rButtonDown
i_popup_menu.PopMenu( PointerX( Parent), &
PointerY( Parent))

The above statement displays i_popup_menu by calling PopMenu() function. PopMenu()


function takes two parameters: X and Y co-ordinates of the screen where the popup menu
is to be displayed. PointerX() and PointerY() functions return the mouse's X and Y co-
ordinates for the specified object.

Now, run the application, retrieve the data and click with the right mouse button on
dw_product DataWindow. You will see the popup menu. You may find it different
compared to the popup menu you see in the PowerBuilder's development
environment. The difference is being the display of DataWindow menu bar. We
don't want to display it in the popup menu. Right? Yes, we can do so with a simple
change. Change the above code as follows:

// Object: dw_product in w_product_master window


// Event: rButtonDown
i_popup_menu.m_datawindow.PopMenu( PointerX( Parent), &
PointerY( Parent))

In the first example, we are calling the PopMenu() function at the menu level, so,
PowerBuilder displays the whole menu. To display menu items under a specific menu bar
item, you need to invoke the PopMenu() at that level. In the second example, we are
calling the PopMenu() function at m_datawindow, which is the name for DataWindow
menu bar item. Now, run the application and see how it works.

Now, we need to write the code for the menu options. We already have code for
sorting, filtering, etc… So, why not use that code.

ParentWindow.TriggerEvent("ue_sort")

Write the above code to the Sort menu option in the m_popup_menu menu and as for the
rest, find what to write from the table below.

Menu Option Replace ue_sort with the following:

Filter ue_filter

Save ue_save

Save As ue_export

Print ue_print

Print Preview ue_print_preview

Print in Black & White Records No code now. Let's write in the next session.

We don't need to write code for "Cancel this Popup Menu" menu option, because, when
user select that option, we do nothing. Remember the popup menu's behavior, once the
menu option is selected, the menu disappears and executes the script for the appropriate
event. If there is no code, as expected, the menu will disappear and does nothing.

Do you see the difference between SDI (Single Document Interface) and MDI (Multiple
Document Interface)? A great interface, isn't it?

We forgot one thing. We didn't write script to exit from the application. Write the
following code for the Exit menu option in m_sheet_menu and m_mdi_menu menus.

Halt Close

Summary
This session explained the concepts of Multiple Document Interfaces and started the
process of converting our 'Product Management System' application to the MDI version.
We finished off with some nice touches related to the toolbars and menu options that can
be added to the application. In the next session we'll introduce you to DataWindow
internals, events, advanced DataWindow scripting, etc…

Review Questions & Answers


Q: What is mdi_1 object?

Ans: mdi_1 object refers to the client area in a MDI! or MDIHelp! type window.

Q: Which function opens a window as a sheet in a MDI window?

Ans: OpenSheet()

Q: Which types of window can be opened in a MDI window?

Ans: All window types except MDI! and MDIHelp! types.

Q: What will open if you open a Response window in a MDI window?

Ans: It behaves like a Main window.

Q: Which function arranges all the sheets in a MDI window in the specified order?

Ans: ArrangeSheets()

Q: From a menu script, how do you refer to the window that is associated with the
menu?

Ans: ParentWindow

Q: Which function gives reference to the next sheet in the MDI window?

Ans: GetNextSheet()

Q: Can I issue OpenSheet() function from outside the menu script?

Ans: Yes.

Q: Can my application have more than one MDI/MDIHelp type windows open at
the same time?

Ans: Yes, you can open any number of MDI/MDIHelp windows in a PowerBuilder
application.
Q: What will happen if I open a window as a sheet (in a MDI window) that has no
menu assigned to it?

Ans: The MDI window's menu will be displayed and acts as a menu to the sheet
window.

Q: GetNextSheet() returns an error if there is no next sheet available in the MDI


window. Is it true?

Ans: No. GetNextSheet() returns NULL if there is no next sheet. You need to use
IsValid() to validate the return value of GetNext() function.

Q: What will happen if both MDI window and the sheet has a separate menu?

Ans: When no sheet is active, PowerBuilder displays MDI window's menu and
toolbar. If a sheet with a menu is active, PowerBuilder displays only sheet's menu
but both MDI window and sheet toolbar.

Q: What will happen if you close the MDI window?

Ans: PowerBuilder continues executing with the next line after the Open() function
that opened the MDI window. A typical application opens MDI window from
Application object's Open event and has no code after that function call; so the
application ends.

Q: What happens if I place any window control in a MDI window?

Ans: Unless you write script to resize the MDI client area mdi_1, no sheet that you
open with OpenSheet()/OpenSheetWithParm will display in that MDI window.

Exercises
Please complete the following exercises. We advise you not to download the solutions till
the completion of exercises.

1. We wrote script in the clicked event of each menu item under Module option to
invoke the respective window as a sheet. As you might have observed, we have
few lines of code that are repeated in these events. What we like you to do is,
write a generic function for the menu object to instantiate the given window and
display it in the menu's parent window, i.e., mdi frame window. Once the function
is ready, change the script of all menu items (menu items under Module option)
clicked events to call this function with appropriate window name.

Tip: You can use the second format of OpenSheet() function.


Solution: ex-10-1-m_mdi_menu_asi.srm
2. Similarly, we wrote script to the clicked events for other menu items such as
Save, Sort, etc.. What we did was to fire appropriate event in the active sheet.
What we like you to do is to channel this functionality as you did in the first
exercise.

Tip: None.

Solution: ex-10-2-m_sheet_menu_asi.srm

Advanced DataWindows – Part I


In the previous session you have learned about creating a great user interface, MDI, to the
'Product Management System'. Prior to that you have learned about getting data from the
database and managing the data.

After this session You Will:

 Understand DataWindow Internals, i.e., what's happening behind the scenes.


 Understand DataWindow Control Events.
 Be able to validate the Data in the DataWindow.
 Be able to update multiple tables in a single DataWindow.
 Be able to use DropDownDataWindows, etc…

Estimated Session Time:

240+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/ Professional/ Enterprise version)


installed on your computer.
 You should complete all the exercises given till 'MDI Applications'.

DataWindow Buffers
Till now we said that, PowerBuilder retrieves data according to the data source definition
and displays the data according to the DataWindow format. That is still right, but how
does PowerBuilder maintain them internally. Knowing DataWindow internals gives you
a better understanding and hence, you can code more efficiently.
When we call the Retrieve() function, PowerBuilder retrieves data from the data source
and stores it in the buffer in memory. PowerBuilder then reads the data from buffer and
formats & displays it in the DataWindow control.

Internally, PowerBuilder maintains one edit control and four buffers for each
DataWindow control. The buffers are:

 Original
 Primary
 Deleted
 Filtered

Let's learn about buffers first. When data is retrieved from the database by using
Retrieve() function, it is retrieved into the Primary buffer and it is the contents of this
buffer that are displayed in the DataWindow:

PowerBuilder always displays the data in DataWindow control from the Primary Buffer.
Data from other buffers are not visible to the user. Of course, a programmer can
manipulate data in those buffers via PowerScript.

Adding Rows
As you learned previously, rows can be added to the DataWindow by using the
InsertRow() function (there are other ways also, we'll tell you later). InsertRow(),
ImportFile(), ImportString(), ImportClipboard() functions adds row(s) to the Primary
Buffer; but it has no effect on other buffers (See the following figure.).

Deleting Rows
When you delete a row with the DeleteRow() function, rows are transferred from
Primary buffer to Deleted buffer. For example, If you delete Product No: 5, buffers
would look like this:
Filtering Rows
When rows are filtered using the SetFilter() function, all rows that don't match the filtered
condition are moved to the Filtered buffer. If we filter on the criteria that the product_no
must be greater than 3, the buffers would look like this:
Modifying Rows
If you modify a row in the DataWindow, the row in the Primary buffer is modified and
the original value is copied into the Original buffer. Suppose we change the description
of product_no 4 from "Desk" to a "Table lamp", the buffers would look like this:
Note that this is the case only for rows that were originally retrieved from the database. If
we modify product_no 6, nothing would be copied to the Original buffer, because this
row was added to the results set after the data was retrieved from the database.

The changes made to the data will not be reflected in the database until the Update()
function is called. Whenever you call the Update() function, PowerBuilder creates
appropriate SQL statements such as INSERT, DELETE, UPDATE statements and sends
them to the database for execution.

PowerBuilder creates the database statements (such as INSERT, DELETE, UPDATE)


depending upon the row status. Row Status? A New term. Let's see what it is.

Each row and column in the DataWindow have certain statuses. When PowerBuilder
retrieves data from the database, all the rows are in the NotModified! status. Please note
that, the status is an enumerated datatype, i.e., ends with an exclamation mark. If you
modify the value of a column in a row, PowerBuilder changes the row status to Modified!
status. Now, say, you insert a record in the DataWindow. As you know, a freshly inserted
row through InsertRow() has no data. In that case, PowerBuilder assigns, New! status to
that row. When you put data in any of the columns in the newly added row, PowerBuilder
changes the row status to NewModified!.

We said that, if you change a row, PowerBuilder copies the original value to Original
buffer from the Primary buffer. Note that, PowerBuilder copies data only when you
modify the row for the first time, and not every time. For example, if you have a
DataWindow defined on product_master, as shown below. Say, you change the
product_description column. PowerBuilder copies that row from Primary buffer to
Original buffer because, you changed it for the first time; it doesn't matter which column
you change. Now, say you change the product_reorder_level. PowerBuilder will not copy
this time, because it already copied the values.

Now, let's see how PowerBuilder knows which SQL statement to create. What happens is
that, PowerBuilder looks into each buffer one at a time.

Buffer Row Status PowerBuilder generates & sends the


following statement to the database
 
Deleted Buffer DELETE
 
Filter Buffer - NONE -

Primary Buffer ( New Record NewModified! INSERT


with some data in, at least in one
field)

Primary Buffer (New Blank New! - NONE -


Row)

Primary Buffer (Changed Rows) DataModified! UPDATE

Primary Buffer (Not Changed) NotModified! - NONE -

From the above table, you can observe that, PowerBuilder doesn't take any action if it is a
new blank row with no data. But, PowerBuilder generates an INSERT statement, if the
record is new and has data in at least one field.

Edit Control Buffer in the DataWindow


Control
Each DataWindow control has one Edit Control. It contains the current column's data.
For example, the cursor is in the product_no column, then Edit Control contains the value
of product_no for that row. When you press the tab key and move to the
product_description column, PowerBuilder copies product_description column’s value
for that row from the Primary buffer into the Edit Control.

For example, say, description for Product No: 1 is "Hard Disk". When product no:1 is
retrieved from the database, both primary buffer and edit control (assuming that the
cursor is in the product_description column) have the same value. You also see the same
data on the screen.

Now, If you change the content of product_description, to say, "Printer", you see
"Printer" on the screen and the value in the Edit Control would be "Printer". That means,
the changed data is automatically reflected in the Edit Control. As long as the cursor
remains in that column, the data you see on the screen and the content of the Edit Control
are same; but, the value in the primary buffer has the old data, "Hard Disk".

When you press the TAB key, or go to other row, PowerBuilder validates the data in Edit
Control and copies it to the Primary buffer, if the data passes the DataWindow
validation. As explained in the buffers section, even before PowerBuilder copies the data
from the Edit Control to the Primary buffer, it copies the row from the Primary buffer
to the Original buffer, if it is the first value to be changed for that row.

DataWindow - Data Validation


The following diagram illustrates PowerBuilder's data validation in the DataWindow.

DataWindow Events
Till now, most of the code was written for CommandButton's Clicked event. Similar to a
CommandButton, a DataWindow Control also has many events. The following sections
cover DataWindow Control's important events in detail.

RetrieveStart Event
This event triggers as soon as PowerBuilder gets notification from the database saying "I
processed your query and this is the result set you are waiting for". Upon successful
execution of this event’s code, PowerBuilder starts retrieving the result set.

This event has no parameters and returns zero by default meaning ‘continue processing’.
For any reason if you do not want to retrieve the result set, then return 1 from this event.
By default, each time you call Retrieve() function, PowerBuilder clears all the existing
data in the DataWindow, i.e., previously retrieved data, any changes you have done to the
retrieved data, and new rows you inserted after previous Retrieve()and populates the
DataWindow with the results of Retrieve() function. For any reason, if you want to keep
the existing data in the DataWindow and want to append the new result set, just return 2
from this event.

The following code displays a message on the status bar, just before PowerBuilder
starts retrieving the data. To understand the behavior of return value, you may
want to append RETURN statement with different return values as explained
earlier to the following code and execute it.

// Object: DataWindow dw_query in w_product_master window


// Event: RetrieveStart
w_mdi_frame.SetMicroHelp( "Starting to Retrieve Rows…" )

RetrieveRow Event
This event triggers immediately after retrieving each row and before displaying it on the
screen. This is the right place to write the code, if you want to do some process on the
row, before the user sees the row on the screen.

Writing code in this event and setting the Asynchronous option of the transaction object
to TRUE, makes PowerBuilder to display the row on the screen, as soon as the code in
this event is executed. For example, say the result set of a query has 10,000 rows. User
will see the data on the screen only when PowerBuilder completes retrieving 10,000th
row*. By writing code in this event (even a single line comment), user will see the data
on the screen as soon as the first row is received by PowerBuilder (of course, code for
this event is executed before it is displayed on the screen).

By writing code or comment increases the total time required to retrieve the result set. To
reduce the time, you might want to write the code in the RetrieveEnd (explained later),
which is executed after retrieving all the rows in the result set. Writing code for the
RetrieveRow or RetrieveEnd depends on the application requirement.

* There is an exception to this behavior. If RetrieveAsNeeded option is set, PowerBuilder


displays data as soon it completes retrieving one screen full of data. Whenever user tries
to scroll down the screen, another screen full of data will be retrieved. PowerBuilder,
depending on the DataWindow control size at run-time determines the number of rows
that fit on a single screen.

Another exception is, you should not use aggregate functions—such as Sum(), Avg(),
Min(), Max(), Count()—etc in the DataWindow and should not set the sort criteria in the
DataWindow. In this situation, PowerBuilder needs the full result set in order to calculate
the aggregate functions or to sort the data.
The following code displays the number rows retrieved on the status bar like a
counter.

// Object: DataWindow dw_query in w_product_master window


// Event: RetrieveRow
w_mdi_frame.SetMicroHelp( "Rows Retrieved: " + &
String( row ) )

RetrieveEnd Event
This event triggers as soon as the last row of the result set is retrieved. The total
number of retrieved rows is passed to this event as a event parameter, RowCount.

// Object: DataWindow "dw_query" in w_product_master window


// Event: RetrieveEnd
w_mdi_frame.SetMicroHelp( "Rows Retrieved: " + &
String( RowCount ) + &
". Query execution complete.")

PrintStart Event
This event occurs as soon as PowerBuilder starts printing. The total number of
pages that are going to print, PagesMax, is passed as parameter to this event. You
might want to ask the user whether the printer has that many papers in the tray or
not. However, you can’t stop printing from this event programmatically, instead
you need to code in the PrintPage event.

// Object: DataWindow dw_product in w_product_master window


// Event: PrintStart
w_mdi_frame.SetMicroHelp( "Starting to Print…" )

PrintPage Event
This event occurs for each page printed (just before the page gets printed. Please
note that, RetrieveRow event occurs after the row is retrieved by PowerBuilder, but
before it displays on the screen). This event gets PageNumber that will be printed as
well as the number of the copy, Copy as parameters to this event.

// Object: DataWindow dw_product in w_product_master window


// Event: PrintPage
w_mdi_frame.SetMicroHelp( "Printing Page # " + &
String( PageNumber) )
In fact, you need not code the above script, because, when you call DataWindow's Print()
function with TRUE as an argument, PowerBuilder automatically displays the page
number being printed and also allows you to cancel if needed.

If you want to skip printing a particular page, return 1 as the return value from this event.

PrintEnd Event
This event occurs as soon as the last page is printed. The number of pages printed,
PagesPrinted, comes as a parameter to this event.

UpdateStart Event
This event occurs just before PowerBuilder starts sending changes back to the database.
Return 1 if you want to cancel update.

UpdateEnd Event
This event gets triggered as soon as the changes are applied to the database. The number
of rows inserted, updated and deleted is passed as parameters to this event.

EditChanged Event
This event gets triggered whenever user types something in the current field (irrespective
of its contents being changed or not). You wouldn't want to code for this event, unless
you want to validate each key the user presses. Do not confuse EditChanged event with
ItemChanged event; EditChanged event triggers whenever user types, where as
ItemChanged event triggers, whenever data is changed and the current field looses its
focus (clicking on other field or other control, pressing tab, etc.).

ItemChanged Event
This event occurs whenever the data is changed and the current field looses its focus
(clicking on other field or other control, pressing tab, etc.). Use this event to validate the
data and trigger ItemError event, whenever there is an error in the data. This is one of the
frequently used events in the DataWindow control. This event gives access to the old data
(which is retrieved from the database), as well as the new data (the data changed by the
user).

Let's write some validation code in the event. For example, let's display an error message,
if the user enters an existing product_no, while entering a new record. Actually, database
gives an error message if duplicate product_no is entered, because, product_no is a
primary key for the product_master table. The reason we would like to validate here is, to
display the error message as soon as the user enters a duplicate product_no, instead of
waiting till the whole record is entered and sent to the database. Which means that we
need to check for the existence of the product_no, as soon as the user presses the tab key
in the new record.

This check can be done by using embedded SQL or with a hidden DataWindow control.
We didn't teach embedded SQL yet, so, let's go with the second method.

Place a new DataWindow control in the window and name it as dw_product_check. Go


to the properties dialog box for this DataWindow control and assign d_display_product
DataWindow object to this control. In case you don't remember, d_display_product takes
an argument product_no and brings the data for the given product_no. If you don't get
any results for the given product_no, it means that the product_no is not existing.

We find no reason to display dw_product_check DataWindow control to the user, so, turn
off its Visible property.

Do you remember what we should do before we do any database operation on this


DataWindow control? I am sure you guessed it by this time! We need to set the
transaction object. Write the following code:

// Object: Window w_product_master


// Event: Open
// Append the following line to the existing code.
dw_product_check.SetTransObject( SQLCA )

Write the following code in the ItemChanged event for the dw_product
DataWindow.

// Object: dw_product in w_product_master Window


// Event: ItemChanged
If This.GetColumnName() = "product_no" And &
( This.GetItemStatus( Row, 0, Primary! ) = New! Or &
This.GetItemStatus( Row, 0, Primary! ) = &
NewModified! ) Then
dw_product_check.Retrieve( Integer( Data ) )
If dw_product_check.RowCount() = 1 Then
MessageBox( "Error", "Product No: '" + Data + &
" ->" + &
dw_product_check.GetItemString(Row, &
"product_description") +&
"' already exists.", StopSign!, OK!,1)
Return 1
End If
End If

This code works fine here, because, product_description column is defined as NOT
NULL in the database, meaning product_description column always contain
something and it is never null. If the column allows NULL value, and if the content
of the product_description for that product is NULL, then the MessageBox() will
never display, because you are sending a NULL value as the parameter to the
MessageBox(). If you want to learn about how the MessageBox() function behaves
when the message text is NULL value, Place a CommandButton in
w_script_practice and write string ls_null; MessageBox( "Test", ls_null), run that
window and click on that button. You will never see that message box being popped
up.) If that is the case, call GetItemString() on a separate line and check for the
NULL value, substitute with appropriate message and then display it. For ex:

String l_desc1
l_desc1 = dw_product_check.GetItemString( Row, &
"product_description" )
if IsNull( l_desc1 ) Then l_desc1 = "Not Defined."
MessageBox( "Error", "Product No: '" + l_desc1 + &
"' already exists, StopSign!, OK!,1)

The first IF statement is checking for two things. One, whether the user is tabbing out of
product_no column or any other column. We are interested in it only if the user is leaving
product_no column. GetColumnName() returns the current column name. Just FYI,
GetColumn() returns the column number.

As you have learned in the "DataWindow Buffers" section, a row can be in one of the
four statuses. New!, NewModified!, NotModified!, DataModified!. Typically, user might
enter the product_no, since it is the first column in the DataWindow. When the row is
inserted, it is in the New! status and remains in the New! status till the user presses the
tab and the ItemChanged event completes the script execution successfully. That's why
we check for the New! status.

Sometimes, user might enter data in the new record and navigate to other records and
come back to change the product_no. For that reason, we need to check for the
NewModified! status.

GetItemStatus() gives the status of the specified column, in the specified buffer. If you
specify the column number as 0 (zero), it returns the row status, instead of the column
status. The third argument is the buffer name. Here, we are interested in the Primary
buffer.

PowerBuilder sends the data entered by the user as parameters to this event in the Data
variable. Remember that it is always in the string format. We need to convert into
appropriate formats. The next line is bringing data from the database. Observe the
datatype conversion of the Retrieve() function parameter. RowCount() function returns
the number of rows in the specified DataWindow. If the row count is greater than zero, it
means that the given product is existing in the product_master. (Here, we are checking
for 1 because, the product_no in product_master table is unique, meaning product_master
would never have multiple records for a given product_no. Checking for >0 instead of =
1 will also serve the purpose; It is useful when you are expecting one or more rows.)
Then we are displaying the error information by using the MessageBox() function.

The last line Return 1 is very important. This statement tells PowerBuilder, what it has to
do after completing the script execution.

Return Code Action

0 (Default) Accept the data value.

1 Reject the data value and don't allow the focus to change.

2 Reject the data value, but allow the focus to change.

If the return value is 0 (Zero), PowerBuilder triggers ItemFocusChanged event and


executes the script for that event and finally, the focus changes to the next column as per
the tab order. In this case, product_description is the next column.

If the return code is 1, PowerBuilder triggers ItemError event and executes the script for
ItemError event and the cursor remains in the current field i.e., product_no. Return code 2
is similar to code 1, except that, it changes focus to the next field in the tab order.

At this point, run the application, add a new record and enter an existing product_no and
press the tab key. You will see two error messages. We were expecting only the
MessageBox() function we placed in the ItemChanged event. Then where did the other
message come from? The next event ItemError will solve this mystery.

ItemError Event
This event gets triggered when validation rules fail (the data doesn't pass the validation
rule), and also when the ItemChanged event returns non-zero return code. If there is no
code in this event by default, PowerBuilder displays an error message in the default
format, and rejects the data and then the cursor remains in the current field.

You can tell PowerBuilder to display error messages or not, to reject data or not, by using
proper return codes.

Return Code Action

0 (Default) Reject the data value and show the error message box.
1 Reject the data value with no message box.

2 Accept the data value.

3 Reject the data value, but allow the focus to change.

Now, you know why PowerBuilder was displaying two error messages. Did you find the
solution for the problem from the return codes above. If not, just type Return 1 in the
ItemError event for the dw_product DataWindow. Now, run the application and test it.

It works perfectly. Do you know what we did? Since we are displaying error message in
the ItemChanged event, we are preventing the default error message from the ItemError
event with the return value 1. However, we fixed one problem and started another.

We have two situations. Display an error message in the ItemChanged event, and don't let
PowerBuilder display an error message in the ItemError event, OR, don't display an error
message in the ItemChanged event, and let PowerBuilder display its default error
message in the ItemError event.

We are displaying our message only for product_no. We are NOT displaying for other
fields and for failed validation rules. So, let PowerBuilder display it's own message.

"Return 1" doesn't display the error message. That means we are telling PowerBuilder
never to display error messages in the ItemError event, rather than telling it to display
conditionally. With the above code, if the user enters a negative product_balance value, it
doesn't pass the validation rule, but, PowerBuilder is not going to display any message.
Now, let's display error messages in the ItemError event, conditionally.

Declare an instance variable as shown below. To declare an instance variable,


display the popup menu in the Script editor and select 'Go To/Instance Variables'
menu option and you can declare those variables in the script area.

Boolean i_DoNotDisplayErrMsg = FALSE

Insert the following code, just above the "Return 1" statement in the ItemChanged
event.

i_DoNotDisplayErrMsg = TRUE

Write the following code in the ItemError event for the dw_product.

// Object: DataWindow_product in w_product_master


// Event: ItemError
If i_DoNotDisplayErrMsg Then
i_DoNotDisplayErrMsg = False
Return 1
Else
Return 0
End If

To test this, run the application. In a new record, enter an existing product_no and see
how many error messages you get. Enter 0 (zero) in the product_reorder_level and see if
you get the error message you defined in the validation rule.

ItemFocusChanged Event
This event gets triggered whenever the current field looses the focus, irrespective of
whether the data was changed or not. Typical use of this event is to give control to the
proper field depending on the value of the current field.

Don't confuse this event with LooseFocus event. LooseFocus event triggers when the
DataWindow itself looses the focus. Remember that there may be other controls in a
window, and the DataWindow control is one of those. A DataWindow control looses its
focus when you click on any other control in the window or press TAB key when the
focus is on the last field (whichever field has the highest tab order in the DataWindow
control) in the last row (unless you add code in the DataWindow control to add a new
row as soon as the tab is pressed in the last field.).

Some functions give different results depending on the event from which you are calling
the function. For example, say, the cursor is in the product_no field and you press tab.
Now, you expect the cursor to go into product_description field. At this point, if you call
GetColumnName() function in the ItemChanged event, you get product_no. If you call,
the same function in ItemFocusChanged event, you get product_description.

DbError Event
Any database-related error in DataWindow triggers DbError event. To find out the error
number, you need not refer to the SQLCA object, instead, you can refer to the event
parameters. SQLDbCode gives you the database specific error number. SQLErrText
contains the actual error message from the database. You can also see the SQL statement
that caused this error by referring to SQLSyntax. The row number that caused the error
can be found in the 'row' and buffer name in the Buffer parameter. We will be using this
event, later in this session.

Error Event
This event is triggered when:

 You use OLE object DataWindow and an error occurs in the OLE object.
 You use wrong expression while accessing DataWindow in the dot notation.

You have the option of ignoring/changing values of the expression, retrying or failing. If
you don't write script, it triggers SystemError event at the application level.

SQLPreview Event
This event is triggered whenever a SQL statement is sent to the database. That means,
this is the right place (only place) to see and modify the SQL statement before sending it
to the database. You can also skip sending the current SQL statement or stop the whole
operation by returning appropriate return code.

For example, see the SQL statement sent by PowerBuilder to the database, by
writing the following code in the SQLPreview' event in dw_product and dw_query.
We are writing in dw_query because, we retrieve the data from dw_query and
update the database using dw_product DataWindows. Writing the following
statement in both DataWindows, display the SQL statements while retrieving as
well as while updating the database.

MessageBox( "SQL", SqlSyntax )

Now, run the application and retrieve the data. PowerBuilder will display the SELECT
statement. Do some changes in the data and save the changes back to the database. You
will see INSERT/ UPDATE/ DELETE statements, depending on the changes you do.

Other Non-Mapped, but Important


DataWindow Events
The following table lists some important events that you may want to write code. Please
note that, the event id listed below is one word and there are no spaces or new line
characters. Same is the case with the suggested event name.

PB Event ID Suggested name When it is fired Comments


for the event
This event is fired when
pbm_dwnbacktabout Ue_BackTabOut the focus is on the first You may want to write
row and first non- code to go to the first
protected, non-zero tab field in the last row of
sequence field in the the DataWindow.

DataWindow and you


press Tab key while
holding down the Shift
key.
 
pbm_dwndropdown Ue_DropDown Occurs just before the
list provided by a
DropDownDataWindow
is displayed. Use this
event to retrieve new
data for the child
DataWindow.
 
pbm_dwngraphcreate Ue_GraphCreate Occurs after the
DataWindow control
creates a graph and
populates it with data,
but before it has
displayed the graph. In
this event, you can
change the appearance
of the data about to be
displayed.
 
pbm_KeyDown Ue_KeyDown Occurs whenever the
user presses a key when
the DW control is in
focus.
 
pbm_dwnmessageText Ue_MessageText Occurs when a crosstab
DataWindow generates
a message. Typical
messages are Retrieving
data and Building
crosstab.
 
pbm_dwnmousemove Ue_MouseMove Occurs when the user
moves the mouse
pointer in a
DataWindow control.
 
pbm_dwnlbuttonup Ue_MouseUp Occurs when the user
releases a mouse button
in a DataWindow
control.
pbm_dwnprocessenter Ue_ProcessEnter Occurs when the user BY default, pressing
presses the Enter key the Enter key moves
when focus is in the the focus to the next
DataWindow or the row into the same
DataWindow's edit column, if there are
control. more rows below.
Nothing happens
otherwise.
If user presses ENTER
pbm_dwntabdownout Ue_TabDownOut Occurs when the current key, then 1.
row is the last row and pbm_dwnProcessEnter
the user presses the 2. ItemChanged (if
ENTER or down arrow data is changed) 3.
to change focus to the This event are fired in
next item in a sequence.
DataWindow column.
If TAB key is pressed,
ItemChanged will fire
before this if data is
changed.

pbm_dwntabout Ue_TabOut Occurs when the focus ItemChanged event is


is on the last row and fired before this if the
column and the user data is changed.
presses TAB key or, in
some edit styles, the
right arrow, to move to
the next cell in the
DataWindow. It also
occurs when the user
types the maximum
number of characters in
a column with the
EditMask edit style so
that focus moves to the
next cell. The
EditMask.AutoSkip
property must be set to
Yes.

pbm_dwntabupout Ue_TabUpOut Occurs the focus is on If user presses Shift +


the first row and the ENTER, then events
user presses are fired in the
Shift+ENTER or up following sequence: 1.
arrow to move to the pbm_dwnProcessEnter
previous item in a 2. ItemChanged (if
DataWindow column. data is changed) and 3.
This event itself.

Removing Unwanted Rows


For any reason if you want to get rid of some rows in a DataWindow (but not from the
database), what you do?

You might have thought of calling DeleteRow() function. Well, you can do that,
however, the DeleteRow() function moves those rows into Delete buffer, meaning, they
will also get deleted from the database when you call Update() next time on that
DataWindow.

The other option is to create a DataStore and assign the same DataWindow object and use
RowsMove() function to move rows from the DataWindow to the DataStore. In case if
the user wants to restore it back, you don't have to retrieve them from the database all
over again; you can move them back to the DataWindow. Please note that, when you
move rows using RowsMove() function, the row status in the target DataWindow is set to
NewModified! And you may want write some code to store the row and column statuses.

If you do not want to restore functionality, you can just call RowsDiscard() function
which removes these rows from the DataWindow, but do not populate Delete buffer.

Data Retrieval Cancellation


If you want to allow the user to stop retrieving data from the database, do you ask the
user to press Escape key while data retrieval is in progress? No, that doesn't serve the
purpose in this situation.

First, you need to display a visual control such as a CommandButton (cb_cancel for
example purpose), on the screen where (s)he can click on it to stop retrieval process. For
the Clicked event of the CommandButton, you can write DBCancel() function to stop
retrieving. DbCancel() cancels retrieving from the database.

Once you issue Retrieve() function, DataWindow doesn't give the control back to the user
till it completes retrieving. That means that if the user clicks on the cb_cancel button
while the data is being retrieved, nothing will happen. There are couple of things you
need to do to make it work.
First, you need to set Asynchronous option on in the DbParam property in the transaction
object. You should do this before your application connects to the database. Setting this
option after connecting to the database doesn't have any effect.

Then you need to write some code in the DataWindow's RetrieveRow event. Even if you
a put a single comment will serve the purpose. The point here is that, PowerBuilder
yields after retrieving each row when it sees some code in RetrieveRow event. Because
of that yielding, user should be able to click on the cb_cancel while data is being
retrieved.

You need to declare an instance boolean variable and set it to true in the cb_cancel's
Clicked event. In the RetrieveRow event, return 1 if that instance variable is set to true.

Appending the New Result Set to the


DataWindow
Every time you call Retrieve() function, PowerBuilder clears the DataWindow and
populates with the new results. By the term 'clearing' we do not mean moving the data
from Primary buffer to the Delete buffer, it means, resetting the DataWindow as if you
never issued a Retrive() function on that DataWindow. If the DataWindow has any
modified or new rows that weren't applied in the database, you will not get any warning
or error message, it simply discards all rows in the DataWindow. It is equaliant to calling
Reset() function internally by the Retrieve() function. Reset() function discards all rows
from all buffers in a given DataWindow.

Now, for any reason, if you want to retain the existing data and want to append the new
result set from the Retrieve() function, what you do? For example, you issued Retrieve()
and got say 1000 rows. Now you want to issue another Retrieve() function with different
arguments (say, you are expecting 200 rows) and want to append those 200 rows to the
DataWindow; Upon completion, you want to see 1200 rows in the DataWindow.

What you can do is, create a DataStore or a hidden DataWindow and assign the same
DataWindow object. Retrieve data into the new DataStore/DataWindow and move those
rows into the original DataWindow using RowsMove() function and set all those moved
rows status to NotModified!

Even though it solves the problem, it is not the right solution. Rather you can solve this
problem with a single line of code. Do you recall it from the 'DataWindow Events' topic?
You need to return 1 from the RetrieveStart event. It is as simple as that.

Appending a Row Automatically


When the user is on the last row and last column, pressing the down arrow key or
ENTER key or TAB key does not automatically append a new row to the DataWindow.
If you need that kind of functionality, we need to write script to do that.

Pressing the TAB key may or may not fire ItemChanged event depending on whether
user changed the data or not. Please recall that ItemChanged event is fired only when the
user changes the data and change the focus to another column/row within the same
DataWindow. It does not fire ItemFocusChanged event because it is the last row and last
column in the DataWindow, meaning nothing will happen by pressing the TAB key since
there is no further field to go.

What you need to do is, declare a user-defined event ue_TabDownOut and map it to
'pbm_dwnTabDownOut event id and write the following code.

// Object: dw_product
// Event: ue_TabDownOut
long ll_RowNo
int li_ColCount
li_ColCount = integer(this.object.DataWindow.Column.Count )
IF this.GetRow() = this.RowCount() and &
this.GetColumn() = li_ColCount THEN
ll_RowNo = this.InsertRow(0) // append a row.
this.SetRow( ll_RowNo ) // set the focus to the new row.
// SetRow doesn't take you to that row,
// so, scroll to that row.
this.ScrollToRow( ll_RowNo )
this.SetColumn( 1 ) // set the focus to the 1st col.
END IF

The IF condition is checking whether the current row is equal to the total row count in the
DataWindow and the current column number is equal to the total column count in the
DataWindow. If so, it is appending a row to the DataWindow by passing zero as
argument to the InsertRow() function and setting the focus to the first column in the last
row.

This logic may not work with all DataWindows, especially when the first column's
tab order is zero or it is protected. It worked well in the version I tested, but it is
better not taking it for granted behavior. You have two options in that situation.
First and easy one is, don't set the column to 1, rather leave to the existing column
number. Second one, where you need to write some code is, in a loop check for the
column's tab order and protection status and set the column's focus to the first
column that has some tab order and not protected.

// Object: dw_product
// Event: ue_TabDownOut
// Replace SetColumn() line with the following code.
// Insert the following 1 line to the variable
// declaration block.
int i, li_TabOrder, li_Protection
FOR i = 1 TO li_ColCount
li_TabOrder = &
integer( this.Describe( "#" + String(i) + &
".TabSequence" ))
li_Protection = &
integer( this.Describe( "#" + String(i) + ".Protect" ))
IF li_TabOrder > 0 AND li_Protection = 0 THEN
this.SetColumn( i )
EXIT
END IF
NEXT

Please note that, when the above code is triggered when the focus is on the last row and
user presses ENTER or down arrow key. Similarly, as explained earlier, user can also
press TAB key when the focus is on the last column in the last row. Don't you think it is a
good idea to provide this functionality when (s)he presses TAB key also. If you agree
with me, then declare a user-defined event ue_TabOut and map it to pbm_dwnTabOut
event id and write the same code listed in this section. Alternately, you may want to just
trigger the other event instead of duplicating the code.

Let's enhance the functionality by removing some code listed above. Yes, we want to
remove some code to enhance the functionality. The above code inserts new row when
the focus is on the last column on the last row. What if, if the focus is on some column in
the last row and user presses down arrow key? The above code doesn't work. What you
need to do to provide that functionality is, you need to remove the IF condition that is
checking the row & column number.

TAB Key Behavior to the ENTER Key in


a DataWindow
By default, pressing the ENTER key changes the focus to the same column in row
below it, if there is any row below it. In a typical data-entry screen in the industry,
pressing the ENTER moves the focus to the next field in the same row. Let's now
add this functionality to the DataWindow. Declare a user-defined event
ue_ProcessEnter and map pbm_dwnProcessEnter event id to it and write the
following code:

// Object: dw_product
// Event: Ue_ProcessEnter
long ll_CurrentRow
int li_ColCount, li_ColNo
int i, li_TabOrder, li_Protection
boolean lb_IsColumnSet
li_ColCount = integer(this.object.DataWindow.Column.Count )
li_ColNo = this.GetColumn()
IF li_ColNo < li_ColCount THEN
this.SetColumn( li_ColNo + 1 )
FOR i = ( li_ColNo + 1 ) TO li_ColCount
li_TabOrder = &
integer( this.Describe( "#" + String(i) + &
".TabSequence" ))
li_Protection = &
integer( this.Describe( "#" + String(i) + &
".Protect" ))
IF li_TabOrder > 0 AND li_Protection = 0 THEN
this.SetColumn( i )
lb_IsColumnSet = TRUE
EXIT
END IF
NEXT
END IF
// No non-protected, non-zero tab order column found.
IF NOT lb_IsColumnSet THEN
ll_CurrentRow = this.GetRow()
// Let's try in the next row, if there is one.
if ll_CurrentRow = this.RowCount() THEN return 1
// There is a row.
FOR i = 1 TO li_ColCount
li_TabOrder = &
integer( this.Describe( "#" + String(i) + &
".TabSequence" ))
li_Protection = &
integer( this.Describe( "#" + String(i) + &
".Protect" ))
IF li_TabOrder > 0 AND li_Protection = 0 THEN
// Column is found. Let's set the focus and
// scroll to that row.
this.SetRow( ll_CurrentRow + 1 )
this.SetColumn( i )
this.ScrollToRow( ll_CurrentRow )
EXIT
END IF
NEXT
END IF

The first IF condition in the above code sets the focus to the next non-protected, non-zero
tab order field, if available. Otherwise, it tries to set on the next row (if next row is there).
If none of them is true, then this code does nothing.

Resetting Update Status of All Rows


Say, the last Retrieve() function on a DataWindow returned 1000 rows. Since then, the
user changed 200 rows and added couple of them and deleted few of them. Now, he
decided to mark all records updated. How will you provide that functionality?

The first thing that might come to your mind is to call Reset() function. However, it
doesn't solve the problem. It simply resets the DataWindow, meaning it empties all the
DataWindow and displays an empty one to the user. This is not what the user wants!
The other way is, you can reset the status of each row to NotModified! in the
DataWindow, delete rows from the Delete buffer using RowsDiscard() function. Sounds
easy? Well, it is easy, if there is no other way.

But, there is an easy way to achieve this. You need to just call ResetUpdate() function.
That's all. No more coding is required.

Powering up the UP Arrow Key in the


DataWindow
Does PowerBuilder takes to the last row when the user press up arrow key while the
focus is on the first row? No, it doesn't. If you need that functionality, then you need to
write the code for it.

Declare a user-defined event 'Ue_TabUpOut' and map it to pbm_dwnTabUpOut


event id and write the following code.

// Object: dw_product
// Event: Ue_TabUpOut
IF this.GetRow() = 1 THEN
this.SetRow( this.RowCount())
this.ScrollToRow( this.RowCount())
END IF

Note that, this code doesn't work if the focus is in a column that has DDDW edit style
and user press up arrow key. This code also works perfectly when you remove the IF
condition from the code.

DataWindow Update Properties


To save changes back to the database from the DataWindow, we use Update() function.
Update() function generates appropriate SQL statements, depending on the row status and
sends them to the database for execution.

We can tell PowerBuilder about the columns it can update and what values it can include
in the WHERE clause. Well, we called the Update() statement without specifying any of
these properties. In that case, PowerBuilder takes the values from the primary key
defined on the table and other defaults.

To specify update properties, you need to select Rows/Update Properties in the design
mode. You will see a dialog box as shown below.
If Allow Updates property is turned off, PowerBuilder doesn't generate SQL statements
for the Update() function. Table to Update contains the table we placed in the FROM
clause, in the SELECT statement. If the DataWindow contains only one table,
PowerBuilder automatically turns on Allow Updates property and selects the table. If the
FROM clause has multiple tables, PowerBuilder automatically turns off Allow Updates
property. You need to turn on this property and select the table name you want for the
Update() function.

Options under Where Clause for Update/Delete specifies the columns to be added in the
WHERE clause. The meaning of Key Columns is, columns specified in the primary key
for the table. Let's take an example. Say, we retrieved a row from product_master table.
As you know, product_no is the primary key for that table. Say, all the columns for this
table, in the DataWindow are updateable and you changed the product_description
column.

In this scenario, if you select 'Key Columns', the WHERE clause would be:

UPDATE product_master
SET product_description = 'new value'
WHERE product_no = 1
If you select 'Key and Modified Columns':

UPDATE product_master
SET product_description = 'new value'
WHERE product_no = 1 and
product_description = 'Hard Disks'

If you select Key and Updateable Columns:

UPDATE product_master
SET product_description = 'new value'
WHERE product_no = 1 and
product_description = 'Hard Disks' and
product_balance = 200.000 and
product_reorder_level = 10.000 and
product_measurement_unit = 'U'

For database performance, use Key Columns. However, sometimes it might lead to 'Lost
Updates'. For example, say the "product_balance" for "product_no=1" is 200, and you
changed the "product_balance" from 200 to 300. That means you increased the balance
by 100. PowerBuilder is not going to send the UPDATE statement as follows:

UPDATE product_master
SET product_balance = product_balance + 100
WHERE product_no = 1

Instead, it will send:

UPDATE product_master
SET product_balance = 300
WHERE product_no = 1

From the above example, you may think that, it doesn't matter how you got the balance,
as long as you see the product_balance as 300. Yes, you are right so long as you are the
only user of the database, but, in a multi-user, you might be in danger.

Say, there is another user in the database. Both of you read the table at the same time.
That means both of you see 200 on the screen. Before you update the database, the
second user increased the product_balance by 60, i.e., 260. That means, after you update
the database, the balance should be 360, but, it won't be. Because, PowerBuilder is
sending a wrong UPDATE statement to the database.

Then, what is the solution for this problem?

The solution is simple. Use Key and Modified Columns option. In this case,
PowerBuilder sends the following statement:

UPDATE product_master
SET product_balance = 300
WHERE product_no = 1 and
product_balance = 200

Since, your friend updated the balance, PowerBuilder doesn't find a row for the above
WHERE clause and updates zero rows. You can check the number of rows updated by
looking into SQLCA.SQLNRows property. If you see zero rows, you can use
ReSelectRow() function, to upload the latest value into the DataWindow.

Using the Updateable columns, you can specify the columns, which PowerBuilder should
update in the database. PowerBuilder automatically detects key columns for the table, if
the table has a primary key defined. If not, you can specify it by selecting columns from
the Unique Key Column(s) ListBox.

PowerBuilder generates the UPDATE statement when data is changed in the


DataWindow. Don't confuse database's UPDATE statement with PowerBuilder's
Update() function; PowerBuilder's Update() function generates INSERT/ DELETE/
UPDATE SQL statements depending on the row status. In some databases, updating the
primary key is not allowed. To update a row in that situation, we need to delete the
existing row and insert a new row. You can tell PowerBuilder to do so by selecting Use
Delete then Insert under the Key Modification option, however, this method is a costly
operation in the database since two the existing record should be deleted and then a new
record should be inserted which is a costly operation in terms of performance.

Sometimes, even though you select ‘use Update’ option, it will result in ‘Use Delete then
Insert’ effect in the connected data source. For example, say the back-end database is
Sybase Adaptive Server and you selected ‘use Update’ option. So, PowerBuilder is going
to send an UPDATE statement to Sybase. Sybase is going to update that row in-place, if
there is no trigger on that table and satisfy some other conditions (conditions vary
between versions), then it will respect that statement, otherwise, it is going to do
DELETE followed by INSERT internally.

Multiple Table DataWindow Update


A limitation of DataWindow is that, you can update only one table at a time. When you
paint a DataWindow based on a single table, PowerBuilder automatically makes that
table updateable. You can see, if you observe the DataWindow update characteristics in
the DataWindow Painter.

However, if a DataWindow has more than one table, PowerBuilder doesn't automatically
set the attributes for you—you can't even programmatically set the attributes for multiple
tables simultaneously.

Fortunately, it doesn't mean that you can't update a multiple table DataWindow. You
have an ultimate DataWindow function, Modify()function, especially for tasks like these.
For example, let's take the simple join between product_master and trans tables, as shown
below:

SELECT "trans"."tran_no",
"trans"."item_no",
"product_master"."product_description",
"trans"."tran_qty",
"product_master"."product_balance"
FROM "product_master", "trans"
WHERE ("product_master"."product_no" = "trans"."item_no")

In the script, if you update the DataWindow by calling Update(), which table will be
updated? None. As explained earlier, PowerBuilder does not set update properties when a
DataWindow contains multiple tables. You need to set one of those several tables
updatable in the Rows/Update properties option. Let's say, you set trans table as
updatable in the DataWindow painter. Now, if you call Update(), PowerBuilder will
update trans table only. So, how would we go about updating the product_master?

The Update() function takes two parameters: the first is whether or not to call the
AcceptText() function internally, and the second one determines whether or not to reset
the row/column statuses.

Calling AcceptText() performs the necessary validations on the last column in the
DataWindow and copies the data from the DataWindow's Edit buffer to the Primary
buffer. For example, if the user edit the value of a column in a row and click on the
Update button without pressing tab, what happens?

The ItemChanged event is not triggered because, the changed column never lost its focus.
Please note that, the column looses its focus when the user moves to another column/row
within the same DataWindow by pressing TAB key or using the arrow key or clicking on
another column/row with the mouse button. In this example, (s)he never did any one of
those, instead clicked on a button placed outside the DataWindow. It means, the
DataWindow itself lost the focus, but not that particular column. Hence, ItemChanged
event is never triggered. If that button has dw_1.Update() function and the first argument
is FALSE, the changed value will never go to the database. That's why we should always
either call AcceptText() before calling Update()or call the Update()function with TRUE
as the first argument.

When the Update() is successful, PowerBuilder resets the status of all rows and columns
to NotModified!.

You can see this in action if you issue an Update() call again. Since all rows currently
hold NotModified! status, PowerBuilder doesn't generate SQL statements.

If you call Update( True, False ) or Update( False, False ), the flag isn't reset, and it
wouldn't be changed until another Update() function with TRUE as the second
argument is called to reset the flag. We can take advantage of this behavior by
updating two or more tables while the status is unset:
int lUpdateStatus
// Update the DataWindow, But, Don't reset the row status.
lUpdateStatus = dw_trans.Update(true,false)
If lUpdateStatus = 1 Then
dw_trans.Modify("trans_tran_no.Update = No")
dw_trans.Modify("trans_item_no.Update = No")
dw_trans.Modify("trans_qty.Update = No")
dw_trans.Modify("trans_tran_no.Key = No")
dw_trans.Modify("trans_item_serial_no.Key = No")
dw_trans.Modify( &
"DataWindow.Table.updateable = ~"product_master~"")
dw_trans.Modify( &
"product_master_product_description.Update = Yes")
dw_trans.Modify( &
"product_master_product_balance.Update = Yes")
dw_trans.Modify("product_master_product_no.Key = Yes")
lUpdateStatus = dw_trans.Update(FALSE, TRUE)
If lUpdateStatus = 1 Then
Commit;
Else
RollBack ;
MessageBox("Update","Error: " + SQLCA.SQLErrorText +&
"~r" + "No changes made to database" )
End If
dw_trans.Modify("DataWindow.Table.Updateable = ~"trans~"")
dw_trans.Modify("trans_tran_no.Update = Yes")
dw_trans.Modify("trans_item_no.Update = Yes")
dw_trans.Modify("trans_tran_qty.Update = Yes")
dw_trans.Modify("trans_tran_no.Key = Yes")
dw_trans.Modify("trans_tran_serial_no.Key = Yes")
dw_trans.Modify( &
"product_master_product_description.Update = No")
dw_trans.Modify( &
"product_master_product_balance.Update = No")
dw_trans.Modify( &
"product_master_product_no.Key = No")
Else
MessageBox("Update", "Error: " + SQLCA.SQLErrorText + &
"~r" + "No changes made to the database" )
RollBack ;
End If

First, we called Update( True,False ), which will update the table and won't reset the row
and column statuses. Then, we make all the columns in the trans not updateable and let
PowerBuilder know not to use the primary key of trans for future updates.

Now, we set all the columns in the product_master table as updateable and also let
PowerBuilder know which key it has to use for an update. When this stage is complete,
we called Update( FALSE, TRUE ), but this time reset flag is TRUE, which will reset the
row status for all columns after a successful update. Notice that the accepttext flag is
FALSE in the second Update(), because we do not need to accept the text again since it
was already done in the first call. Once all updates are successful, we need to set the
DataWindow properties back, i.e., trans table as updateable and product_master table as
not updateable, because if this script executes next time, it should exactly what it did
now.

If you have more than two tables you can call Update( True, False ) for all tables except
the last table, which should set the flag and cause all the tables to update.

Parent-Child Tables Data-Entry


Till now you have learned about updating DataWindows with single and multiple-tables.
They are typically, either the master files (independent tables) or related tables (joins, for
example, department & employees). In product_master/trans tables, any entry in trans has
to update product_master. If the transaction is a receipt, product_balance in
product_master should to be increased, and should be decreased in case of issue and so
on. That means, you need to write script to update product_master table also, instead of
just giving Update() for the trans table.

Other than updating the master file, you need to consider one more thing. Take trans
table, values for some columns repeat for the transactions. For example, a receipt with 10
items has the same tran_no, tran_date and tran_type for all 10 items. You have to make
provision for the user, to enter these repeated values only once. To do this, create a
DataWindow with GROUP presentation style or create a DataWindow with tabular
presentation style and use GROUP BY clause for the repeated columns.

In both the cases, by default, the columns that are part of the group have 0 (Zero) tab
order. Even if you change it at design time, at run-time these are set back to 0 (Zero) and
the user can't enter data in the columns that are part of the group.

The solution for this is, create two DataWindows for the trans table. One DataWindow to
capture header data, tran_no, tran_date and tran_type, another to capture transaction
details such as serial_no, item_no and quantity.

For the header DataWindow, select only three columns from the trans table, i.e., tran_no,
tran_date and tran_type as the data source. For detail DataWindow, unlike the header
DataWindow, select all columns from the trans table and display only tran_item_no,
tran_qty and tran_serial_no on the screen. Delete the rest of columns from the screen (not
from the SELECT list in the SELECT statement), i.e. that are present in the first
DataWindow.

We are using the header DataWindow, to capture repeated values in the transaction. We
never update the header DataWindow. Whenever user wants to save the changes to the
database, we populate the detail DataWindow from the header DataWindow. Now, detail
DataWindow has all the information about a transaction. Now, we update the detail
DataWindow.
Using two DataWindows, one for the header info and other for the detail info, is the
solution for "How to avoid the user from entering duplicate info?". We still don't have the
solution for updating the product_master table, for changes done in the trans table.
Actually, we already explained the solution in "Multiple Table DataWindow Update"
section. That section requires you (as a PowerBuilder programmer) to know the
relationship between tables in the database. This is also called as Fat Client, i.e., most of
the logic is written in the client process.

Push some load onto the server using "triggers". These days, all most all RDBMSs
support triggers. A trigger is a special type of stored procedure maintained and executed
automatically in the database. You can write one trigger for each INSERT, DELTE,
UPDATE operations. That means, if you write one trigger for the INSERT operation, the
database automatically executes the trigger for each INSERT operation on that table. In
the trigger logic, we can update the product_master table.

In our application, let's implement Server Side logic. We already painted the required
DataWindow objects.

Create a new window w_transactions. Place three DataWindow controls as shown in the
above picture.

 
DataWindow control Name DataWindow Object

Left Top dw_tr_head d_trans_data_entry_header

Left Bottom dw_tr_detail d_trans_data_entry_detail

Right dw_product d_display_product


Assign m_sheet_menu menu to this window and write the following in the Open event of
w_transactions

// Object: w_transactions
// Event: Open
dw_product.SetTransObject( SQLCA )
dw_tr_head.SetTransObject( SQLCA )
dw_tr_detail.SetTransObject( SQLCA )

dw_product.Modify( "DataWindow.ReadOnly=Yes" )

What we plan to do is, when the user enters a product_no, we retrieve information about
the product and display it in the dw_product DataWindow control. Since, we are using
dw_product DataWindow for display purpose only, it would be better to make it read-
only. When a DataWindow is read-only, PowerBuilder doesn't maintain all buffers, i.e.,
"Deleted", "Filtered", "Original" for it. That means efficient use of resources. We are
making dw_product DataWindow as read-only, by calling Modify() function.

Now, we need to allow the user to add new record. Declare a user-defined event
"ue_add" for the window. (Click on the 'Event List' tab page in the Script view,
invoke popup menu and select Add menu option. Make sure that you are creating
this event for the window, and not for any control within the window). You can map
it to any custom event, say, pbm_custom01. Write the following script to the event.

// Object: w_transactions
// Event: ue_add
Long l_NewRowNo
If dw_tr_head.RowCount() = 0 and &
dw_tr_detail.RowCount() = 0 Then
dw_tr_head.InsertRow(0)
dw_tr_detail.InsertRow(0)
SetFocus( dw_tr_head )
dw_tr_head.SetColumn( "tran_no" )
Else
l_NewRowNo = dw_tr_detail.InsertRow(0)
dw_tr_detail.ScrollToRow( l_NewRowNo )
SetFocus( dw_tr_detail )
dw_tr_detail.SetColumn( "tran_serial_no" )
End If

If the user is adding a new record, we need to take the header information also. That's
why, we are inserting rows in header as well as in detail DataWindows. SetFocus()
function changes the focus to the specified control. We want the user to enter the header
information first, so, we are setting the focus to the dw_tr_head DataWindow. We need
to use SetColumn() function, to set the focus to a column within the DataWindow. If the
user has already entered the header info, we insert only in the detail DataWindow. In
which case, the cursor focus is set to the tran_serial_no column.
When the user enters a product_no and tabs out of tran_item_no column, we want
to display product information in the dw_product DataWindow. So, write the
following code for the ItemChanged event in the dw_tr_detail DataWindow.

// Object: dw_tr_detail DataWindow in w_transactions window.


// Event: ItemChanged
If This.GetColumnName() = "tran_item_no" Then
dw_product.Retrieve( Integer( Data ) )
If dw_product.RowCount() <> 1 Then
MessageBox( "Error", "Product No: '" + Data + &
"' is not existing in the master file." + "~n" + &
"Pl. Enter in the Master File First.", &
StopSign!, OK!,1)
i_DoNotDisplayErrMsg = True
Return 1
End If
End If

If you run the application at this time, you may not be able to enter the data in
dw_tr_head DataWindow. If you take a look at the SELECT statement for this
DataWindow, you will find that the SELECT statement doesn't have all the columns from
the trans table. Since, all the columns in the trans table are defined as "NOT NULL", and
are not included in the SELECT statement, PowerBuilder automatically turns off the
Allow Update property. So, what do we do? Click on the dw_tr_head DataWindow with
the right mouse button and select the Modify DataWindow option. PowerBuilder now
opens the d_trans_data_entry_header in the DataWindow painter. Select Rows/Update
Properties from the menu. Turn on the Allow Updates option. Select trans for the Table
to Update prompt. Select all the columns under Updateable Columns. Select Tran_no
from Unique Key Column(s) option and click the OK CommandButton.

Now, select Format/Tab Order from the menu and set the tab order to 10, 20, 30 from left
to right. Select Format/Tab Order from the menu again and close the DataWindow
painter. Enable "V Scrollbar" for the dw_tr_detail DataWindow. Turn off borders for the
transaction DataWindows. Want to be fancy, then draw a rectangle around the transaction
DataWindows, to make the user believe it as a single object.

You need to write the following script for both m_mdi_menu and m_sheet_menu
menus.

// Object: m_sheet_menu, m_mdi_menu menus.


// event: Clicked event for Module/Transactions menu
// option.
w_transactions l_sheet1
OpenSheet( l_sheet1, ParentWindow, 0, Cascaded! )

We are done. Right? No. We haven't written code for saving data in the database.
Then declare a user-defined event ue_save for w_transactions window and map the
event to "pbm_custom02" event id. Write the following code to that event.

// Object: w_transaction window


// Event: ue_save
Int lTranNo, lUpdateStatus
Long lTotalItems, i
Date lTranDate
String lTranType
dwItemStatus lRowStatus
If dw_tr_head.Rowcount() = 0 Then Return 0
lTotalItems = dw_tr_detail.RowCount()
lTranNo = dw_tr_head.GetItemNumber(1, "tran_no")
lTranDate = dw_tr_head.GetItemDate(1, "tran_date")
lTranType = dw_tr_head.GetItemString(1, "tran_type")
For i = 1 to lTotalItems Step 1
lRowStatus = dw_tr_detail.GetItemStatus( i,0,Primary!)
If ( lRowStatus <> New! ) and &
( lRowStatus <> NotModified! ) Then
dw_tr_detail.SetItem( i, "tran_no", lTranNo )
dw_tr_detail.SetItem( i, "tran_date", lTranDate )
dw_tr_detail.SetItem( i, "tran_type", lTranType )
End If
Next
lUpdateStatus = dw_tr_detail.update()
If lUpdateStatus = 1 Then
Commit using sqlca ;
dw_tr_head.Reset()
dw_tr_detail.Reset()
dw_product.Reset()
Else
Rollback using sqlca ;
MessageBox( "Database Error", SQLCA.SQLErrText )
Return -1
End If

We are checking if the header DataWindow has a row or not. If not, there is nothing to
process, so return, but, if there is a row, we read the information into variables. We are
populating the same information in the detail DataWindow. We now have all the
information required in the detail DataWindow. We then update the detail DataWindow.
If the update is successful, we reset all the DataWindows, i.e., empty them, in order to
make them ready for the next transaction.

Note: Even though you see few columns in the detail DataWindow, the actual SELECT
statement has all columns in its list. Even though you delete some columns from the
screen, values of the deleted columns (from the screen) are still available in the script (as
long as they are present in the SELECT list).

Now, we are ready to run the application. Right? Again, the answer is no. In the above
script, you are simply calling the Update() for the transaction details DataWindow. What
about updating the master table? For that, we need to write triggers in the database.

Sybase's Adaptive Server Anywhere supports four types of triggers, INSERT, DELETE,
UPDATE, and UPDATE specific columns. This example requires the first three triggers.
Invoke the database painter and write the following triggers in the ISQL view and
execute them.
INSERT Trigger On trans Table
CREATE trigger tran_new_trig after insert on trans
REFERENCING NEW AS new_tran
FOR EACH ROW
BEGIN
If "new_tran"."tran_type" = 'R' Then
update "product_master"
set "product_master"."product_balance" =
"product_master"."product_balance" +
"new_tran"."tran_qty"
where "product_master"."product_no" =
"new_tran"."tran_item_no"
Elseif "new_tran"."tran_type" = 'T' Then
update "product_master"
set "product_master"."product_balance" =
"product_master"."product_balance" +
"new_tran"."tran_qty"
where "product_master"."product_no" =
"new_tran"."tran_item_no"
Elseif "new_tran"."tran_type" = 'I' Then
update "product_master"
set "product_master"."product_balance" =
"product_master"."product_balance" -
"new_tran"."tran_qty"
where "product_master"."product_no" =
"new_tran"."tran_item_no"
End If
END;

Type the above code in the database administration painter and execute it. Do the same
for the next two triggers. We don't want to go into details, because, it has just few simple
SQL statements.

DELETE Trigger On trans Table


CREATE trigger tran_del_trig after delete on trans
REFERENCING old AS old_tran
FOR EACH ROW
BEGIN
If "old_tran"."tran_type" = 'R' Or
"old_tran"."tran_type" = 'T' Then
Update "product_master"
Set "product_master"."product_balance" =
"product_master"."product_balance" -
"old_tran"."tran_qty"
Where "product_master"."product_no" =
"old_tran"."tran_item_no"
Elseif "old_tran"."tran_type" = 'I' Then
Update "product_master"
Set "product_master"."product_balance" =
"product_master"."product_balance" +
"old_tran"."tran_qty"
Where "product_master"."product_no" =
"old_tran"."tran_item_no"
End If
END;

UPDATE Trigger On trans Table


CREATE trigger tran_upd_trig after update on trans
REFERENCING old AS old_tran new AS new_tran
FOR EACH ROW
BEGIN
If "old_tran"."tran_type" = 'R' Or
"old_tran"."tran_type" = 'T' Then
Update "product_master"
Set "product_master"."product_balance" =
"product_master"."product_balance" -
"old_tran"."tran_qty" + "new_tran"."tran_qty"
Where "product_master"."product_no" =
"old_tran"."tran_item_no"
Elseif "old_tran"."tran_type" = 'I' Then
Update "product_master"
Set "product_master"."product_balance" =
"product_master"."product_balance" +
"old_tran"."tran_qty" - "new_tran"."tran_qty"
Where "product_master"."product_no" =
"old_tran"."tran_item_no"
End If
END;

Now is the right time to close database painter and run the application and test it. Check
if product_master is properly updated or not.

An Introduction to Triggers
If you know about triggers, you can skip this section. In case you don't, I think it will give
you a good starting point about triggers.

In relational database terms, a trigger is nothing but one or more SQL statements put
together and are fired on a specific event. For example, in SQL Server you can write
three triggers for a table. One trigger for INSERT operation, one for UPDATE and the
other one is for DELETE.

The following example assumes that a BEGIN TRANSACTION is being used and there
are no nested transactions and there are no errors in the whole process.

For example, if you write a trigger for INSERT operation on trans table, the trigger is
fired every time you issue INSERT statement and all the SQL statements written in the
trigger will execute. In Sybase SQL Server, as soon as you issue an INSERT statement,
the changes required for the INSERT statement in the trans table are logged (written to
the syslogs table). Then the trigger written for the INSERT operation executes and upon
completion, the control of execution flow goes to the next statement after that INSERT
statement. The changes will be written to the database (actual table trans) when the
control reaches COMMIT statement. If execution flow encounters ROLLBACK instead
of COMMIT, then no changes are done to the trans table.

You may want to validate the changes that are being done. For example, if the transaction
is being taking place on a holiday, you may want to reject that transaction. How do you
access the data that is being inserted? Well, the implementation differs in each database.
In Sybase SQL Server, two read-only magic tables inserted and deleted are made
available in the trigger code. These tables are not available anywhere else other than in
the trigger code. In the trigger written for the INSERT operation, you can query 'inserted'
table and get the data that is being inserted and do your checking. For the above example
sake, let's assume the transaction date is a holiday and you want to reject that transaction.
Then you can either ROLLBACK TRIGGER OR ROLLBACK TRANSACTION. The
statement you use depends on the SQL Server version and other business logic.

In Sybase SQL Server, a trigger is fired once per statement irrespective of the number of
rows effected by that statement. If you are using a SELECT … INTO… statement or
INSERT...SELECT statement and if it inserts 10,000 rows or if it inserts zero rows, the
trigger is going to fire only once. You need to either use a CURSOR or temp table or
SET ROWCOUNT 1 or another technique to read each row from the inserted row and
apply business rules.

However, triggers in Adaptive Server Anywhere works in a different way. Basically,


there are two types of triggers available for each type of operation, i.e., INSERT,
DELETE and UPDATE. First type is row level trigger and the second one is statement or
table level trigger. In case of row level trigger, you can write trigger to fire before the
actual operation and/or after the actual operation. For example, you can write a 'before'
INSERT trigger and a 'after' INSERT trigger. The following is the CREATE TRIGGER
statement syntax

CREATE TRIGGER trigger-name trigger-time trigger-event [, trigger-


event,..]
... [ ORDER integer ] ON table-name
... [ REFERENCING [ OLD AS old-name ]
[ NEW AS new-name ] ]
[ REMOTE AS remote-name ] ]
... [ FOR EACH { ROW | STATEMENT } ]
... [ WHEN ( search-condition ) ]
... [ IF UPDATE ( column-name ) THEN
... [ { AND | OR } UPDATE ( column-name ) ] ... ]
... compound-statement
... [ ELSEIF UPDATE ( column-name ) THEN
... [ { AND | OR } UPDATE ( column-name ) ] ...
... compound-statement
... END IF ] ]
When you write a row level trigger you can access the data that is being changed using an
alias to OLD and NEW. In the following update trigger example, the old data is being
accessed from old_data.

Create trigger tran_upd after update on trans


referencing old as old_data,
referencing new as new_data

That means if you update tran_qty from 100 to 200, old_data.tran_qty contains 100 and
new_data.tran_qty contains 200. In row level triggers the aliases 'old_data' and 'new_data'
are available as a record -- which is not the case in the statement level triggers -- that
means, you can't give SELECT * from old_tran in the row-level trigger.

... [ REFERENCING [ OLD AS old-name ]


[ NEW AS new-name ] ]

However, if you write the same trigger for statement (table) level, the trigger will fire
only once irrespective of the number of rows effected by that operation. In this case,
within the trigger code you need to use FOR EACH STATEMENT and old_data from the
above example is referred as a table like in Sybase SQL Server, that means you can give
SELECT statements on that table. Please note that FOR EACH STATEMENT syntax is
not allowed in row-level triggers, only FOR EACH ROW syntax is allowed.

The trigger mechanism in other databases may be different than from the databases
explained above. I think the above explanation will give you a good starting point in case
you are not familiar with relational databases.

DropDown/Child DataWindows
A column in a DataWindow can have one of the six edit styles. We've looked at most of
them and seen situations where each style can be applied, but one edit style that we
haven't looked in detail is the DropDownDataWindow Edit Style. When using other edit
styles, the information you supply is stored in the PowerBuilder library. For example, we
have e_tran_type edit style that is of DropDownListBox edit style. This, e_tran_type is a
PowerBuilder object and is stored in the PowerBuilder system tables in the connected
database. This is fine for the static data, but if you want to use dynamic data, the best
solution would be to use DropDownDataWindow edit style.

For example, when adding a new product to the product_master table,


product_measuring_unit should exist in the units table. We can use a
DropDownDataWindow to display the information in the units table and allow the user to
select from the available options, rather than checking the existence in the units table by
issuing an SQL statement.
For it, all we need to do is create a DataWindow for the units table, with SQL Select data
source and tabular presentation style. We already created this DataWindow,
d_units_dropdown. Open that DataWindow in the DataWindow painter. Observe that, we
deleted everything from the header.

Now, we need to change the edit style of product_measuring_unit column in


d_product_maint DataWindow. Open the d_product_maint DataWindow, and double-
click on the product_measuring_unit column. Click on the Edit Style tab page.

Select DropDownDW for the edit style prompt, d_units_dropdown for the DataWindow
prompt, unit_description for the Display column prompt and unit for the Data Column
prompt. Selecting Always Show Arrow option would always display an arrow at the right
side of the column in the DataWindow.

Now, if we preview d_product_maint DataWindow, you'll see that for the


product_measuring_unit column, we have a DropDownDataWindow containing details
from the units table. An user always sees the unit_description on the screen, but,
PowerBuilder sends the value of the "Data Column", i.e., "unit" to the database. This edit
style allows us to use dynamic and up-to-date data with descriptive options.

Modifying DDDW Dynamically


All attributes you set in the DropDownDataWindow Edit Style dialog box can be
modified using the Modify() function. The important attributes that can be modified
using this method include:

 name
 data column
 display column

For example, to make "unit" column as the display column for the above
DropDown DataWindow, you would write the following code:

datawindowchild dwc1
dwc1.Modify( "product_measuring_unit.dddw.displaycolumn=~"unit~"")
dwc1.Settransobject( sqlca )
dwc1.retrieve( /* Arguments */ )

In the above Modify() function, product_measuring_unit is the column name, and


"dddw.displaycolumn" is the property of the column to change. In the exercise we just
did, d_products_maint is called as 'Parent DataWindow', and d_units_dropdown is called
as 'Child DataWindow'. When you issue Retrieve()on the parent DataWindow,
PowerBuilder automatically retrieves data for the child DataWindow, using the same
transaction object you set either in the SetTrans() or in the SetTransObject().

Irrespective of the number of rows in the DataWindow, PowerBuilder retrieves data from
the child DataWindow only once.

The important point to remember about Child DataWindow is that, PowerBuilder doesn't
retrieve data from the child DataWindow, if the child DataWindow contains at least one
row; the row can even be an empty one.

Sometimes, you may want to bring data into the child DataWindow using a different
transaction object. For example, say product_master table is in Sybase and units table is
in Informix. Assume that we are using SQLCA to connect to Sybase and created a new
transaction object for Informix (we will explain about creating a new transaction in a
moment).

When you issue Retrieve(), as we said, PowerBuilder automatically tries to retrieve data
using the transaction object of the parent. Here, our units table is in Informix, and is not
existing in the connected Sybase database. Hence, it will fail. How do we solve this
problem?
Here's a tip for you. Recall "Storing data in the DataWindow" topic. Put what we said
above and this together, and you should have no problem solving it. If you are not able to,
then read the following, you can do it otherwise also.

What we said early was that, if a child DataWindow already has some data in it,
PowerBuilder doesn't retrieve from the child DataWindow. Now the problem is,
how to store data in the child DataWindow, without retrieving from the database.
The solution is, open child DataWindow in the DataWindow painter, select
"Rows/Data" from the menu and insert a blank row and save it. Now that a blank
row is part of it, PowerBuilder doesn't retrieve from the child DataWindow. Then,
how to retrieve data using transaction object created for "Informix".

DataWindowChild l_UnitChildDw
integer l_ReturnCode
l_ReturnCode = dw_product.GetChild( &
'product_measuring_unit', l_UnitChildDw )
IF l_ReturnCode = -1 THEN Return 0
// Establish the connection if not already connected
// We assume that this transaction object was already populated.
CONNECT USING G_TranForInformix;
// Set the transaction object for the child
l_UnitChildDw.SetTransObject( G_TranForInformix )
// Populate the child DataWindow
l_UnitChildDw.Retrieve()
CONNECT USING SQLCA;
dw_product.SetTransObject(SQLCA)
dw_product.Retrieve()

In the above code, we declared a variable of type DataWindowChild. Then we are calling
GetChild() function to get a reference to the child DataWindow associated with the
product_measuring_unit column. Then, we connected to the Informix database, using the
transaction object created for it and retrieved the data. As you can see from the code, we
are using SQLCA for the parent DataWindow.

We know that you still have a question for us, "How to create our own Transaction
Object"? The answer is simple, use the CREATE statement.

Transaction G_TranForInformix
G_TranForInformix = CREATE Transaction
// Assign variables with values, for ex:
// G_TranForInformix.DBMS = "Informix"

Dynamic DataWindows
This section discusses about changing DataWindows dynamically. This may involve
changing SQL statements or DataWindow attributes, and it might also involve recreating
the DataWindow dynamically.
DataWindow Object—Dynamic
Assignment
As you learned previously, DataWindow control and DataWindow object are two
different things. One can link a DataWindow object to many DataWindow controls, and
also, link and de-link these two objects at run time.

For example, say you need to create 10 different reports. One solution is to create
ten different windows, one for each report, which is a straightforward method.
Another solution is to create a window, and change the DataWindow object in the
DataWindow control, depending on the report. We can dynamically assign a
DataWindow object to a DataWindow control, using the DataObject attribute. Since
all events and scripts are associated with a DataWindow control, it will operate on
any DataWindow object that is assigned to the DataWindow control, if the code is
generic enough. For example:

dw_product.DataObject = "d_product_custom_query"
dw_product.SetTransObject( SQLCA )
dw_product.Retrieve()
dw_product.Print(TRUE)

After changing the DataObject, you need to set the 'transaction object' by calling either
SetTransObject() or SetTrans(), before performing any operation related to the database.
This method is typically used to allow the user to select from a list of reports and then
display the selected report in a single DataWindow control or a standard user object of
this type.

If you are dynamically assigning DataWindow objects, make sure to add a


corresponding entry in the resource file. For example:

C:\WORKDIR\PRODUCT.PBL(d_product_custom_query)

You will learn more on resource files, in the "Application Deployment" session.

Modifying Data Source Definitions


This section discusses various methods that can be used for modifying the data source
definition, i.e., SELECT statement in the DataWindow.

GetSqlPreview() & SetSqlPreview()


These statements allow you to see and change the SQL statements dynamically at run-
time. In a DataWindow control, executing the Retrieve() function triggers RetrieveStart
event, and executing the Update() function triggers an UpdateStart event. Both of these
two events, RetrieveStart and UpdateStart then go on to trigger SqlPreview event.

The actual SQL statements are executed only if the SqlPreview script is successfully
completed. To see the UPDATE, INSERT or DELETE statements generated by
PowerBuilder, or the SELECT statement defined at the painting time, call
GetSQLPreview() from the SqlPreview event. In version 5.0, the SQL statement comes
as a parameter, SQLSyntax to the event and GetSQLPreview() function is obsolete from
v5.0.

We can then use the SetSQLPreview() function to change the SQL statement. Typically,
these commands are used to dynamically build WHERE and HAVING clauses. You can
also change the list of the columns in SELECT statement, but, the number of columns
and their data types should match the ones that are already present in the DataWindow
object.

You can access statements such as UPDATE, SELECT, INSERT and DELETE by
calling this command. For example, if 10 rows were modified and two rows were deleted,
calling MessageBox("SQL", SQLSyntax) would display 10 UPDATE statements and 2
DELETE statements.

If you change the table name in the SELECT statement using SetSQLPreview() function,
it wouldn't change the UPDATE characteristics. For example, if you change the table
name from 'product_master' to 'product_master_history', PowerBuilder would still
generate the data manipulation commands pointing to product_master.

GetSQLSelect() and SetSQLSelect()


Unlike GetSqlPreview(), GetSQLSelect() and SetSQLSelect() functions can be called
from other events. When PowerBuilder validates the SELECT statement against the
database, SetSqlPreview() is called - provided the DataWindow is updateable.

If the SELECT list doesn't match the previously defined version, it returns an error and
the new list of columns won't take effect. If the DataWindow is not updateable,
PowerBuilder doesn't check for the validity of the SELECT statement.

If the SELECT statement contains computed columns, or if the FROM clause contains
more than one table, PowerBuilder sets the DataWindow as not updateable, which makes
Update() function call to fail. If this happens, you need to change the UPDATE
characteristics by calling Modify(), which is explained next. Typically, these commands
are called when a WHERE or HAVING clause has to be changed dynamically.

Modify() Function
We've already seen how to use Modify() function to change most of the attributes of
a DataWindow, and in the same way we can use it to change the SQL statements.
The attribute you need to modify, in order to change the SQL statement, is
table.select. For example:

String lArg1, lResult


lArg1 = "datawindow.table.select='select product_no, " + &
"product_description," + " product_balance from "+&
"product_master where product_balance < " + &
"product_reorder_leval'"
lResult = dwc_1.Modify( lArg1)
If lResult = "" Then
dwc_1.Retrieve()
Else
MessageBox( "Error in changing the SQL statement",lResult)
Return -1
End If

When Modify() is used, it doesn't check SQL statement against the database; this makes
it faster, but obviously prone to errors.

We would recommend that you thoroughly test any SQL statements associated with this
function.

The DW Syntax Tool


To help you create DataWindow statements, PowerBuilder ships with a syntax tool,
which in version 7.0 is integrated within the PowerBuilder interface. To invoke this tool,
select File/New from menu and double click on 'DataWindow Syntax' icon located in the
Tools tab page.
When you run this application, you are presented with the following screen:

It displays the full syntax for any DataWindow object attribute, and object within the
DataWindow object attribute. It displays syntax in both Modifu()/Describe() format as
well as DOT notation format. By selecting appropriate option from the menu, you can
display syntax to create a DataWindow from script, modify or describe the DataWindow
object and you can even display syntax to destroy objects within the DataWindow object.
You can then copy the syntax to the clipboard and then paste into the PowerBuilder
script. This is an invaluable tool for a PowerBuilder programmer and saves days worth of
trail & error attempts in making the DataWindow syntax to work.

On Fly DataWindow Creation


PowerBuilder allows you to create a DataWindow dynamically at execution time by
using Create() function. For example, say you display a list of columns to the user and let
him pick the ones he want to see in the report. In such a case, at painting time, you will
not know anything about the appearance of the finished DataWindow.
When having a DataWindow or the custom user object control on your window, you need
to complete the three steps involved in the dynamic creation of a DataWindow, in your
script:

 Dynamically build a SELECT statement and place it in a string variable.


 Create the DataWindow syntax by calling SyntaxFromSQL().
 Create the DataWindow from the syntax by calling Create().

You can then set the transaction object and retrieve the data as normal. The reason
we call SyntaxFromSQL() is, that the syntax for a DataWindow is different from the
SELECT statement; it also contains the column attributes such as font details,
colors and so on, as well as other attributes such as band details. A sample of a
DataWindow's syntax is shown below:

release 4;
DataWindow(units=0 timer_interval=0 color=1073741824
processing=1 print.margin.bottom=97 print.margin.left=110
print.margin.right=110 print.margin.top=97)
Table(column=(type=decimal(0) update=yes key=yes initial="0"
name=product_no dbname="product_master.product_no")

As an example, the following script creates a grid style DataWindow and retrieves
data from product_master:

String lSQLStr, lErrorStr, ldwSyntax


Integer lResult
lSQLStr = "select product_no,product_description,product_balance"+ &
" from product_master"
ldwSyntax = SQLCA.SyntaxFromSQL( lSQLStr, "style &
(type=grid)",lErrorStr )
lResult = dwc_1.Create( ldwSyntax, lErrorStr )
dwc_1.SetTransObject( SQLCA )
dwc_1.Retrieve()

SyntaxFromSQL() takes three parameters: a SELECT statement, a style and a


string variable to populate any error information. The style attribute is the most
complex parameter. The syntax is as follows:

"Style(Type=value attribute=value ...) &


DataWindow(attribute=value ...) &
Column(attribute=value ...) &
Group(groupby_col1 groupby_col2 ... attribute ...) &
Text(attribute=value ...) &
Title('titlestring') "

Tabular DataWindow style is the default, and to assume the default values, simply
specify an empty string as the parameter.

Note that this method does not support the Composite presentation style.
Again, the simplest way to create this syntax is to use DW Syntax with the Create option
and paste the results directly into the code.

On Fly DataWindow Creation


PowerBuilder allows you to create a DataWindow dynamically at execution time by
using Create() function. For example, say you display a list of columns to the user and let
him pick the ones he want to see in the report. In such a case, at painting time, you will
not know anything about the appearance of the finished DataWindow.

When having a DataWindow or the custom user object control on your window, you need
to complete the three steps involved in the dynamic creation of a DataWindow, in your
script:

 Dynamically build a SELECT statement and place it in a string variable.


 Create the DataWindow syntax by calling SyntaxFromSQL().
 Create the DataWindow from the syntax by calling Create().

You can then set the transaction object and retrieve the data as normal. The reason
we call SyntaxFromSQL() is, that the syntax for a DataWindow is different from the
SELECT statement; it also contains the column attributes such as font details,
colors and so on, as well as other attributes such as band details. A sample of a
DataWindow's syntax is shown below:

release 4;
DataWindow(units=0 timer_interval=0 color=1073741824
processing=1 print.margin.bottom=97 print.margin.left=110
print.margin.right=110 print.margin.top=97)
Table(column=(type=decimal(0) update=yes key=yes initial="0"
name=product_no dbname="product_master.product_no")

As an example, the following script creates a grid style DataWindow and retrieves
data from product_master:

String lSQLStr, lErrorStr, ldwSyntax


Integer lResult
lSQLStr = "select product_no,product_description,product_balance"+ &
" from product_master"
ldwSyntax = SQLCA.SyntaxFromSQL( lSQLStr, "style &
(type=grid)",lErrorStr )
lResult = dwc_1.Create( ldwSyntax, lErrorStr )
dwc_1.SetTransObject( SQLCA )
dwc_1.Retrieve()

SyntaxFromSQL() takes three parameters: a SELECT statement, a style and a


string variable to populate any error information. The style attribute is the most
complex parameter. The syntax is as follows:
"Style(Type=value attribute=value ...) &
DataWindow(attribute=value ...) &
Column(attribute=value ...) &
Group(groupby_col1 groupby_col2 ... attribute ...) &
Text(attribute=value ...) &
Title('titlestring') "

Tabular DataWindow style is the default, and to assume the default values, simply
specify an empty string as the parameter.

Note that this method does not support the Composite presentation style.

Again, the simplest way to create this syntax is to use DW Syntax with the Create option
and paste the results directly into the code.

Review Questions & Answers


Q: What are the various buffers available in a DataWindow?

Ans: Primary, Original, Delete, Filter

Q: Which buffer contains the retrieved data?

Ans: Primary buffer.

Q: After changing the retrieved data, which buffer contains the original data that
was retrieved from the data source?

Ans: Original buffer.

Q: Why PowerBuilder keeps a copy of original data in the Original buffer, when the
latest data is available in the Primary buffer?

Ans: PowerBuilder uses the data from the original buffer to generate WHERE
clause. Data from the Primary buffer is used in the INSERT/ DELETE/ UPDATE
statements.

Q: What SQL statement PowerBuilder generates for those rows that are in the
Filtered buffer.

Ans: In terms of generating SQL statements, Filter buffer belongs to the same
category of Primary buffer, meaning, PowerBuilder will generate INSERT
statement for NewModified! row status and UPDATE statement for DataModified!
row status.
Q: What SQL statement PowerBuilder generates for those rows that have New!,
NewModified! row status.

Ans: PowerBuilder doesn't generate any SQL statement for those rows that have
New! row status. NewModified! row status will result in INSERT statement
generation.

Q: What are the different statuses a row can have in the Primary buffer?

Ans: New!, NewModified!, DataModified! and NotModified!

Q: What are different statuses that a column can have in a DataWindow?

Ans: A column can have NotModified! or DataModified! statuses. It can not have
NewModified! and New! statuses since every column's data in the row that has
NewModified! row status is used in the INSERT statement, hence there is no need to
keep track of column status in a row with New! or NewModified! row status.

Q: What is the difference between NewModified! and DataModified! row statuses?

Ans: A row would have NewModified! status when a blank row is inserted using
InsertRow() function and data is populated in at least one of those fields in that row.
Rows that are imported using ImportFile(), ImportString(), ImportClipboard()
functions also have NewModified! row status. It will retain the status till next
successful update() function call.

A row would have DataModified! status when that row is retrieved from the data
source and is modified. It will retain the status till next successful update() function
call.

Q: PowerBuilder generates DELETE SQL statement for all rows that are in the
Delete buffer. Is it correct?

Ans: No. PowerBuilder doesn't generate any SQL statement for rows with New! and
NewModified! row statuses in the Delete buffer.

Q: Explain how to find a row or column status.

Ans: You can use GetItemStatus() function to get both column and row status. If
you pass the column number/name to this function, you will get that specific
column's status in the specified row. If you pass zero as the column number, then
you will get the row status.

Q: How many times PowerBuilder retrieves data into a DropDownDataWindow


when the parent DataWindow has one thousand rows?
Ans: The answer varies depending on how the rows in that DataWindow got there.

If those 1000 rows are retrieved from the database, then PowerBuilder retrieves the
DropDownDataWindow only once unless the DropDownDataWindow has at least
one row (even an empty row has the same effect). If DDDW has some data, then
PowerBuilder doesn’t retrieve at all. Not even once.

If you imported those 1000 rows using ImportClipboard() or ImportFile() functions


are copied/moved data from other DataWindows/DataStores using RowsCopy() or
RowsMove() functions, then PowerBuilder does not retrieve data into DDDW at all.

Q: How do you prevent PowerBuilder from retrieving data into a Child


DataWindow (DropDownDataWindow) automatically?

Ans: Insert a row in the DDDW when you are in the DW painter. That row doesn’t
have to have data in it, it can even be empty. The thumb rule is, if PowerBuilder sees
at least one row in DDDW, then it doesn’t retrieve data into DDDW.

Q: I have a DataWindow which has a DropDownDataWindow.I have a situation


where I need to retrieve data into the parent DataWindow from Sybase, but data
into the Child DataWindow should come from Informix. Is there any solution for
this problem.

Ans: Yes, there is a solution. Declare a DataWindowChild type variable and get
reference of the DDDW using GetChild() function. Then set the Informix
transaction object to that DataWindow using SetTransObject() and retrieve the
data.

Q: I have a situation where I need to do some manipulation before data from a


DropDown DataWindow displays on the screen. Is there any way of solving this
problem?

Ans: Define a user-defined event in that DataWindow and map it to


pbm_dwndropdown PowerBuilder event. Write your code to that event.
PowerBuilder fires this event as soon as it completes retrieving from the DDDW and
displays it on the screen.

Q: I have a DataWindow that has a DDDW. The parent DataWindow has 1000 rows
and the DDDW has 40 rows. In this scenerio, how many times the RetrieveRow
event is fired?

Ans: PowerBuilder doesn’t fire RetrieveRow event for the DDDW rows. It only fires
for the parent DataWindow. In this case, it fires 1000 times, not 1040 times.

Q: What would be the status of a row in the target DataWindow when copied using
RowsCopy() function?
Ans: The row status in the target DataWindow will be NewModified!, meaning it
will result in INSERT statement generation for the Update() call on the target
DataWindow.

Q: What would be the status of a row in the target DataWindow when copied using
RowsMove() function?

Ans: The row status in the target DataWindow will be NewModified!, meaning it
will result in INSERT statement generation for the Update() call on the target
DataWindow.

Q: There is a DataWindow which contains a DropDownDataWindow. The


DataWindow contains no data. If you insert a new row in the parent DataWindow,
Will PowerBuilder automatically retrieves the data into the
DropDownDataWindow?

Ans: The answer is Yes when you insert rows using InsertRow(). The answer is No
when you insert rows using RowsCopy() / RowsMove(), ImportClipboard()/
ImportFile() functions. In the later case, it is your responsibility to retrieve the data
into the child DataWindow.

Q: Given the situation where the user wants to print only selected range of rows or
all selected rows in a DataWindow. How do you solve this problem?

Ans: You can use a DataStore and set the DataWindow object same as the selected
DataWindow control's object and copy rows to the DataStore using RowsCopy()
function and print DataStore.

Q: Can I use RowsCopy() and RowsMove() functions to copy/move data between


buffers in the same DataWindow?

Ans: You can move rows between buffers using RowsMove() function. However,
RowsCopy() doesn't allow to copy rows between buffers within the same
DataWindow, and I don't think you ever need that functionality. If you need, you
can copy to a different buffer in the target DataWindow using RowsCopy() function.

Q: What would be the status of a row when moved using RowsMove() function?

Ans: If you are moving rows between buffers in the same DataWindow, then there
will be no change in the row/column status. However, if you move between
DataWindows, then the row status in the target DataWindow would be
NewModified!, meaning, an INSERT statement will be generated for the Update()
function call.
Q: I know pressing the Escape key after changing the data in a column will restore
the old data—basically the Undo functionality. Does PowerBuilder provide Undo
functionality for row insert/delete/update?

Ans: There is no built-in functionality like that.

If you need that functionality, you can maintain a DataStore and move rows to the
DataStore whenever user wants to delete them. You need to maintain those row and
column statuses in another DataStore or in an array. When user wants to undo it,
move those rows from the DataStore back to the original DataWindow and set row
and column statuses appropriately.

Q: Can I move/copy rows from Primary buffer to Original buffer?

Ans: There is no direct function or attribute to do that for you. However, as soon as
you change the data using SetItem() function or interactively, PowerBuilder copies
the original data into the Original buffer automatically. But, you can not have the
same data both in Primary and Original buffer at the same time and I don’t see any
need of that in real-world projects.

Q: I have a DataWindow with 10000 rows. I want to get rid of selected rows from
the DataWindow, i.e., PowerBuilder should not generate DELETE statement for
those rows when I call Update() function. What should I do?

Ans: You can delete those rows using DeleteRow() function, however, it will result in
DELETE statement generation when Update() function is called.

You can create a DataStore and move those rows into that DataStore using
RowsMove() function, and never call Update() on the DataStore; but it will be
additional overhead.

The best method is, call RowsDiscard() function. This function doesn’t move data to
Delete buffer, it simply gets rid of those rows from the DataWindow altogether.

Q: I have a DataWindow which has 10,000 rows and 200 rows among them are
modified. For some reason, I want to have all rows in the DataWindow including the
modified rows look like as if the DataWindow is retrieved just now. but I do not
want to retrieve them all over again from the database. Is it possible?

Ans: Yes, it is possible.

Solution 1: You can set the status of all those rows that are modified to
NotModified! using setItemStatus() function which might take longer time than the
later solution.
Solution 2: You can just call ResetUpdate() function which resets the flags for you
and empties the Delete buffer. You can use RowsDiscard() to discard all rows that
have New! or NewModified! status. Do not confuse ResetUpdate() with Reset()
function; the later function clears all rows from the DataWindow and looks like as if
no data was retrieved into this DataWindow.

Q: What exactly AcceptText() function does? Explain in detail.

Ans: AcceptText() function, after successful validation and ItemChanged event


execution, takes the content of the DataWindow's Edit control and copies into the
Primary buffer. This function triggers ItemChanged event; it triggers ItemError
event in the case of validation rule failure or datatype test failure or non-zero return
code from ItemChanged event. By default, this function is called by the Update()
function, unless you specify not to call in the Update() function.

Q: I have a DataWindow with one thousand rows and some rows among them are
modified. I set the DataWindow into QueryMode, and set it back to normal mode,
without retrieving from the database. Does this reset my DataWindow?

Ans: No. As long as you do not call Retrieve() function on that DataWindow,
switching between query mode and normal mode doesn’t affect the data in the
DataWindow.

Q: How do you update a DataWindow that is based on a multiple table join?

Ans: Well, you set one table as updatable in the DataWindow painter. At run-time,
call Update() function with FALSE as the second parameter. Upon successful
execution, make the second table as updatable and all other tables as non-
updatabale and call the Update() as described above and continue till the last table.
For the last table, call Update() function with TRUE as the second parameter and
commit the transaction upon successful operation. Finally, make the first table as
updatable which will be useful for the next update.

Q: What kind of DataWindows are not updatable?

Ans: DataWindows with Crosstab, Group, Composite, OLE presentation styles are
not updatable. You can set Group presentation style DataWindow updatable,
however you can't change the columns on which the group is based on.

Q: Explain the major difference between RetrieveRow, DeleteRow, InsertRow and


UpdateRow events in a DataWindow.

Ans: Other than the RetrieveRow event, the rest of the events doesn’t exist for a
DataWindow control/DataStore object. UpdateStart event is fired before
PowerBuilder starts applying DataWindow changes to the data source. Since then,
for each INSERT/ UPDATE/ DELETE statement, PowerBuilder fires SQLPreview
event. The functionality you are expecting in those non-existing events in your
question is available in the SQLPreview event.

Exercises:
Please complete the following exercises. We advise you not to download the solution
until you complete the exercises. Reading the solution without completing the exercise
might make things tougher in your real-life projects, since you never tried the code
before. If you have any questions, you can always e-mail me at prasad@applied-
software.com

1. We didn't write script to print the transaction in w_transactions window. Provide


that functionality to the user.

Tip: It would be very tedious to print multiple DataWindows on the same page. You
may want create another DataWindow object with group presentation style and
assign it to a hidden DataWindow control or use the DataStore object (Go for
hidden DataWindow control instead of DataStore since this topic is not covered
yet). Another tip: RowsCopy() function.

Solution: ex-11-1.zip
2. While entering the transactions, we are displaying the product information when
the user tabs out of the "tran_item_no" field. We didn't give the functionality to
display the product information when the user moves between rows using
keyboard or when clicking with the mouse button in the dw_tr_detail
DataWindow. Provide this functionality.

Tip: Write script in the RowFocusChanged event.

Solution: ex-11-2.zip
3. There is no delete functionality in the "w_transaction" window. Provide that
functionality. User should be able to delete a row in the dw_tr_detail
DataWindow and also he should be able to delete the header info record from the
dw_tr_head DataWindow. When (s)he tries to delete from the header
DataWindow, delete the whole transaction.

Tip: Read the code for the "ue_add" event and get an idea about what to write.

Solution: ex-11-3.zip
4. Do you remember writing script for ue_query and ue_retrieve events in
w_product_master window? We need to provide similar functionality, i.e., query
& retrieve functionality in w_transactions window also. You may want to use a
different DataWindow for query & retrieve purposes. When you retrieve data,
make the results DataWindow read-only. Double clicking on a record in the
results DataWindow should display dw_tr_header and dw_tr_detail
DataWindows, and allow the user to edit the clicked record in those
DataWindows. When the focus is in the results DataWindow, selecting 'Print
Preview' or 'Print' options from the menu should act on the results DataWindow.
In the query mode, allocate the whole window space for this purpose.

Tip: You may use the DataWindow control used to print a transaction. However,
create a different DataWindow object for this purpose. Use Modify() function to
put the DataWindow in read-only mode and vice-versa. Use ItemChanged event
to set the focus to detail DataWindow and "tran_serial_no" field.

Solution: ex-11-4.zip
5. This exercise is not part of the project. This exercise will give you good
experience in using PowerScript and in using various functions and events. Create
a window as shown below.

The functionality of this window is to allow the user to select a table owned by him/her.
Upon table selection, the script should bring the columns for the selected table and
display them in a list box, as shown in the step 2. You should allow the user to select
multiple columns from the ListBox. Allow the user to select the presentation style of the
report (s)he wants. When every thing is selected, and when the user clicks on the
CommandButton shown in step 4, generate the SELECT statement for the selected table
depending on the columns selected and create a DataWindow dynamically and display it
to the user. If the user doesn't select any column, display all columns in the selected table
for the report. If the user doesn't select any presentation style, set the default to 'Grid'.
 You can get table listing from 'systable' table and column listing from
Tip: 'syscolumn' table.
 Current user id can be obtained using user_id().
 Check the available presentation styles for dynamic DataWindows.

 DataWindow control can be used to bring table & column names.

Solution: ex-11-5.zip
Object Orientation—PB Implementation
In previous sessions, we have created "Product Management System" with SDI and MDI. The application
is almost ready. However, we haven't taught you about object orientation, which is very important. By
this time, we hope you are comfortable with PowerBuilder terminology, programming and so on. Now,
let's learn what OOP is and how PowerBuilder implements OOP principles.

In This Session You Will Learn:

 OOP terminology.
 OOP concepts.
 Inheritance.
 Encapsulation.
 Polymorphism.
Estimated Session Time:

180+ minutes

Prerequisites:

 None

What Are Objects?


They're nothing but software programming models. In your everyday life, you're surrounded by objects:
cars, coffee machines, phones, fax machines, and so on. Similarly, software applications contain objects:
buttons, spreadsheets, spreadsheet cells, property lists, menus, and so on. These objects have states and
behaviors. You can represent these things with software constructs called objects, which can be defined by
their state and their behavior.

Take your everyday transportation, a car, it can be modeled by an object. A car has state (how fast it's
going, in which direction, its fuel consumption, and so on) and behavior (starts, stops, turns, slides, and
runs into trees).

You drive your car to the office, where you keep track of your stock portfolio. In your daily interactions
with the stock market, a stock can be modeled as an object. A stock has state (daily high, daily low, open
price, close price, earnings per share, relative strength), and behavior (changes value, performs splits,
has dividends).

After watching your stock tumble, you rush to the cafe to console yourself with a cup of hot coffee. The
espresso machine can also be modeled as an object. It has state (water temperature, amount of coffee in
the hopper) and it has behavior (emits steam, makes noise, and brews a perfect cup of coffee).

Objects Basics
Every object has a state and behavior. For an object, attribute defines the state and service describes the
behavior.

The attributes of an object are the characteristics (that represent the properties) that give it a distinct
identity. They may be called attributes, properties, or fields, but, they always represent the same thing.
Attributes represent the set of properties to which values can be assigned in order to describe the object,
and thus establish its identity. In practical terms, attributes are the data elements that can be accessed in
an object. In programming implementation of an object, attributes are defined by its instance variables.
Instance variables are private to the object. Unless explicitly made public or made available to other
"friendly" classes, an object's instance variables are inaccessible from outside the object.

An object's behavior is defined by its services. Many different terms are used to refer to the services of
objects. They can be called functions, services, responsibilities, methods or operations. They all represent
something, which responds either directly or indirectly to the actions, directed at one object by other
objects. In practical terms, services are the processes that can be triggered in an object. Services
manipulate the instance variables to create new state; can also create new objects.

The small picture above is a commonly used graphical representation of an object. The diagram illustrates
the conceptual structure of a software object - it's like a cell, with an outer membrane, its interface to the
world, and an inner nucleus protected by the outer membrane.

Class
A class is a software construct that defines the data (instance variables) and services of an object. A class
in itself is not an object. A class is a template, which defines how an object looks and behaves, when the
object is created or instantiated from the specification declared by the class. You obtain concrete objects
by instantiating a previously defined class. You can instantiate many objects from one class definition, just
as you can construct many houses from a single architect's drawing.

Instances
Objects that behave in a manner specified by a class are called instances of that class; these instances are
created, when a message to create it is received by the class. All objects are instances of one class or
another.
Once an instance of a class is created, it behaves just like other instances of its class and is able to
perform any service for which it has methods. An application can have as many or as few instances of a
particular class as required.

Abstract Class
An abstract class, simply stated as a catalog of common services and attributes. It is the one in which
there is no physical representation of its associated object. In other words, the object associated with an
abstract class can have no instances. Abstract classes are formed to represent the common services and
attributes extracted from objects during classification and are the upper-level classes in an inheritance
(we will explain in a moment) hierarchy. The distinguishing feature of an abstract class is that it always
has subclasses.

Abstract classes are often used to define default process services and to set the initial conditions of data
attributes of the objects associated with its subclasses. The services and attributes contained in an
abstract class tend to be separate and distinct from one another. They are merely cataloged together
because they all serve the same set of lower-level objects. Each has its own internal design, which can be
modified at any time without disrupting the integrity of the design of the other objects. Just as the
services and attributes of an abstract class can be used in its subclass objects, an abstract class can make
use of the services and attributes from a higher-level abstract class to which it is a subclass.

Concrete Class
The classes of objects remaining after extracting common services and attributes are said to be concrete
classes. A concrete class is the one in which there can be a physical representation of its associated object
at runtime. In other words, the object associated with a concrete class can have instances. They are the
lower-level classes in an inheritance hierarchy. Concrete classes may or may not have subclasses, but the
distinguishing feature of a concrete class is that the object associated with it can always have instances at
runtime.
Messages
Unlike passive data items in traditional systems, objects have the ability to act. An action occurs when an
object receives a message, that is, a request asking the object to behave in a certain way.

When an object sends a message to another object, the sender is requesting the receiver to perform the
named service and possibly return some information. When the receiver receives the message, it performs
the requested service in any way that it knows. The request doesn't specify how a service has to be
performed - such information is always hidden from the sender.

The set of messages to which an object can respond is known as the behavior of the object. However,
not all messages that an object can respond to needs to be part of its publicly accessible interface. An
object can send private messages to itself, to implement publicly accessible services.

For example, suppose a drawing object has received a message to draw a pie chart. The object may
internally call a method to calculate the percentage of each member in the pie chart. However, if you send
a message to the object to calculate the percentages for each member, it would result in an error saying
that this service isn't available. Since it is private, it can be called only by another method of the same
object.

Methods
When an object receives a message, it performs the requested service by executing a method. A method
is a step-by-step algorithm, which is executed in response to a message, whose name matches with the
method. A method is always part of the private representation of an object; it is never part of the public
interface. For example, an object can send a message to the drawing object to draw a circle, but the
sender object never says how to draw the circle.

SubClass
A Subclass is a class that inherits behavior from another class. A subclass inherits all the behavior of its
parent class and then adds its own specific behavior to define an unique object.

SuperClass
A Super class is a class from which specific behaviors are inherited. A class might have only one Super
class, or it might have several, combining behaviors from several sources and adding only a little of its
own to produce its own unique object.

Inheritance
Inheritance is a mechanism, which allows objects to share attributes, and services based on a predefined
relationship; it allows you to define a hierarchy of objects. The inherited object is called a descendant
object, while the object from which the descendant is inherited is called an ancestor.

If we define a vehicle on certain attributes such as height, weight, speed, maximum load and so on. Even
though vehicles share these common attributes, they are not same. We might have a car, a truck and an
emergency vehicle, each with their own specific attributes.

The first thing we need to do is define a class, called Vehicle, which has the general attributes of our
vehicles. From there on, we can derive other classes such as a Car class and a Truck class by inheriting
from this Vehicle class. The Vehicle class is called the ancestor, and the Car and Truck classes are called
descendants.

Inheritance hierarchies are a means of classifying objects. They are inverted trees of common
subroutines. We use the term inverted, to convey the sense that, the commonalty of objects is portrayed
upwardly as in a tree as compared to the downward representation of common modules in a structure
chart. Each higher object represents a set of services and attributes common to all levels to which it is
connected below.

Each object in the tree is called the class of the objects below it. An object will inherit services and
attributes as common characteristics from the class to which it belongs. This means that you need not
redesign these characteristics every time you use them in an object, yet can reference them as if they
were a part of the object.

Base Classes
In this example, Vehicle class is called a base class. Base classes are not meant to be used directly, but
they are used to define the general attributes that appear in the derived objects. As you descend the
hierarchy, you define more specific attributes and functionality.

The descendant class, a Taxi, inherits all attributes, variables, structures, functions and behavior from the
ancestor, the Car class. An ancestor can have any number of descendant classes, and a descendant class
can become an ancestor to other descendants. The attributes defined at the ancestor level are not copied
into the descendant class, but are passed down to the descendant for reference.

If you change attributes at the ancestor level, the changes reflect in descendent objects immediately. At
the same time, changes done at the descendent level are not reflected at the ancestor level. In the above
example, additional functionality defined at Car class is available to Taxi, but not to the Vehicle class. This
allows you to localize changes and localize testing.

Inheriting the Code


You can design class hierarchies just for code reuse or for sharing the interface or a mix of these two. In
this section let's examine inheritance for code reuse.

Let's consider a program for designing data entry forms, where user fills out fields on the screen. The data
entry forms may be for different purposes, i.e., entering a record in the inventory master, or entering a
transaction into a transaction file, or for a purchase order. Note that, all these forms share functionality,
such as, querying the form, printing the form. As a result, you can implement this functionality in the base
class. You can save yourself the effort and as well as reduce the size of the program by defining a base
class called data-entry-form that implements the above mentioned common functionality. Such a class
hierarchy also reduces the effort required to fix bugs or add features, since the changes have to be done
in one place only.
A class hierarchy, designed for code sharing has most of its code in the base class (near the top of the
hierarchy). This way the code can be reused in many classes. The derived classes represent the
specialized classes, i.e., extend the base class's functionality.

Inheriting the Interface


Using this strategy, you can design class hierarchy to inherit just the interface by the derived classes, and
not the code. You write the actual code in the derived class. By this, the derived class has the same
interface (in other words, same function names and arguments) as the base class, but behave differently
for the same method.

Consider the previous example, every data-entry-form class, i.e., master entry, transaction entry have
some common functionality, saving the data entered by the user. Since all these classes have common
functionality, you may want to define the method "Save" at the base class. However, the implementation
of saving the data is different from form to form. For example, master entry form is saving the data to the
master file, while transaction entry form has to check for existence of the information in the master file
and also update the master file whenever it writes to the transaction file. Thus, individual form objects
may exhibit different behaviors, but they all share the same interface and so can be treated as 'data-
entry-form' objects.

In this situation, you can define a method "Save" at the base class with no code in it and write the actual
code in the derived classes. To do this, you need to use polymorphism, which is explained later in this
chapter.

A class hierarchy designed for interface sharing has most of its code in the derived classes (near the
bottom of the hierarchy). The derived classes represent working versions of an abstract model defined by
the class.
Some times, you may need to design a class hierarchy, which has to implement both code and interface
inheritance. If you take the examples from both the topics, and combine them, i.e., some methods, and
some interfaces are common. So, the final diagram would look like the one shown below:

Multiple Inheritance
Inheritance hierarchies need not be limited to single inheritance. Multiple inheritance is complex to
understand, but it follows the same principles as single inheritance. Multiple inheritance is a set of many-
to-many parent/child relationships among the classes associated with objects. Multiple inheritance allows
an object to inherit functionality from more than one object.
In the above example, the Ambulance class not only inherits functionality from the Car, but also from the
Emergency class. It therefore gets all the features of an Emergency vehicle, together with those on offer
from the Car.

Multiple inheritance is useful in situations where there are multiple groups of common characteristics,
which partially overlap one another.

Benefits of Inheritance

 Allows reuse of code.


 Reduces development time.
 Improves consistency, both internally within the application and between
them.
 Reduces the chances of error.
 Makes maintenance easier.
 Reduces space usage.

Encapsulation
Encapsulation is a process of hiding the internal workings of a object, to support or enforce abstraction. It
also attempts to separate an object's interface from its underlying implementation.
Methods (object's interface) are the services that are provided for other objects to interact with this
object. Methods operate on the data defined at the object level and are typically implemented by defining
functions at the object level. Methods can take input and (sometimes) generate output. In simple terms, a
method describes what an object can do, while the implementation describes how it does it. This
distinction helps in hiding details of the data and complexity of the method implementation from other
objects, by exposing only the relevant properties of an object; a user views an object in terms of the
services it can perform, and not in terms of its data structure.

For example, return to our idea of a drawing object. As you already know, you can define attributes as
well as associated methods for the drawing object - in this case, these methods include the logic required
to draw the object. By defining methods at the object level, an object can call a function draw() with a
radius and a center point to draw a circle.

Sometimes encapsulation is defined as the act of combining functions and data, but it is misleading. You
can join functions and data together in a class and make the members public, but that is not an example
of encapsulation. A truly encapsulated class "surrounds" or hides its data with its functions, so that, you
can access the data only by calling functions.

One of the advantages of this 'method hiding' is the logic maintenance. As long as you leave the message
syntax alone, you can alter the contents of the method without needing to alter any references to the
draw() function throughout the application.

Benefits of Encapsulation
The benefit of encapsulation is to guarantee that an object satisfies application-defined integrity
constraints.

Polymorphism
In simple words, Polymorphism is the ability of a function to behave differently, depending on the context
in which it is called.

Consider the example of calculating the salary of an employee. An employee can be a worker, a sales
person, a manager, etc.. The calculation for each employee might be different. To implement this, you
need to use either CASE or if.. else if., to call different versions of ComputePay() function. Using the CASE
statements becomes a nightmare when modifying the system, since, each CASE statement needs to be
updated. Polymorphism gives you the flexibility of avoiding the CASE and if.. else if statements. Here, at
run-time, the compiler takes care of calling the appropriate function depending on the context in which
the function was called.

Polymorphism can be implemented in two ways:

 Overloading
 Overriding

Overloading
A function is overloaded when it is defined more than once either at the same object level or in its
inheritance hierarchy with the same name.

When a function is defined more than once with the same name, the interface will be different in one of
the following way:

 The number of arguments.


 Same number of arguments but, different datatypes.
For example, the MessageBox function. It displays a given message on the screen. You can call
this function with two parameters:

MessageBox("Title", "Message to Display")

You can also call the same function with five parameters:

MessageBox("Title", "Message to Display", Icon, Buttons, DefaultValue)

Here, you are calling the same function, but the number of arguments is different.

In the first format, both parameters are of string data type. However, there is another format
that accepts both String and Integer:

MessageBox( "Title", Error-Number )

You can implement function overloading in two ways. If the language allows you to define it at the same
object level, you can do so. Otherwise, you need to use inheritance to implement function overloading. At
the highest level, define the function and create a new class by deriving from the highest class and define
the same function in the derived class with either different arguments or different data types, depending
on the need. The later method comes with a price, if you want to implement more than two flavors of the
same function, since, you need to derive one class for each format of the function. In the above example,
you need to use at least three levels of inheritance.

Compiler resolves the references at run-time, depending on the context, i.e., the arguments supplied,
argument datatypes, etc... For objects using inheritance, the compiler searches up the inheritance
hierarchy to resolve an implied call to the function.

Inclusional Polymorphism OR Overriding


For this, you need to use inheritance. In function overriding, the function name and arguments are
typically same (though they can be different), but the code inside the function is different. For example,
you can define a 'Save' function in the 'data-entry-form' object at the higher level and can have different
code in each of the derived object, by defining the same name with different code. Saving a master form
needs to update only the master file, and saving a transaction form needs to update master file as well as
transaction file. In cases like this, function overriding can be used. This is also called as 'inclusional
polymorphism'.

Operational Polymorphism
When multiple classes that inherit from the same parent implement the same function in different ways
with the same interface, it is called 'operational polymorphism'.

Delegation - Aggregate Relationship


In this method, the main object offloads processing to other objects —called service objects. When the
main object requires the service it invokes the service object and the service object acts on the object that
requested the service. The association between the main object the service object is called 'aggregate
relationship'. This is explained in detail in the PFC (PowerBuilder Foundation Class library) session

Inheritance in PowerBuilder
PowerBuilder supports inheritance from three objects:

 Menus
 Windows
 User Objects
You can inherit an object by selecting File > Inherit menu option and selecting the parent object.

When an object is inherited, everything in the ancestor object is passed onto the descendent. If another
object is derived from the descendent, the new object inherits everything from the first ancestor, as well
as any new definitions defined in its immediate ancestor.

Inheriting a Window
In the descendent window, you can do any of the following:

 Override or extend the inherited events scripts.


 Override and extend some events scripts.
 Override/Overload object functions.
 Reference the ancestor's events, functions (as long as they aren't declared
as private).
 Change attributes of controls and the content of variables.
These are the things that you can't do:

 Delete any control in the descendent window if they are painted in the
ancestor.
 Change the names of the inherited controls in descendant window.
You can change the position of controls in the descendant window, but if you do so, further changes to the
position of the control in the ancestor window will not be reflected in the descendant window. The same is
true for changes made to any other attribute in the descendant window - It is something like overriding
ancestor attribute values.

Scripts
When you go into the Script Painter, there can be three versions of the standard script icon, displayed
beside the event names:

Icon Description

Full Color Script Only ancestor has the script.

Black & White Script Only descendant has the script.

Color & BW Script Both ancestor & descendant have script.

Viewing Ancestor Script


When you invoke the Script Painter for a descendent window, PowerBuilder doesn't display the ancestor
script. For example, if you open a new window and inherit from w_about window in the example
application (PBEXAMFE.PBL), you won't be able to see script for the open event. However, you will see a
colored script icon beside the event, indicating that there is script in the ancestor window.

To see the script for the ancestor window, you need display popup menu in the script painter and select
'Go To > Ancestor Event' menu option. You are not allowed to edit an ancestor script from the
descendant, but needed, you can copy and paste the script to and from the clipboard and then edit it. To
come back to the current object's script, select 'Go To > Descendent Event' menu option.

There is an easy of viewing ancestor script up to any level without going so many menu options. Just click
on the right most DropDownListBox in the script painter and select the ancestor object you want to see
the script. PowerBuilder lists objects in that DDLB in the reverse inheritance hierarchical order. The grand
parent on the bottom and it's descendent on top of it and so on. The following diagram displays the same
for a window inherited from PFC w_sheet window.

Extending Ancestor Script


If both the ancestor and descendant windows have scripts, then the script for the ancestor window will be
executed first, followed by the descendant window script. This is PowerBuilder's default behavior and is
specified by displaying popup menu and turning on 'Extending Ancestor Event' menu option.

Overriding Ancestor Script


If you don't want to execute the ancestor script, you can display popup menu and turn off 'Extending
Ancestor Event' menu option. When this option is turned off, the ancestor level script isn't executed -
results you get from this object when this event occurs are related to the descendant level script. In cases
where you don't want to execute an ancestor level script and where there is no script in the descendant
window, simply add a comment in the descendant script, after selecting the above option. If you don't do
this, PowerBuilder assumes that you still want the ancestor script to be executed and will ignore the menu
selection.

Calling Ancestor Scripts


Sometimes, you may want to execute a script for the descendant window before the ancestor script is
executed. For that, follow these steps:

 Display popup menu in the script painter and turn off 'Extending
Ancestor Event' menu option.
 Write the required script for the descendant window.
 Call the ancestor script using the following syntax:

Call Super::<Event Name>

The SUPER pronoun refers to the same object in the ancestor. You can also execute an
event of another control in the ancestor by using the following syntax:

Call Super'<Control Name>::<Event name>

For example, to execute the script for the OK CommandButton in our w_about window from the
close event of a descendant window, we would use the following code:

Call Super'cb_ok::clicked

Inheriting Menu Objects


Inheriting menus is similar to inheriting windows, but the following restrictions apply. You can't:

 Change the order of menu items.


 Delete menu items.
 Insert menu items between other items.
However, PowerBuilder allows you to change the order of menu items by selecting ShiftToRight option in
the inherited menu object.
In the above picture, the bottom menu item separator, Close and Printer Setup options have ShiftToRight
option turned-on by that the newly added 'Close' menu option in the descendent will show up right below
the menu item separator located below Open menu option.

If you do it for a menu item on the menu bar, it shifts all the way right on the menu bar.

Multiple Inheritance
PowerBuilder doesn't directly support multiple inheritance. For example, to inherit properties and
functionality for a window, from two other windows, you need to workaround using user objects. Instead
of building functionality in two windows, you divide the functionality and create a window and a user
object. When you create your window, you need to inherit from the window and place the user object in
the descendant window to get the required functionality.

We can use our vehicle analogy to show the difference between a traditional implementation of multiple
inheritance and PowerBuilder's implementation.

Traditional Implementation of Multiple Inheritance


PowerBuilder's Implementation of Multiple Inheritance

The emergency vehicle user object is placed in the fire engine object to give it the functionality of both an
emergency vehicle and a truck.

Encapsulation in PowerBuilder

Variables & Scoping


Declaring a variable in Power Script is simple. The following example declares an integer
variable 'li_counter'.

Int li_counter

You can initialize a variable at declaration time as shown below:

Int li_counter = 10
All numeric variables are initialized to zero by default at declaration time, unless you specify some value
as shown above. String variables are initialized to zero length, i.e., "". You can store up to 60000
characters in a string variable, however, when you declare a string variable, it won't occupy that much
space; when you assign some value to the string, its length is automatically set to the length of the value
specified.

When the user creates an object, it doesn't belong to a new type, but is an instance of an existing class.
For example, if I create a new window, the resulting object is an instance of the window class inherited
from PowerBuilder's window class. The resulting object is of type WINDOW, which has all the attributes
and properties defined to that class.

Similar to traditional data type variables, you can declare PowerBuilder objects as variables.
For example, the following example declares a window variable and a user object variable.

Window lw_Window1
uo_datawindow ludw_DataWindow1

When you declare a variable in an event script, it is destroyed as soon as the script
completes execution. Variables declared in an event script are called 'local' variables. A
variable in PowerScript can have one of the four levels of scope. When considering
Object Oriented programming, it's important to keep track of the variables you have
access to, in different areas of the application. The following table summarizes these
variable types:

Scope Description

Local These variables are available only in the declared script and cannot be accessed by any other script or function. They are
when the script or function completes execution.

Instance Each instance of an object has its own set of instance variables. They are available to all object level scripts and function
destroyed when the instance is destroyed.

Shared These variables are available to all object level scripts and functions. They are shared among instances of an object and
memory even after the last instance of an object is destroyed. These variables are not available to its descendants.

Global These variables are available to all scripts and functions in an application and are destroyed only when the application ex
completed.

Local variables are declared in event or function script. To declare variables with other scope, you need to
display popup menu in the script painter select 'Go To > <Scope> Variables' menu option, where
<Scope> is either Instance/Shared/Global and declare the variable as shown below, in the dialog box.

As soon as the script execution completes, a local variable is no longer available. You might think that a
local variable would be available in the same event script for the descendent; it's not true. Even though
you can execute ancestor event script, ancestor and descendent event scripts are entirely independent,
i.e., events/functions don't have access to other event/function's local variables.

Declaring an instance variable is nothing but adding new attributes to the object. The default
attributes available at any PowerBuilder object are nothing but instance variables, declared at
the object level by Powersoft. You can change the attributes, by specifying the values as you
paint the object or at run-time using scripts. For example, take the Window painter. You paint a
Window in the Window painter and specify certain attributes, such as window type, color, title,
associated menu and so on at design time. However, you can change the title of the window at
run-time using the following code:
Window_Name.Title = "New Title"

Similarly, you can also access the declared instance variables. For example:

Window_Name.ib_RegularClose = True

The contents of instance variable of an object, in each instance, are independent. In contrast, as the name
says, the shared variable is shared among instances of the same object. Even though you can't access a
shared variable after closing all the instances of an object, the variable content is retained in the memory.
When another instance is created, the previous value is still there. Creating instances is explained later in
this chapter.

Name Spaces
You can declare a global variable and a local variable with the same name. In such cases, in
versions prior to 5.0, you can't access the global variable from the script in which the local
variable is declared with the global variable name. With version 5.0, compiler gives you an
informational message, and you can access both as shown below, even when their names are
same:

var1 = 100 // Sets the local variable


::var1 = 200 // Sets the global variable

"::" (double colon) is the global scope operator and allows accessing global variable from the script in
which the same variable is declared as a local.

If you have declared an instance and global variables with same name, referring to them
without any prefix would refer to the global variable. Note that, doing the same with local and
global variables would refer to the 'local' variable and not the global. You need to use THIS
pronoun to access the instance variable.

// Global and instance variables are declared with same name.


This.var1 = 100 // Sets instance variable
var1 = 100 // Sets global variable
::var1 = 100 // Sets global variable
// Global, instance and local variable are declared same.
This.var1 = 100 // Sets instance variable
var1 = 100 // Sets local variable
::var1 = 100 // Sets global variable

Access Levels - Variables


As we have seen, when instances from generic classes are created, they acquire two types of variables:
instance and shared variables. The instance variables are used to hold any information that is directly
related to that instance of the object, while shared variables hold information that is applicable to all of
the instances derived from their associated class.

Scoping of variables can be very useful. In certain circumstances, it becomes even more necessary to
further refine the availability of these variables, throughout the application. You must remember that even
though an instance variable holds information directly related to the current instance, it is still available
for interrogation and manipulation by other objects in the application.

To solve this problem, PowerBuilder provides Access Levels. Access levels say, which
object can access the specified variable. You can specify three different access levels:
Access LevelDescription

Public Accessible by all objects in an application - this is the default access level for all user defined objects. Global variabl
always public.

Protected Only accessible by that object and also by its descendant objects.

Private Only accessible by other functions and scripts declared for the same object. Shared variables are always Private.

These user defined access levels only apply to instance variables, because other variable types have
access levels that can't be changed.

By default all instance variables are public. To specify other access level to instance variables
declared at the object, specify the keyword private or protected before the data type. For
example:

Private Int ii_counter1, ii_counter2

When you specify an access level, all the following variables are assigned that access level until
another access level declaration is encountered. In the following example, all the variables will
be Private, until it encounters either Public or Protected.

Private:
Int ii_counter1
Int ii_counter2
Protected:
Int ii_counter3

With version 5.0, further restrictions (listed below) can be applied on variables.

Modifier Description

PrivateRead Only the defining class can read.

PrivateWrite Only the defining class can write.

ProtectedRead Only the defining class or direct descendants can read.

ProtectedWrite Only the defining class or direct descendants can modify.

To support new restrictions, the syntax is modified as follows:

{Visibility}{ReadAccess}{WriteAccess}<data type><variable name>

Declaring a variable "Protected PrivateRead PrivateWrite" makes the variable visible and
prevents its descendants from declaring the same variable again. PowerBuilder gives an
informational message as shown below, when you try to declare it again, but won't prevent you
from saving the object.

Information C0148: The identifier 'ii_wo_var1' conflicts with an


existing property with this name. The new definition of 'ii_wo_var1'
will take precedence and the prior value will be ignored until this
version of 'ii_wo_var1' goes out of scope

If you try to read or modify the variable in the descendent scripts, you see the following
message. In short, declaring the same variable in the descendent is of no use.

Error C0158: The property "ii_wo_var1" was found in class "w_parent",


but insufficient rights are available to read its value.
Error C0143: This property only be modified by an event or function in
it's parent class.

In some cases, like the following code, it won't prevent you from declaring and initializing it, at
the same time.

// Ancestor
Protected PrivateWrite ii_wo_var1 = 100
// Descendent
/* You will get information message, but it won't
prevent you from declaring it. */
ii_wo_var1 = 200
// In the script
// The MessageBox() displays 200
MessageBox( "Value in descendent", ii_wo_var1 )

The following table lists possible combinations and its validity.

Read/Write Modifier/ Visibility Modifier Public Protected Private


   
ProtectedRead X
   
ProtectedWrite X
   
PrivateRead X
   
PrivateWrite X

Access Levels - Functions


Two types of functions can be declared in PowerBuilder; global functions and object functions. Declaring a
function in function painter becomes global function. To declare a function at an object level, you need to
display popup menu in the script painter and select 'Go To > Function' and select '(New Function)' from
the second left DDLB. Global functions are always public. You can specify access levels for object functions
in the function declaration dialog box, as shown below:
You can't specify Read/Write modifiers for functions, which makes sense.

Choosing Between Events and Functions

The following table lists the differences between events and functions.

Event Function

Executing a non-existing event returns null Executing a non-existing function, generates error and it
value. has to be taken care in the error handling.

An event is always public, and can be called by You can define access levels to a function.
any object.

In the descendent object, you have the option of Declaring the same function with the same interface in
extending or overriding an event script. the descendent object always overrides the ancestor
function.
The searching order for unqualified names is done in the
following order:
When calling an event, the search starts from
ancestor down to the object from where it is
called. Global External Functions (External functions are
explained in a later session)

Global Function.

Local External Functions.

Object Level functions. (When calling a function the


search starts from the descendent and upwards.)

System Function.
Events can't be overloaded. Functions can be overloaded.

Creating an Instance
It requires only one line of code to create and display an instance of an object on the screen.
The following script would create an instance of Window, Sheet and User Object respectively,
with and without parameters:

/* Create an instance of a window in memory and


display the created window on the screen */
// Open() Format 1,2
Open( WindowVariable {, ParentWindowVariable} )
Open( WindowVariable, WindowNameString {, &
ParentWindowVariable} )

The first format opens an instance of the specified window. If you want to open a child window,
specify the parent window name in which you want to open the child window as the second
parameter. The second format allows you to specify the window name as a string in the second
parameter, and place the reference to the window instance after opening in the first parameter
window variable.

// OpenWithParm() Format 1,2


OpenWithParm( WindowVariable, Parameter &
{,ParentWindowVariable})
OpenWithParm( WinVar, Parm, WindowNameString &
(,ParentWindowVariable})

This format works similar to the above formats, and also allow sending parameters to the
opening window. The parameter is stored in the message object. In simple terms message
object is nothing but a global structure variable, which is used for inter object communication.
This is discussed in detail in "Inter Object Communication" topic.

/* Functions to open a window as a sheet */


OpenSheet( Windowvar {,WindowNameString}, MDIWindowName &
{, Position {, WindowArrangeStyle}} )
OpenSheetWithParm(Windowvar, Parameter {, &
WindowNameString},MDIWindowName {, Position{, &
WindowArrangeStyle}} )

These formats allow you to open a window as a sheet, in either MDI frame or MDI frame with
MicroHelp window. The following example opens w_sheet in w_frame window and then
cascades all windows.

OpenSheet( w_sheet, w_frame, 2, Cascaded! )


/* Functions to open a User Object. */
WindowName.OpenUserObject( User Object{, x, y} )
WindowName.OpenUserObject(User Object, &
ObjectType{, x, y })
Windowname.OpenUserObjectWithParm( User Object {, x, y })
WindowName.OpenUserObjectWithParm (User Object, &
ObjectType {, x, y })

These formats allow us to open user objects and place them in the specified window, at the specified x
and y co-ordinates.
Any object you define through PowerBuilder painter, is declared as global. If you export
w_variable_test window from the Library Painter and then have a look at the .SRW file, you'll
see the following:

$PBExportHeader$w_variable_test.srw
forward
global type w_variable_test from Window
end type
type mle_help from multilineedit within w_variable_test
end type
type cb_3 from commandbutton within w_variable_test
end type
type cb_2 from commandbutton within w_variable_test
end type
type cb_1 from commandbutton within w_variable_test
end type
end forward

The parameter to this function may be, either the name of a window or a variable that refers to
an instance of a specific window. Either of the following is correct:

/* Method 1: Specifying the window name directly */


Open(w_item_master)
/* Method 2: Using a variable */
w_item_master lWindow1
Open( lWindow1 )

The first method only allows you to define one instance of an object; this is because of how PowerBuilder
allocates memory to the objects created. When you create an instance of an object, PowerBuilder
allocates it some memory. If you try and call this function again, PowerBuilder simply returns this instance
to you. This is because, the instance has a global scope; that means, a reference to the instance is
recognized throughout the application and can't be duplicated.

The second method allows you to open more than one instance of the window, because it is basing the
instance of an object on a variable that can be given a scope. Imagine that you are opening this window
from a menu option. If you declare it as an instance variable, the variable is created when the menu is
created and is destroyed when the menu is destroyed. This means that as long as the menu exists, the
memory allocated for it is same, which allows you to open only one instance of the window, no matter
how many times you try to open the window.

To open more than one instance of the window, you need to declare the window variable as local in the
menu script. The local variable is created when the script starts executing and will be destroyed only when
the execution of the script completes. That means that each time the script executes, a new variable is
created and memory is allocated to it. This makes it possible to open more than one instance of the
window.

Unfortunately, there is one problem associated with this; the external reference to other windows, except
for the active window, is gone. Suppose you opened four instances of a window and with the third
instance active, you may want to disable a CommandButton in the first instance. Using the above method,
you don't have the ability to reference the first instance specifically, so, you can't disable the
CommandButton. In interactive debugging too, you can't see other instances. In this situation, you may
have to put some debug statements, something like MessageBox() for debugging.

The solution for this problem is to declare an array of instance window variables. For example:

// Declare these 2 variables as instance variables for


// the menu
w_item_master i_item_master[]
Int InstanceNo = 1
// Script for the menu item
OpenSheet(i_item_master[i], ParentWindow, 1, Cascaded!)
InstanceNo++

The above functions create an instance in the memory, and those instances are displayed on
the screen. Sometimes, you may want to create an instance, but don't want to display on the
screen. Well, for that, set the visible attribute after creating the instance. Otherwise, you can
use CREATE statement. The CREATE statement is used to create an object only in memory. The
syntax is as follows:

CREATE <Object Name>

At times, you may want to connect to two different databases at the same time. So, you need
two transaction objects. One transaction object SQLCA is available to us by default, so, we
need to create one more. The following code creates a new transaction object instance.

Transaction g_TranForSybase
g_TranForSybase = Create Transaction

Now, g_TranForSybase is available for use in the code. The following code sets proper values,
by which we can use this for database connection.

g_TranForSybase.servername = &
ProfileString( "sales.ini", "sybase", "server", "")
g_TranForSybase.logid = &
ProfileString( "sales.ini", "sybase", "logid", "")
g_TranForSybase.logpass = &
ProfileString( "sales.ini", "sybase", "logpass", "")
g_TranForSybase.database = &
ProfileString( "sales.ini", "sybase", "database", "")
g_TranForSybase.dbparm = &
ProfileString( "sales.ini", "sybase", "dbparm", "")

The following line connects to the database:

Connect using g_TranForSybase


// Error Handling...

Transaction object has no visual component, so, creating it in the memory is well enough. If
you look at the exported version of the application, you can observe the CREATE statements
that PowerBuilder uses to create global objects internally:

on oop_pb_impl.create
appname = "oop_pb_impl"
sqlca = create transaction
sqlda = create dynamicdescriptionarea
sqlsa = create dynamicstagingarea
error = create error
message = create message
end on

With version 5.0, we can create an instance of a class if we have the name of the class in a
string. The following example lists all the menu names from a specified library and creates the
selected menu in memory.
// Read All menu entries in the library and populate
// the datawindow
dw_menus.ReSet()
dw_menus.ImportString ( Librarydirectory( &
"userlib.pbl", DirMenu! ) )
// instance variables
menu i_Menu
String i_user_menu_name
// Get user selected menu into i_user_menu_name
i_user_menu_name = dw_menus.GetItemString( &
dw_menus.GetRow(), dw_menus.GetColumn() )
i_Menu = Create Using l_user_Menu_name
// Now you can find all menu items names, bitmap names,
// etc...

With version 5.0, you can automatically instantiate user object by setting AutoInstantiate
attribute. If you set this option to true, the following line is equal to two lines following after
that line.

nc_string_functions l_str_cls
// The above equals to two of the following lines, if
// "autoinstantiate" is not set
nc_string_functions l_str_cls
l_str_cls = create nc_string_functions

Till now we have seen how to create instances, now let's see how to destroy those created instances.

Destroying an Instance
There are two ways of destroying an instance. It depends on how the object was created. If the
instance was created using one of the Open function, you should use the Close() function:

Close() /* Clears an instance of a window from the


screen and from memory */
CloseUserObject() /* Clears an instance of a User Object
from the screen and from memory */

If it was created by using the CREATE statement (a transaction object, for example), you
should use the DESTROY statement:

DESTROY <Object Name> // Removes the object from memory

You can only create and destroy one object at a time. When you create an instance, by declaring the
variable as a local variable, it is destroyed automatically after the execution of the script. However, it is a
good programming practice to explicitly destroy all objects created by Create, by using the Destroy
command.

Garbage Collection
From version 6.0, PowerBuilder automatically removes all unreferenced objects from the
memory in its default setting of 0.5 seconds. The garbage collection removes objects when
objects goes out of scope—an class variable declared in a script and the script execution
completes— and also when another object is assigned to the declared variable. For example:

// No memory is allocated at this time.


uo_xyz luo_1
// uo_xyz is instantiated and memory is allocated.
luo_1 = CREATE uo_xyz
// Other code.
luo_1 = auo_abc
// another object is assigned to luo_1, so 'garbage
// collection' feature removes uo_xyz from the memory in
// its next round of collection.

You may have got a question, what if, if I post an event and the current script completes before the
posted event starts execution? Good question. PowerBuilder adds an internal reference to all objects that
are passed to the posted events and functions, by that those objects will not get removed by the 'garbage
collection' feature since they have at least one reference.

There are three exceptions to the garbage collection. The first one is visual objects, garbage
collection do not automatically remove visual objects from memory instead they are removed
when they are closed. For example, the following code written for the clicked event of a menu
item is declaring a local window variable and opening that window. Once the script completes,
the reference to the window is gone, so that window should be removed.

w_product_master lw_PrdMstr
OpenSheet( lw_PrdMstr, ParentWindow, 0, Cascaded! )

The second exception is running timing objects. When you start the timing object using Start()function,
PowerBuilder adds an internal reference to the timing object.

The third one is shared objects for which PowerBuilder adds an internal reference when you issue
SharedObjectRegister() function.

There are three functions available to control garbage collection.

 GarbageCollectGetTimeLimit()—returns the current garbage collection


interval time in milliseconds.
 GarbageCollectSetTimeLimit()—sets the garbage collection interval
time in milliseconds.
 GarbageCollect()—forces the garbage collector to collect the garbage
now.

Polymorphism in PowerBuilder
PowerBuilder allows you to implement polymorphism in two ways:

 Sending Messages.
 Function Overloading.
 Function Overriding.
PowerBuilder itself implements all of these in the PowerClass. Let's look at each method one after the
other.

Sending Messages
If you look at the PowerBuilder system class hierarchy, you'll see that PictureButton control is inherited
from the CommandButton control. This makes sense, as both perform the same function.
Whenever you click a CommandButton, the script for the clicked event is executed; the same is true for a
PictureButton. Since both are in the same polymorphic family, there is no need to call different event
names for each of them. They can have the same event, but execute different script. This is polymorphism
in practice - the same event is called in both cases, but the script that is to be executed is passed like a
parameter to the executing function, depending on the control being clicked.

Function Overloading
Function overloading allows you to use the same function name to get different sets of results/behavior
from the script. PowerBuilder allows this flexibility by responding to the parameters that are sent with the
function call.

As an example of how function overloading works, let's look at PowerBuilder's MessageBox() function. This
function allows you to output to the screen, via a dialog box, a string, a numeric or a boolean value.
Usually, you would need to develop three nearly identical scripts to handle this functionality, but by using
function overloading you can achieve the same result with only little effort.

The compiler identifies the version of the function to use depending on one of the arguments that must be
passed to it, which in this case is the text parameter. This allows you to use a small function set, with
great range of functionality. The disadvantage is its run-time overhead. Each time the function is called,
the compiler has to find the appropriate function, depending on the arguments passed.

In PowerBuilder, function overloading can be done in two ways: inheritance or external functions. With
version 5.0, you can overload the function without using inheritance.

Function Overloading Using Inheritance


If you want to use inheritance to allow function overloading, you must complete two basic tasks:

 Define a base class that contains the basic definition of the function.
 Define another class by inheriting from the base class and alter the
function definition in the descendent class to reflect the required
overloading.
By performing these steps, you produce two similarly named functions, that share a relationship, and
which are organized into a predefined structure, that PowerBuilder understands and can use it when a call
to this function name is received. PowerBuilder looks for the appropriate function in the entire object
hierarchy, starting from the current level and working towards the top, until it finds the first appropriate
version of the function.

Function Overloading Without Using Inheritance


With version 5.0, you can define multiple functions with the same name and with different arguments, at
the same object level. This reduces the overhead, because, there is no need to use complex inheritance
hierarchy for one function overloading. Let us take the same example of MessageBox() function. You see
multiple MessageBox() functions definition at the same object level, i.e., SystemFunctions class. In prior
versions, Powersoft did the same thing, but didn't allow it for the developers. With version 5.0, developers
are allowed to do the same. Other examples would be TriggerEvent(), PostEvent() defined at PowerObject.

However, it is not allowed if you try to overload the function just because the return data type is different.

Function Overriding
You can define the same function at different levels of the inheritance hierarchy tree. For example, you
can define a save() at the base window, and can define the same function in the master and transaction
windows, inherited from the base window. Now, you can write different script in the save() in the master
window and the save() in the transaction window. This is because, PowerBuilder looks for the function
from the current object level to up; calling the function in the master window always executes the logic
written in the master window save function.

For example, suppose you have three objects A, B and C in a hierarchy, where A is at the top and B is
inherited from it. If you declared a function in A that prints a message to the printer, B inherits it. By
modifying this function, you can set up your overloaded function too, say, print out a message in Times
New Roman, rather than the default font.

If you now call this function from C, the function declared at B will be executed, causing the specified
message to appear at your printer in Times New Roman, rather than the default font defined at A.

PowerBuilder's Class Hierarchy


PowerBuilder is an object-oriented tool internally as well as externally. It provides the developer with base
objects, such as windows, user objects and DataWindows, for developing applications, each derived from
an internal PowerBuilder class. These classes make up PowerBuilder's internal class hierarchy.

Classes and objects can be confusing in PowerBuilder, as they refer most things as objects. This is
because, in most cases, PowerBuilder handles instantiating class into an object for you. As such,
when you create a window, it is created within a running version of PowerBuilder, which has already
instantiated the window object from an internal window class. Also, most people recognize the term object
more readily than class. In C++ or other language-based environments, it's much easier to conceptualize
the difference between an object and a class. A class is what you code and an object is what becomes of
that code when you run your program. In 4GL languages, this difference gets blurred as the environment
does most of this internally. However, classes are fundamental to the concept of object-oriented
programming and you should make sure that you're comfortable with the term.

In PowerBuilder, all classes are ultimately derived from the PowerBuilder super class, PowerObject. This
forms the top level of the class hierarchy, along with the SystemFunctions class. PowerObject is an
internal abstract class that encapsulates the standard functions available in any PowerBuilder object. As
an abstract class, it cannot be instantiated-you cannot create a PowerObject.

The PowerObject class has a number of descendant classes, from which the standard PowerBuilder objects
available to the developer are derived. Let's take a look at the PowerBuilder class hierarchy.
The PowerObject class has eight direct descendants, each further defining the base functionality for the
standard objects that are provided to the developer. These direct descendants are as follows. Classes that
are added in version 7.0 are marked 'v7.0' in the following picture.
Application: Application class is used to instantiate the application object, the starting point for all
PowerBuilder applications.

Function_object: Function_object class is used to instantiate a global function object. A global function
object is a special purpose object, that encapsulates a single method. Global functions are used by
developers to create globally accessible logic.

GraphicObject: GraphicObject class is the ancestor of all PowerBuilder's visual objects. Menu and window
classes are inherited from this class, as is the WindowObject class. The WindowObject class is the
ancestor of all visual object classes. It, in turn, has three descendants, each with further refined
attributes. PowerBuilder's class hierarchy breaks down visual objects into three categories: objects that
can be dragged, objects that cannot be dragged and a special class for MDI client objects.

GrAxis: This class provides the base functionality for PowerBuilder's Graph object. The GrAxis class has
three descendant classes: Category, Series and Values.

GrDispAttr: GrDispAttr class is the base class used to specify the appearance of text objects on a graph.
PowerBuilder provides descendant classes from the GrDispAttr class for graph titles, legends, pie graph
text and two descendant classes (DispAttr and LabelDispAttr) for the three axes (category, series and
value) in a graph.

NonVisualObject: This class is the base class for non-visual PowerBuilder objects, such as an Error object
and the Message object.
Structure: The Structure class is the base class for a special object that encapsulates a number of
properties, but doesn't have methods. In PowerBuilder, structure provides a mechanism to store related
data elements.

Each of the seven descendants is further refined as we move down the PowerBuilder's class hierarchy,
until a specific object type is made available to the developer. Then the developer creates instances of
them using PowerBuilder painters.

The Object Browser


PowerBuilder's Object browser is really a classic and it has two uses. It allows you to view the class
hierarchy in both PowerBuilder and your own application. It also allows you to view the properties, events
and functions that are available in any created object. The system tab is where you can view
PowerBuilder's internal class hierarchy. The other tabs allow you to view objects created in your
application.

Objects are displayed on the left panel and on the right panel, various properties, events, functions,
variables and structures associated with the selected object are displayed.

Clicking the right mouse button in the left panel expands each object. For example, expanding a window
object would display objects that were painted in that window. You can display object hierarchies
wherever inheritance is supported by PowerBuilder, i.e. in window, user object and menu tab folders.

The icons in the right panel indicate several things about the property, event, function, variable or
structure under scrutiny:

 An anti-clockwise arrow shows that the property / variable / event / function / structure is inherited.

 The number of squares shows whether the property / variable / event / function / structure is public
(three squares), protected (two squares) or private (one square).

 Other restrictions such as read-only are shown by half-colored squares.


The Document menu option in the popup menu allows you to display the information in rich text format,
which you can later print or copy to the clipboard, for pasting into Windows word processor. The Copy
menu option allows you to copy and paste properties, variables, functions, events and structures into your
code. The browser is also useful for reviewing OLE objects and controls. While you are in the OLE tab
folder, right panel is not available, instead all values are displayed in one panel.

The 'Uses' tab page displays all objects that are being used in each object in the current application
starting from the Application object.

Object browser window is a modeless window. That means, you can keep this window open and work in
your application at the same time.

Summary
In this session, you have learned object orientation concepts, terminology, inheritance, encapsulation and
polymorphism. You have also learned how PowerBuilder implements these technologies. In the next
session, you will learn about user objects.

Oop-pb.ppt—a PowerPoint presentation—presents you the full summary of this session

User Objects
We've looked into the theory and benefits of using object-oriented programming techniques. In this
session, we'll go into the details of one of the major object-oriented features of PowerBuilder - user
objects.

We'll look at the various types of user objects you can create, both visual and non-visual, and we'll see
exactly how these can be used to speed up the application development process.

In This Session You Will Learn:

 About various types of user objects.


 About implementing context sensitive help.
 To use external objects.
 Tips for programming with user objects.
Estimated Session Time

240+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/ Professional/ Enterprise


version) installed on your computer.
 You should complete all the exercises given till 'MDI Applications'.

Introduction
One of the main features of object oriented programming is reusability. In an application, you will
normally have a selection of controls or objects, that essentially perform the same task, scattered
throughout the design. Two good examples are the 'Print' CommandButtons, which allow users to receive
hard copies of what they see on screen, and the related printer CommandButtons, that allow the users to
specify printer parameters and so on.

If you have number of windows, you might want to allow the user to print from each one of them, a
feature that would usually mean rewriting the same piece of code many times. User objects provide a
solution, by allowing you to reuse one object or a single piece of code many times throughout the
application.

User objects allow you to customize PowerBuilder's standard controls, to make use of third party controls,
and moreover to create your own. You can reuse these objects and simplify standardization of the
functionality in your applications. For example, you could use the same 'Print' CommandButton User
object throughout the application and never worry about, or spend time for checking the functionality
being the same or not, in each instance.

User objects can be broadly divided into two categories:

 Visual
 Class (non-visual)
Let's take a look at each of these categories.

Visual User Objects


A visual user object is a control or a set of controls, with certain functionality. Our 'Print' CommandButton
is an example of a visual user object. There are four types of visual user objects:

 Standard
 Custom
 External

Class (Non-Visual User Objects)


This type of user object doesn't have a visual component—it only has functionality. For example, you
could write a function DisplayErrorMessage(), and then call this function whenever you need it in your
application.

Non-Visual user objects allow you to write business rules and perform other processing, which can be later
on reused as many times as required. There are three types of non-visual user objects: Till version 4.0,
they are called non-visual user objects and from v4.0 onwards this category is called Class.

 Standard
 Custom

Creating a Standard User Object


To create a user object, click File > New menu option.

Note that the C++ option will only be available if you have the Enterprise version of PowerBuilder or if the
'Advanced Developer's Kit' is installed.

In this dialog box, you need to specify which type of user object you want to create. Let's see how we
created our example from Session 12, which displayed various quotes when different CommandButtons
were clicked.

Double click on the 'Standard Visual' icon located under Objects tab page, and select CommandButton
from the list of standard visual objects.

A CommandButton should be ready and waiting for you. The user object development environment is
similar to that found in the window painter.

If you remember back to our discussion of inheritance, we have just created our own class, crossing the
boundary from PowerBuilder to user defined classes. After we add our functionality to this
CommandButton, we will be able to create instances of it.

Creating a CommandButton User Object


Events available to the standard CommandButton control are available to this user object. In order to
customize our user object, PowerBuilder offers us the chance to declare variables, structures, functions
and our own user events.

To complete our CommandButton user object, we need to add the functionality that displays
MircroHelp when the mouse moves over the CommandButton. Let's declare an user defined
event, ue_mousemove. Display the popup menu in the Script painter and select 'Go To >
Events' menu option and select '(New Event)' from the DDLB, Alternately, you can select 'Event
List' tab page and display popup menu and select Add menu option. Declare ue_mousemove
event and map it to pbm_mousemove event id. Type the following script for the
ue_mousemove event:

// Object: uo_CommandButton user object


// Event: ue_mousemove
w_mdi_frame.SetMicroHelp( This.Tag )

While creating a user object, try to avoid hard coding anything into the script. By hard coding in the script,
you are reducing the flexibility of the user object, the cornerstone of their usefulness. In this case, we've
used the THIS keyword, so that the displayed message depends on the underlying object.

Save the user object as uo_commandbutton and create a new window. Now, you may want to test this
user object. Open the w_about window. Select 'Insert > Control > User object' from the menu. Select
uo_CommandButton. Click on the window workspace next to the Done CommandButton. We have script
for the Done CommandButton's clicked event. Copy that script to the new CommandButton's clicked
event, and delete the Done CommandButton. Change the new CommandButton's name to cb_done and
the text to "&Done". Similarly, replace all other CommandButtons. Define the help for each
CommandButton in its TAG property; this can be done from the Properties sheet for each
CommandButton.

Confused about what's happening? What's happening is that, we replaced all the standard PowerBuilder
CommandButtons in w_about window with the user object of type CommandButton. That means, we are
using our own CommandButton instead of PowerBuilder's standard CommandButton.

Now, save the window and run the application. Select Help > About menu option. Move the mouse over
these CommandButtons and see if PowerBuilder displays help on the Statusbar.

The code written for the user object class is available to each user object instance placed in the window
and changes to the user object class will be reflected in each instance of the object. This provides you with
a code that is reusable, easier to maintain and takes less time to develop.

Let's now create a more complex standard user object that we can be used in our Product Management
application.

Creating a DataWindow User Object


By default, you can't select multiple rows, either continuously or randomly, in a DataWindow; you are
restricted to one row at a time. This can be restrictive, so let's create a DataWindow user object with this
functionality.

Note that this kind of functionality is already supported by programs such as Window Explorer. It has been
commonly accepted that the users need to use the Ctrl and Shift keys in conjunction with a mouse click to
select random or continuous items respectively, and so it would make sense to use this standard.

Invoke the user object painter and this time create a Standard DataWindow object.

The logic for this user object is quite simple. We store the selected row in an instance variable and then if
the user uses the SHIFT key while selecting a new row, we select all the rows in a loop between the
previously selected row and the current row. If they use the CTRL key, we simply add the current row to
the list of selected rows.

We could write the entire code in the object's clicked event, but splitting the logic between two events,
one standard and one custom, would be better. Therefore, let's define a user-defined event called
ue_shiftclicked. DO NOT assign any event id to this event. Define a parameter paramrow, datatype long
passed by value.

Next, we have to declare an instance variable to hold the selected rows.

Long il_DWSelectedRow

Now write the following code in the Clicked event for the user object:

// Object: uo_DataWindow control user object


// Event: Clicked
If Row > 0 Then
If KeyDown(KeyControl!) Then
This.selectrow(Row,true)
Else
This.selectrow(0,false)
This.selectrow(Row,true)
End If
If KeyDown(KeyShift!) Then
This.Event Trigger ue_ShiftClicked( Row )
Return
End If
End If
il_DWSelectedRow = Row
this.SetRow(Row)

The KeyDown()function is used to establish the key pressed by the user. If the user clicks the left mouse
button without using any other key combination, we select the clicked row, deselecting others. If the Ctrl
key combination is used, we add the highlighted row to our current selection and if the Shift key
combination is used we trigger the user event.

The parameter for the KeyDown() is an enumerated data type. PowerBuilder's on-line help contains the
list of all the keys that can be used as parameters for the KeyDown() function.

If you now look at the list of events that are possible for the user object, you'll find that our
new user event ue_ShiftClicked is available. We need to write the following code into this
event:

// Object: uo_DataWindow Control user object


// Event: ue_ShiftClicked
Long ll_OldRow, ll_StartRow, ll_EndRow, ll_RowCounter
ll_OldRow = il_DWSelectedRow
If ll_OldRow > ParamRow Then
ll_StartRow = ParamRow
ll_EndRow = ll_OldRow
Else
ll_StartRow = ll_OldRow
ll_EndRow = ParamRow
End If
For ll_RowCounter = ll_StartRow to ll_EndRow Step 1
this.SelectRow( ll_RowCounter, true )
Next
this.SetRow( ParamRow )
this.SetColumn( 1 )
Return 0

You should be aware that the currently selected row might be either greater or lesser than the previously
clicked row. For example, if you click on row 10 and click on row 16 with the Shift combination, the
ll_StartRow will be10 and ll_EndRow will be16. In another case if you click on another row with the Shift
combination that is less than 10, say 5, then in that case, ll_StartRow will be 5 and ll_EndRow will be 10.
These endpoints are organized into sensible start and final variables by the if statement.

The FOR loop is used by the SelectRow() function to select all the rows from ll_StartRow to ll_EndRow.
The second argument to the SelectRow() decides whether to select (true) or deselect (false) the specified
row. After this, we store the currently selected row in the il_DWSelectedRow variable.

 Save this user object as uo_DataWindow, and to use it:


 Open up the w_product_master window.
 Add the user object control uo_DataWindow.
 Link d_products_maint DataWindow object to the new DataWindow
control.
 Copy script from dw_product to the corresponding events in the new
DataWindow control.
 Delete the dw_product DataWindow control.
 Rename the new DataWindow control as dw_product.
We've effectively replaced dw_product with our user object of type DataWindow. If you run the window as
it is, you be able to see the effect of our new functionality.

As you can see, we can now select multiple rows on the DataWindow.

Context Sensitive Help For DataWindow Controls


Like any other control in a window, you can specify TAG value for the DataWindow control and then use
them to display context sensitive help. However this is restricted to DataWindow as a whole. You
encounter problems, if you want to display different textual help messages for each field within the
DataWindow control.

To solve this problem, we need to find out the column in which the user is currently focused and then
supply an appropriate message. For the first part of the problem, we can make use of the
ItemFocusChanged event. When the user presses the Tab key or clicks on another column,
ItemChanged, ItemError or ItemFocusChanged events will be triggered depending on whether or not the
data has been changed, and if it has changed whether the new entry passes any predefined validation
rules.

By using GetColumnName() function in the ItemChanged event, we can get the current column and by
using the same command in the ItemFocusChanged event, we can get the next column name in the tab
order.

For example, if the user presses TAB key when the focus is on product_no in the w_product_master
window, the GetColumnName() function in the ItemChanged event would return product_no, while the
same function in the ItemFocusChanged event would return product_description.

The following code gets the column name, and sets the MircroHelp, by obtaining the tag values
using the Describe() function. Put this script in the ItemFocusChanged event for the
DataWindow Control user object uo_DataWindow that we have just created.

// Object: uo_DataWindow Control user object


// Event: ItemFocusChanged
string ls_ColumnName, ls_TagValue, ls_Argument
ls_ColumnName = GetColumnName()
ls_Argument = trim( ls_ColumnName ) + ".Tag"
ls_TagValue = Describe( ls_Argument )
If ls_TagValue = "" OR &
isnull( ls_TagValue ) OR &
trim( ls_TagValue ) = "?" OR &
trim( ls_TagValue ) = "!" Then
ls_TagValue = "Ready"
End If
w_mdi_frame.SetMicroHelp( ls_TagValue )

The Describe() function returns an exclamation mark (!), when the argument is bad and returns a
question mark (?), when the specified attribute has no value. We can use these returns in a test, setting
ls_TagValue to 'Ready', the default value that appears on the status bar if anything goes wrong.

If you run the application, open the w_product_master by selecting 'Module > Product Master Maint'.
option. Retrieve the data by selecting File > Retrieve from the menu. Now, keep on pressing the TAB key
and see the help on the status bar. If you see Ready for a column, it means that you didn't define a tag
value for that column in the DataWindow. To define the TAG value, open w_product_master and click with
the right mouse button on dw_product DataWindow. Select 'Modify DataWindow' menu option. Now you
are in the DataWindow design mode. Select one column at a time and go to the properties for each
column and define the TAG value.

You can now use uo_DataWindow, instead of the standard DataWindow control where ever you need multi
row selection and MicroHelp display functionality, without writing a single line of new code.

Custom User Objects


A custom user object is created by grouping more than one control. For example, in the first version of
our w_product_master window, we had a series of CommandButtons and a DataWindow control, but by
using a custom user object, we could group all of them together. Once we have an object like that, we can
use it anywhere in the application.

While painting custom user objects, you can insert another user object, and in fact other custom user
objects also. This allows us to create any type of user object that we can think of, using objects offered by
PowerBuilder, or any of the object that has been customized or inherited from the PowerBuilder parents.

In this session, let us create a custom user object using the CommandButton user object
uo_CommandButton and user object uo_DataWindow. The reason we are using the previously created
user objects is that we added some of the functionality that we need.
In the above object, we can display items on the left hand side DataWindow and allow the user to select
and put the selected items in the basket (right hand side DataWindow).

It is based on a simple idea. The user highlights the entries that he is interested in and passes them over
to the right hand side of the window. The user can also remove previous selections or pass all of the
options from one side to the other at a click of a button. This is very useful because it allows you to
include functionality that act upon the entries that appear in the list.

These types of windows are useful for multi-stage selections such as, when defining fields for a query or
the sort order the query is supposed to use. You can also use this functionality to add addresses while
creating the mail and so on.

Painting the Custom User Object


We are going to paint a custom user object uo_shuffle_rows_between_2_dws. Invoke the user object
Painter and double click on 'Custom Visual' icon.

Insert a user object by selecting 'Insert > Control > User object' menu option and selecting the user
object uo_DataWindow from the list and click on the workspace. Name this as dw_left. Follow the same
steps and insert one more, and name it as dw_right. Make sure that you place it on the right side of
dw_left.

Similarly insert the user object uo_CommandButton and place it between two DataWindow controls. Name
this as cb_transfer_to_right. Insert one more and place below the first one and name it
cb_transfer_to_left. Place one more below the dw_left DataWindow and name it as cb_select_all. Place
one more below the dw_right DataWindow and name it as cb_deselect_all.

You can see that the ue_shiftclicked event is the ancestor script for the DataWindow control. This is object
oriented design working in practice; when you placed uo_DataWindow control in the window, you are
creating a new control which is inherited from the uo_DataWindow, that means the new DataWindow
controls (dw_left and dw_right) gained all the functionality of the previously defined user object
(uo_DataWindow). That means that we don't have to write the same code over and over again.

Creating Functions For the Custom User Object


A function is basically a collection of PowerScript statements, which perform some processing. If you have
series of statements that are expected to be used several times in the application then, it makes sense to
put them in a function and call the function every time you want to execute those statements.

Functions consist of two distinct components: a function declaration and the actual code. A function
declaration defines arguments the function is going to use as variables in the code. The code manipulates
the arguments to achieve the required functionality.

We use several functions in this example:

 f_copyrecord: This is used to copy records between controls.


 uf_initialize: This is used to initialize the left and right DataWindow
Controls and the headings.
We'll look briefly into each of these functions.

Global Function f_copyrecord

As with all functions, this function is defined in the Function Painter. Invoke the Function
painter (Select File > New menu option and double click on Function icon located under
Objects tab page). Declare the function, as shown in below and set the return type to
Integer.

Name Type Pass By

Adw_destination DataWindow Ref

Adw_source DataWindow Ref

Al_row Long Read-Only

Write the following code:

// Global Function: f_CopyRecord()


String ls_ColType
int li_ColCount, li_ColCounter
Long ll_CurrentRow
ll_CurrentRow = adw_destination.InsertRow(0)
li_ColCount = integer( adw_source.Describe( &
"datawindow.column.count"))
For li_ColCounter = 1 To li_ColCount
ls_ColType = adw_source.Describe("#" + &
String( li_ColCounter ) + ".ColType")
Choose case upper( left( ls_ColType,5))
Case "CHAR("
adw_destination.SetItem( &
ll_CurrentRow, li_ColCounter, &
adw_source.GetItemString( &
al_row, li_ColCounter ))
Case "NUMBE","DECIM"
adw_destination.SetItem( &
ll_CurrentRow, li_ColCounter, &
adw_source.GetItemNumber( &
al_row, li_ColCounter ))
Case "DATE"
adw_destination.SetItem( &
ll_CurrentRow, li_ColCounter, &
adw_source.GetItemDate( &
al_row, li_ColCounter ))
Case "DATET"
adw_destination.SetItem( &
ll_CurrentRow, li_ColCounter, &
adw_source.GetItemDateTime( &
al_row,li_ColCounter ))
Case "TIME"
adw_destination.SetItem( &
ll_CurrentRow, li_ColCounter, &
adw_source.GetItemTime( &
al_row, li_ColCounter ))
End Choose
Next
Return ll_CurrentRow

You can replace the above function with RowsCopy()/RowsMove() DataWindow function, available from
version 4.0 onwards. We still wrote this code, to expose you to more DataWindow related programming.

This function inserts the row made up from all the columns within the source DataWindow into the
destination DataWindow. The arguments for the Describe() function return the total number of columns to
the source DataWindow, while the FOR loop determines the data type of the columns. Finally the CASE
statement simply calls the appropriate function, depending on the data type, to get the column value and
then copies it to the destination DataWindow.

We use the first five characters of the data type to decide which case statement to execute, as it is the
minimum number required to give unique values. If we use less than five, we wouldn't be able to
distinguish between DATE and DATETIME.

While writing the code, if you want to change the function declaration, display the prototype and change it
(To display the prototype, click on the second icon from right located on the right top corner of the Script
Editor view. If the view is pinned, you can find this icon right below the Maximize icon).

We declare the access level as Public so that the function is available to all objects in the application. We
don't need to return anything from this function, but we do need to pass three parameters to it, each by
value.

You can send any number of parameters to a function, but you have to specify how they are passed;
either by value or by reference or by read-only. This completes the definition of f_copyrecord function.

User Object Level Function uf_initialize

This is declared at the user object level. Go to the Script painter, display popup menu and
select 'Go To > Functions' menu option. Declare the function as shown below and set the
return value to none.

Name Type Pass By


As_heading_left String Read-Only

As_heading_right String Read-Only

As_dwo string Read-Only

Atr_tran1 Transaction Ref

The difference between f_copyrecord and uf_initialize function is that, the former one is Global (available
in all objects in the application) and has Public access level. You can't define access level to a global
function, it is always Public. On the other hand, you have the freedom of declaring the access level to
functions defined at the (User) object level.

We declared this function public because, we want all objects to use this function. This function
allows initialization of the user object, which makes the user object more re-usable.

// Object: uo_shuffle_rows_between_2_dws
// Function: uf_initialize()
st_left_heading.text = As_heading_left
st_right_heading.text = As_heading_right
dw_left.dataobject = As_dwo
dw_right.dataobject = As_dwo
dw_left.SetTransObject( Atr_tran1 )
dw_right.SetTransObject( Atr_tran1 )
dw_left.retrieve()

Since this is a reusable object, we neither specified any text for the StaticText controls nor assigned
DataWindow objects to dw_left and dw_right DataWindow controls. To set these controls, we write an
initialization function. The function passes the transaction object as a parameter, which makes it flexible,
if you are using more than one transaction in an application.

Code For The DataWindow Control


Write the following code for the Clicked event of the dw_left control:

// Object: DW_left in uo_shuffle_rows_between_2_dws


// Event: Clicked
If Row > 0 Then
cb_transfer_to_right.Enabled = TRUE
Else
cb_transfer_to_right.Enabled = FALSE
End If

This simply checks if any rows are selected in the control and enables or disables the cb_transfer_to_right
CommandButton accordingly. The logic for the other DataWindow control is exactly the same.

The script for both the DataWindows are identical, so, we can implement the script in a function and
distinguish between them by passing relevant parameters and then call it from both the DataWindows.

Code For the CommandButton


Write the following code for the cb_transfer_to_right CommandButton`s clicked event:
// Object: cb_transfer in uo_shuffle_rows_between_2_dws
// Event: Clicked
/* Copies all the selected rows to the right side
DataWindow and deletes each row when copied. Later sorts
both the DataWindows on the first column */
Long ll_SelectedRow = 0
string ls_ColName
ll_SelectedRow = dw_left.GetSelectedRow( ll_SelectedRow )
If ll_SelectedRow = 0 Then Return
Do While ll_SelectedRow <> 0
f_CopyRecord( dw_right, dw_left, ll_SelectedRow )
dw_left.DeleteRow( ll_SelectedRow )
ll_SelectedRow = ll_SelectedRow - 1
ll_SelectedRow = dw_left.GetSelectedRow(ll_SelectedRow )
Loop
ls_ColName = dw_left.Describe("#1.Name")
dw_left.setsort( ls_ColName + " A" )
dw_left.Sort()
ls_ColName = dw_right.Describe("#1.Name")
dw_right.setsort( ls_ColName + " A" )
dw_right.Sort()
If dw_left.rowcount() = 0 Then
cb_select_all.enabled = False
cb_transfer_to_right.enabled = False
Else
cb_select_all.enabled = True
cb_transfer_to_right.enabled = True
End If
If dw_right.rowcount() = 0 Then
cb_deselect_all.enabled = False
cb_transfer_to_left.enabled = False
Else
cb_deselect_all.enabled = True
cb_transfer_to_left.enabled = True
End If

In our design, we have allowed the user to select more than one row using the CTRL and SHIFT keys, so
we have to take care to transfer all the selected rows. GetSelectedRow() gives the next highlighted row
after the argument row number and once we know the selected row, we call the f_copy_record function to
copy the specified row from the source DataWindow to the destination DataWindow.

Once the row has been copied over, we delete it from the source DataWindow and sort the destination
DataWindow on the first column of the DataWindow. The arguments passed to the Describe() function
returns the name of the first column in the DataWindow and we use the SetSort() function to specify the
sort criteria, before performing the actual sort, using the Sort()function.

Note that, if you haven't specified a sort criteria when using SetSort() and moreover if you haven't set it
before, PowerBuilder will prompt for a sort criteria.

Copy the script to the cb_transfer_to_left CommandButtons's clicked event. After copying,
swap the words dw_left and dw_right in the code.

// Object: cb_select_all
// Event: Clicked
dw_left.SelectRow( 0, True )
// Object: cb_select_all
// Event: Clicked
dw_right.SelectRow( 0, True )

To test the custom user object, paint a window and place it in the new window. In the new
window's open event, write the following code:

// Object: w_shuffle_test
// Event: Open
uo_1.uf_initialize("Available Products", &
"Selected Products ", "d_test", SQLCA )

We assume that you painted a test DataWindow with tabular presentation style and named it
d_test. You can take the following data source for the d_test DataWindow.

SELECT product_description FROM product_master

Save the window as w_shuffle_test. Open this window from the Application object's Open
event. Append the following line of code to the Open event code:

Open( w_shuffle_test )

Once you complete the testing, make sure to remove the above code and restore the old code.

Now, you can use this custom user object in any window you want, with just a single line of code. This has
been a fairly complex example, but you can see that we've made most out of PowerBuilder's object
oriented features, and reduced the amount of code we have to write. We've also tried to make the
example as generic as possible, so that, you could easily manipulate them for your own applications. You
only have to change the database to the one to which you log on and change the DataWindow assignment
statements to match the new database.

External User Objects


In this section, we explain you the theory behind the external user objects. With version 7.0, no example
DLLs are being shipped.

External user objects are created from underlying Windows DLLs. Lot of third party companies market
windows controls with lot of functionality (typically they program in C/C++). You can use those objects
(available in DLL) in PowerBuilder, by defining it as an external user object. For example, in PowerBuilder
there is no control to display the progress of a process. The CPALETTE.DLL (in version 4.0) in the
EXAMPLES directory has a control which allows you to solve the problem.

It ultimately means that, these types of user objects let you use third party controls. When using controls
derived from DLLs, you should be able to find out the events declared, how they respond and the
limitations of the control in general. The events declared for the control are not displayed in the user
object Painter, so you will have to go through the documentation provided with the DLL provider.

In the user object painter, you can still declare user events, functions, structures and variables, thus
allowing you to access the control written in another language and to extend the functionality of the user
object, even if you aren't familiar with that language.

Standard Class
Non-visual user objects are like any other object that you define in PowerBuilder, except that they have
no visual or GUI component. They act just like classes in C++. These objects can have variables, events
and functions associated with them and they allow you to implement encapsulation.

You need to declare variables of these classes and instantiate them. Do you remember the way
instantiated a popup menu? In the same way, we can instantiate these classes. For ex:
ClassYouCreated lClassVariable
lClassVariable = CREATE ClassYouCreated

PowerBuilder automatically instantiates these classes for you, if you turn on the AutoInstantiate property.
You can turn on this option from the popup menu, in "Class" user object painter. If you do so, you do not
need to call the second line of code (shown above), i.e., the CREATE statement.

PowerBuilder comes with some standard classes, such as Error, Message, SQLCA and so on. You can
create Class user object, by inheriting from one of these standard classes. Let's create a class
uo_transaction, which inherits from the Transaction class.

Did you know that we don't have certain functionality in SQLCA? For example, there is no method defined
to display the error message, and moreover there is no method defined to log when you login and logout
of the database. May be we can create our own transaction class, by inheriting from the standard
transaction class and adding the frequently used functionality and use this class instead of SQLCA.

Select File > New menu option and double click on the 'Standard Class' icon located under
object tab page. Select Transaction from the ListBox. You will be presented with the empty
work space. Remember, you can't paint any control in the workspace. Let's define a method
(function) to display the error message.

// Function Name: uf_display_error


// Return Value: Integer
// Access: Public
// Parameters: string pm_message by value,
// string pm_button_type by value
Button lButton
Integer l_UserAnswer
If Upper(Pm_ButtonType) = "RETRYCANCEL" Then
lButton = RetryCancel!
Else
lButton = OK!
End If
If IsNull( Pm_Message ) Then Pm_Message = ""
l_UserAnswer = MessageBox( "Database Error", &
"Error # " + String( This.SQLCode ) + "~n" + &
"Error Message: " + This.SQLErrText + "~n" + &
"Database Error # " + String( This.SQLDbCode ) &
+ "~n" + "Database Error Message: " + &
This.SQLReturnData + "~n" + &
Pm_Message, Exclamation!,lButton,1 )
Return l_UserAnswer

This function displays an error message along with the user specified message. You can ask the function
to display Retry, Cancel buttons at the bottom of the message box.

Did we really implement polymorphism in the application? No. Let's implement it now. As you know, the
above function takes two parameters. Let's define one more function with the same name, but WITH NO
PARAMETERS. Let the compiler call the appropriate function depending on the parameter.

Define the other function uf_display_error with public access and integer as return type.
Declare no parameters. Type the following code:

// Function Name: uf_display_error


// Return Value: Integer
// Access: Public
// Parameters: None
MessageBox( "Database Error", &
"Error # " + String( This.SQLCode ) + "~n" + &
"Error Message: " + This.SQLErrText + "~n" + &
"Database Error # " + String( This.SQLDbCode ) &
+ "~n" + "Database Error Message: " + &
This.SQLReturnData, Exclamation!, OK! )
Return 1

Save this class as uo_transaction and close the user object painter. Close all the open painters.

Do you know what we just did? We have created a class by inheriting from the Transaction class. Do you
know what SQLCA is? SQLCA is a global variable of type transaction. The term 'type' means that, it is an
instance of the specified class and can't be changed, i.e., you can't define new attributes or methods at
that class, however you can change values of existing attributes. There is a difference between inheriting
a class and declaring a type variable. When you inherit a class, you can define new attributes, methods
and isntantiate that class. When you declare it as a type variable, you are instantiating a class as it is,
without changing (adding new functions, events and attributes) it.

If you want to use this new class uo_transaction instead of SQLCA, you need to find and replace all
occurrences of SQLCA with this new name. Right? NOooope.

As explained earlier, SQLCA is a type variable of 'transaction'. Now, you created a class uo_transaction
which is inherited from Transaction. If we declare SQLCA as an uo_transaction type variable, instead of
Transaction, we need not change the code. Then the question is, how can we declare SQLCA as a
uo_transaction type variable instead of Transaction?

 Display Application Object Painter.


 Select 'Additional Properties' button from the Properties sheet.
 Click on the Variable types tab page. Change Transaction to
uo_transaction for the SQLCA prompt.
 Click OK CommandButton.
 Save and Close the Application Painter.
That's all, we are done. Now, let's make use of the functions we defined at uo_transaction class.

Open the w_login window. Go to the Clicked event for the OK CommandButton. Delete the code
from the line that starts with If SQLCA.SQLCode (including that line) till the end and replace it
with the following code.

// Check for errors


If SQLCA.sqlCode <> 0 Then
If SQLCA.uf_display_error( &
"Do you want to try again ? ", &
"RETRYCANCEL" ) = 2 Then
Halt
Else
Return 0
End If
Else
Close( Parent )
End If
In this code, we are calling the function we defined at the user object level, instead of calling
MessageBox() function. Save the changes and test it. How to test it? Just supply a wrong password and
click the OK CommandButton. You will see the error message, as shown in the following picture.

If you have successfully got into the application, instead of getting this error message, there must be
something wrong somewhere, and where would that be?

The answer is simple, but, takes a while to explain. Invoke the database painter and select 'Window >
Database Profile' menu option. Select Product from the list and click on Edit CommandButton. Remove
values from the 'User ID' and 'Password' prompts located under the Connection tab page and uncheck
both those options and click OK button.

When PowerBuilder reads this information, it reads the product profile (stored in .INI files and/OR in the
Window's registry) and uses those values to connect to the database, since we are specifying the profile
name in the DBParm parameter. That means, values specified in the DBParm takes precedence over other
parameters (if you specify both). The conclusion is that, even if you provide wrong information in the
w_login window, PowerBuilder sign you into the database successfully because, the correct password is
stored in the database profile.

You will learn more on this topic, in the "Application Deployment" session. For now, let's concentrate on
how to make PowerBuilder fail from logging, when a wrong password is supplied.

Let's remove the password from the profile file. We can do the editing of .INI files and windows registry
right from PowerBuilder itself. We can't edit this profile now, because, we are using it to connect to the
product database. To edit this, we need to connect to some other database temporarily.

 Display popup menu on 'EAS Demo DB V3' located under the ODBC
folder and select Connect menu option. Now, you are connected to the
demo database.
 Display popup menu on 'product' located under the ODBC folder and
select Disconnect menu option. Now, you are disconnected to the
product database.
 Double click on the 'ODBC Administrator' option located under ODBC
> Utilities folder.
 Select Product option located under 'User DSN' tab page and click on
Configure button.
 Remove values from the 'User ID' and 'Password' prompts located under
Login tab page and click OK button. Click OK button again. Close the
Database painter.
Now, run the application and test the class user object functionality. After that, don't forget to connect to
the product database, from the database painter, otherwise, PowerBuilder connects to the demo database,
every time you open the Database painter.

Custom Class
Typically, they are used to encapsulate business rules and extra functionality. You can add attributes
(instance variables) and declare events and functions (methods). Whenever you want to manipulate the
attributes, simply call the methods.

AutoInstantiate property is available only to the Custom Class user objects. It is not available to the
standard Class user objects.

You should declare instance variables as, either protected or private, otherwise any object can set their
values, without going through the methods declared at the object.

For example, you could create an external function user object, to access the 16-bit Windows API calls.
Methods (functions) are then defined to get information from Windows by using these external functions.

Notes on User Object Programming


This section contains useful hints and tips, for using the created user objects.

User Objects On Windows


You can use user objects in a window in two ways:

 By placing the user object in a window.


 By opening the user object dynamically from script.
When you place an user object in the window, you must give it a name just as you would for any other
type of controls. You can then refer to the user object in your script by using this name.

Resizing User Objects


When the user object is resized, the controls within don't resize accordingly. To allow the user to resize
the user object at execution time, you need to write the code for doing it.

Time Related Existence


There may be situations where you only need the user object to exist in the window for a certain period of
time. You can do this in one of two ways:

You can place the user object in the window and toggle its visible property, so that it gets displayed only
when you need it. Here, the memory used is same in both the cases.

You can open the user object dynamically whenever required and close it down when it is no longer
needed. The function to open the user object is OpenUserObject(). To send parameters to the user object,
you can use OpenUserObjectWithParm(). In both the functions, you can specify the x and y co-ordinates
of the user object in the window.

Placing Non-Visual User Objects


Till version 7.0, you can't place a class user object (Non-Visual user object) on the surface of a
window. What you can do is, declare a variable and create an instance of the class and access
that instance. For example, if you have a custom class cc_1, and want to call uf_close(), the
typical code would be:

cc_1 l_nvo_1
l_nvo_1 = CREATE cc_1
l_nvo_1.uf_Close()

Whenever you are finished with the class, you can destroy it:

DESTROY l_nvo_1

However, with version 7.0, you can place them like any other visual object. To write a script for those
classes that are placed on the window, select 'View > Non-Visual Objects List' menu option and select the
class, display the popup menu and select Script menu option.

PARENT and THIS Pronouns


If you create a standard user object, you can use the reserved words Parent and This, similar to a
standard control. For example, if you write Close(Parent) for the clicked event of a CommandButton type
user object, clicking on it would close the window. This will happen even if you open the user object
dynamically with OpenUserObject() or OpenUserObjectWithParm() function.

User Object Events


In custom and C++ class user objects, only Constructor and Destructor events are available. In
Custom/external visual user objects, Drag & Drop events are also available.

Summary
We've learned quite a bit of theory in this session and seen several examples of how to use user objects in
PowerBuilder applications. The object-oriented approach provides a higher level of abstraction than the
traditional procedures and data.

We hope that we've illustrated, through the use of user objects, how powerful the object-oriented features
of PowerBuilder can be. If you put some thought into the application development process before you
begin, you can save yourself a lot of time in the later stages of a project. We didn't explain about C++
user object here because, it itself is a big topic. So, it has a special session, following this one. For a quick
summary of this session, browse user-objects.ppt (a PowerPoint presentation).

Exercises
Write a function to log 'login time' and 'log out time' for each user, in a database table and implement it.
We don't have a table to store the login and logout information. You need to create that table also. So,
design and create the table.

Tip: You can create two functions to log the above specified details, one to log login info and another
to log logout info. You can create these functions at uo_transaction user object.
Solution: ex-13-1.zip

Create a class user object, inheriting from the Error class. Implement a function to save the error
information to a specified error file. Use this in the application, instead of Error object. Use this newly
created function from "Save to Log".

Tip: You can copy code written to 'Save to Log' CommandButton in the w_error window and modify it
and put in the written function.

Solution: ex-13-2.zip

This exercise would be interesting. Recall that we placed five SingleLineEdit controls in the w_login
window. For each control, we wrote script in the GotFocus and LoseFocus events, to display help on the
st_help StaticText control (recall that st_help is located in the w_login window). What we want you to do
is, create an user object uo_sle of type SingleLineEdit control and write script in those events, to display
help on the st_help control. Once you create uo_sle, replace all SingleLineEdit controls in the w_login
window, with uo_sle. The challenging part of this exercise is that, you can't directly refer to st_help from
the user object uo_sle, because, st_help is not part of uo_sle, it is part of w_login window.

Tip: You can create an instance variable of type StaticText control in uo_sle and assign st_help to
that instance variable from outside, may be from w_login window's open event.

Solution: ex-13-3.zip

Embedded & Dynamic SQL


Till now, you have learned how to use the SELECT statement in a DataWindow. You also came to know
that, when you issue an Update() function, PowerBuilder automatically generates INSERT, DELETE and
UPDATE statements, depending on the row status, effectively saving the information back to the database.

We now extend our coverage of SQL to two particular subsets: embedded SQL and dynamic SQL. We'll
also take a look at dynamic DataWindows, cursors and stored procedures.

In This Session You Will Learn About:

 Embedded SQL.
 Using cursors in PowerScript.
 Using stored procedures in PowerScript.
 Using dynamic SQL in PowerScript.
Estimated Session Time

240+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/Professional/Enterprise


version) installed on your computer.
Introduction
The Structured Query Language is the most popular and commonly known database query language, so, it
makes sense to use SQL to query a database.

Providing support for Embedded SQL ensures that, a language has the means to query most database
engines for information. When you use this particular type of SQL, the statements are directly embedded
into the code, introduced by keywords specific to the language being used.

These statements are put through pre-compilation which translate them into the equivalent
functions or statements in the host language. For example, you can use embedded SQL in a C
program as follows:

EXEC SQL INCLUDE SQLCA ;


EXEC SQL BEGIN DECLARE SECTION ;
DBCHAR mDescription[32] ;
DBINT mItemNumber ;
EXEC SQL END DECLARE SECTION ;
EXEC SQL SELECT product_description
INTO :mDescription
FROM product_master
WHERE product_no = :mItemNumber ;
...
printf( "Product No: %d, Product_description: %s",
mItemNumber, mDescription) ;

EXEC SQL is the keyword used by C to identify SQL statements. Otherwise, the SQL statements are
similar to the ones found in a database administrator's handbook.

Writing embedded SQL is very easy in PowerBuilder as it automatically detects SQL keywords, making
special keywords redundant. The only thing you need to do is terminate the SQL statements with a
semicolon (;).

SQL statements can span multiple lines, without the need for a line continuation character.

Embedded SQL - PowerBuilder Implementation


SQL and in fact, the whole theory of relational databases, is based on sets. The subject of database theory
is a book by itself, so we can't teach you everything here, but we can do is look at some of PowerBuilder's
implementations.

The simplest way of using SQL in the PowerScript code is to use the Paste SQL option in the Script
Painterbar. When you click on this icon  , PowerBuilder presents you with the following options:
This allows you to select the type of SQL statement you want to insert, after which PowerBuilder launches
the usual query selection dialog boxes, designed to speed up the creation of the statement.

For your reference, PowerBuilder allows you to select the tables to be displayed in the SQL statement.
Before we take a look at the various SQL structures that we can use, we must take a look at the concept
of Host Variables.

Using Host Variables


SQL statements in PowerBuilder can make use of the existing variables by prefixing the
variable name with a colon (:). For example, in the following code we make reference to the
li_ProductNo variable in the SQL statement:

Int li_ProductNo
li_ProductNo = Integer( sle_itemno.Text )
SELECT "product_master"."item_product_description"
INTO :lProductDesc
FROM "product_master"
WHERE "product_master"."product_no" = : li_ProductNo ;

Host variables are most commonly used in the following areas:

 WHERE clause.
 HAVING clause.
 INTO variables.
 VALUES clause in INSERT statements.
They are also used in place of values in the SET clauses in UPDATE statements and as parameter values to
the Stored Procedures.

SELECT INTO
When the SQL statement returns a single row, you can use the SELECT INTO statement, to pass
the results into the host variables. For example:

String lProductDesc
Int li_ProductNo
li_ProductNo = Integer( sle_itemno.Text )
SELECT "product_master"."item_product_description"
INTO :lProductDesc
FROM "product_master"
WHERE "product_master"."product_no" = :li_ProductNo ;
If SQLCA.SQLCODE <> 0 then
// ....
End if

This stores the result of the SELECT statement in the host variable lProductDesc, earlier
initialized as a string. SQLCA contains the status of the last executed SQL statement, so,
we can use it to check the success of the operation. The SQLCODE attribute can have
three possible values:

Value Result

0 Success

-1 Error

100 No results returned

As we saw in a previous session that, you can check other attributes of SQLCA for more information such
as, error messages in SQLCA.SqlErrText, and database error numbers in the SQLCA.SQLDbCode.

There are times when the result column returns NULL values. Even though PowerBuilder doesn't provide
you with a method to check the value in the normal SQL, there is a way to check it in the Embedded SQL.
This method is to suffix the host column variable name with an indicator, separated by a colon.

Note that the indicator for NULL value checking should be declared as an integer.

The following example assumes that the product_description column in product_master


allows NULL values:

String lProductDesc
Int li_ProductNo, lDescInd1
li_ProductNo = Integer( sle_itemno.Text )
SELECT "product_master"."item_product_description"
INTO :lProductDesc:lDescInd1
FROM "product_master"
WHERE "product_master"."product_no" = :li_ProductNo;
If SQLCA.SQLCODE <> 0 then
// ....
End if

lDescInd1 is the indicator we have used in the above example, and it can have one of the
following values:

Value Result
0 Valid, not a NULL

-1 NULL

-2 Conversion error

The UPDATE Statement


You can explicitly execute an UPDATE statement through Embedded SQL. For example, in the
w_transactions window in our application, you have to update the product_balance in
product_master table for each transaction, i.e., receipt, return or receipt. Calling the Update()
function for the DataWindow would update the transaction table trans but not product_master
unless you have defined triggers on the trans table in the database. The alternate solution from
the client application is update 'product_master' table using Embedded SQL as shown in the
following example:

UPDATE "product_master"
SET "product_balance" = product_balance + :lQuantity
WHERE product_no = :li_ProductNo;
If SQLCA.SQLNRows <> 1 Then
RollBack Using SQLCA;
SQLCA.uf_display_error()
Return 0
End If

To find the number of rows affected by the UPDATE statement, we can check SQLCA.SqlNRows after the
UPDATE command is successfully executed.

The INSERT Statement


You can execute two forms of the INSERT statement. The following format inserts a single row
into the 'product_master' table.

INSERT INTO guest.product_master


( product_no, product_description, product_balance,
product_reorder_level,product_measuring_unit )
VALUES ( :li_ProductNo, :lDesc, :lBal, :lROrdLvl, :lMsUnt);

The second format inserts more than one row using a single statement. Suppose you wanted to
insert all records from 'product_master' table into a history table product_master. You could
use the following code:

INSERT INTO product_master ( <column list> )


SELECT < column list > FROM product_master ;

where you replace <column list> with the list of all the columns in the table. If you have a
table with many columns, this can turn into a long SQL statement. However, PowerBuilder does
support the asterisk wildcard character (*), which allows you to select all the columns at once:

// Sybase supports selecting values from a table


// and inserting into the same table.
INSERT INTO product_master
SELECT * FROM product_master ;
Inserting into the table from which you are selecting may not work with all databases, so you will have to
check with your specific database vendor's documentation.

The DELETE Statement


The DELETE statement can be used to delete selected entries from a database table. It can be
used as follows:

Int iDelYears
iDelYears = Integer( em_DelYears.Text )
DELETE FROM trans
WHERE datediff(Year,GetDate(),tran_date) > :iDelYears;

This uses the datediff() function to check the difference, in years, between the current date and the
tran_date column, in the table. It deletes any record whose difference is greater than the value in the
iDelYears variable, entered by the user. Here, we could also check SQLNRows to determine the rows
affected.

Cursors
Till now, in the embedded SELECT statements, we were able to retrieve only one row. Cursors are useful
to retrieve more than one result row, and it allows you to process one row at a time. Similar to the cursor
on a computer screen, a cursor indicates the position in the result set. The cursor points to a single row of
data and can only scroll forward, one row at a time. In other words, an application can retrieve one single
row, or move through the result set, one row at a time.

Declaring And Executing a Cursor


To use a cursor in an application, you need to define it using the DECLARE statement. DECLARE statement
allows you to define both the cursor name and the associated SQL statement, you want to execute.
Simply defining the cursor with the DECLARE statement doesn't automatically execute the specified SQL
statement - it's merely a declarative statement.

Creating a Cursor
When you paste a cursor declaration in your PowerScript, PowerBuilder allows you to select a table and
then it creates the template for you.

Before pasting the statement in your script, PowerBuilder prompts for the cursor name.

For example, you could declare as follows, to retrieve all product numbers and descriptions
from the product_master:

DECLARE lProductMasterCursor1 CURSOR FOR


SELECT product_no,
item_product_description
FROM product_master ;

lProductMasterCursor1 is the name of the cursor we refer to in the following scripts.

Executing the Cursor


An OPEN statement actually executes the cursor:

OPEN lProductMasterCursor1 ;
If SQLCA.SQLCODE <> 0 then
//
End If

Again, we can use SQLCODE to provide error handling.

Isolation Levels
Watcom supports isolation levels with the OPEN <cursor name> statement, but PowerBuilder doesn't
support it. The isolation level specifies the kind of actions that are not permitted while the current
transaction executes. The ANSI standard defines three levels of isolation for the SQL transactions:

 Level 1 prevents dirty reads.


 Level 2 prevents non-repeatable reads.
 Level 3 prevents both types of reads and phantoms.

Dirty Reads
A dirty read occurs when one transaction modifies a row and a second one reads it, before the first has
been able to commit the change. If the first transaction rolls back the changes, the information read by
the second transaction becomes invalid.

For example, Tran1 reads the row for product_no 10, with a product_balance of 120, and updates it to
100, but the transaction is not yet committed. In the mean time, Tran2 reads the same row and gets the
product_balance as 100. Now Tran1 issues a ROLLBACK, which sets the product_balance to its previous
value of 120. At this moment, Tran2 thinks the product_balance as 100, when it really is 120:

Non-Repeatable Reads
A non-repeatable read occurs when one transaction reads a row, and then a second transaction modifies
that row. If the second transaction commits its change, subsequent reads by the first transaction yield
different results.

For example, Tran1 reads the row for product_no 10 and sets the product_balance to 120. Now Tran2
reads the same row, and updates it to 80 and commits. If Tran1 then reads the same row again, it gets
80, instead of 120. That means, reading the same row is resulting in two different values.

Phantom Rows
A phantom row occurs when one transaction reads a set of rows which satisfy search criteria, before a
second transaction modifies the data (through an INSERT, DELETE, UPDATE and so on). If the first
transaction repeats the read with the same search conditions, it obtains a different set of rows.

Suppose Tran1 issues the following SELECT statement:

SELECT count(*) FROM product_master


WHERE product_balance < product_reorder_level;

and gets 75 rows. If Tran2 then does some processing, which makes the product_balance of another 10
items fall below the product_reorder_level, and now when Tran1 issues the same statement again, it gets
85 rows instead of 75.

The difference between "No-Repeatable reads" and "Phantom Reads" is that, the former one is dealing
with a single row value, where as the later is dealing with the result from a set of rows.

The FETCH Statement


To retrieve each row in a cursor and load it into host variables, you use a FETCH statement.
This statement allows you to fetch one row at a time, moving through the result set, after each
FETCH. You need to check SQLCA.SQLCODE to find the end of the result set. Have a look at the
following example:

FETCH lProductMasterCursor1
INTO :li_ProductNo, :lProductDesc ;
Do While SQLCA.SQLCODE <> 100
// Do While SQLCA.SQLCODE = 100
// Some processing such as loading into a ListBox, etc.
FETCH lProductMasterCursor1
INTO :li_ProductNo, :lProductDesc ;
Loop
CLOSE lProductMasterCursor1 ;

Once all rows are returned, we close the cursor with the CLOSE command.

Cursors and Transaction Objects


The DECLARE statement uses a SQLCA Transaction Object. If you need to use a different
Transaction Object, you have to add a USING <TransactObject_Name> clause at the end of the
SELECT part of the DECLARE statement. For example:

DECLARE lProductMasterCursor1 CURSOR FOR


SELECT product_no, product_description
FROM product_master
USING TranObjForSQLServer ;

You can specify the Transaction Object name only in the DECLARE statement. There is no need to specify
it in the OPEN, FETCH and CLOSE statements.

Scrollable Cursors
You can also make a cursor scroll. It can move forwards or backwards through the result set, or move to
an absolute or relative position in the result set. In other words, an application can retrieve the next or
previous row, retrieve a specific row of data, or retrieve a row of data that is at a specified distance from
the current row.

Even though PowerBuilder supports scrollable cursors, being able to use them depends on the back-end
support. You can use the following commands while using scrollable cursors:

 FETCH PRIOR
 FETCH FIRST
 FETCH NEXT
 FETCH LAST
If you don't specify any clause after the FETCH, it is NEXT by default. Sybase System 10 doesn't support
scrollable cursors, but Adaptive Server Anywhere supports scrolling to an absolute position or relative
position in the cursor result set.

Updating Through a Cursor


An application can update or delete a row to which the cursor currently points in the result set,
using CURRENT. For example:

UPDATE "product_master"
SET "product_balance" = product_balance + 100
WHERE CURRENT OF lProductMasterCursor1 ;

In this example, instead of using a WHERE clause, we give the name of the cursor. This updates the row
at the current cursor position - suppose you fetch four items and issue this command, the fourth row is
the one that gets updated. Updating a cursor doesn't change the row position in the result set. There are
few points to remember when updating using a cursor:

 A fetch, update or delete returns an error, only if any of the columns


were changed since the last read, even if the column is not included in
the SELECT list. If any of the rows in the result set has been deleted, it
creates a hole, and if you try to fetch it, it results in an error.
 You can get around this by using a Dynamic Scroll cursor. The SQL Anywhere
syntax for declaring a cursor is:

DECLARE <CursorName> [ SCROLL | NO SCROLL | DYNAMIC SCROLL]


CURSOR FOR <Statement> [ FOR UPDATE | FOR READ ONLY ]
 You simply specify whether you want scrolling, no scrolling or dynamic
scrolling. A dynamic cursor won't return an error, but will skip the
changed row and fetch the next row. The final clause of the declaration
statement specifies whether you want to allow updates or make it a read-
only.
 When using aggregate functions, DISTINCT options, GROUP BY
clauses, ORDER BY clauses or UNION operators, the cursor is not
updateable, also, when you specify FOR UPDATE, the table should
have at least one unique index, otherwise it results in an error.
 You can't paint this UPDATE statement with a WHERE CURRENT OF
clause from the Edit > Paste SQL menu option, if you have declared a
cursor in the script. Only when you declare a cursor as a shared, instance
or global cursor can you paste the SQL, by selecting Declare from the
menu option.

Deleting Through a Cursor


To delete the current row in a cursor result set, the procedure is similar to updating:

DELETE FROM "product_master"


WHERE CURRENT OF lProductMasterCursor1 ;

As with updating through a cursor, deleting a row is not allowed if the SELECT list contains aggregate
functions or uses a GROUP BY clause.

Stored Procedures
Stored procedures are collections of SQL statements, which are stored in the database. When a stored
procedure is first run, an execution plan is prepared, which makes any subsequent execution of the
procedure fast. Also, since the stored procedure is stored in the database, the client application only needs
to send the stored procedure name and parameters if any, for the procedure to run. This reduces network
traffic and makes the execution faster.

If the database back-end supports stored procedures, you can execute them from a PowerBuilder
application's script. Both SQL Anywhere and SQL Server support stored procedures.
Let's look at the syntax involved in each.

SQL Anywhere Stored Procedures


A simple example of a procedure is shown below:

CREATE PROCEDURE sp_list_item_product_balance


(IN pProduct_balance integer)
RESULT(product_no integer, product_description char(32),
product_balance integer)
BEGIN
SELECT product_no, product_description, product_balance
FROM product_master
WHERE product_balance > pProduct_balance
END ;

Note that we place any required parameters into the brackets following the procedure name. The keyword
IN specifies the purpose of the parameter, and can be either IN, OUT or INOUT, and is followed by the
name of the parameter and its data type.

In the result parentheses, you need to declare all the columns returned by the SELECT statement. We
then declare the SELECT statement between the BEGIN and END keywords.

The names in the result declaration can be same as the column names.

We could execute this procedure from the Database Administration Painter, using the
following syntax:

EXECUTE sp_list_item_product_balance( 1000 );

There are many limitations to SQL Anywhere stored procedures, such as, without conditional statements,
you can't have more than one SQL statement and you can't specify a default value to be used as the
parameter.

SQL Server Stored Procedures


On the other hand, SQL Server stored procedures are both powerful and flexible. You specify
the OUTPUT keyword only for those parameters for which you want a value to be returned. You
can specify default values and have as many SQL statements as you want. The syntax is as
follows:

CREATE PROCEDURE Proc2


@Product_balance int, @affected_count int =NULL OUTPUT
as
SELECT product_no, product_description, product_balance
FROM product_master
WHERE product_balance > @product_balance
SELECT @affected_count = @@rowcount
SELECT @affected_count
return (0)

Note that in SQL Server, there is no need to declare the result set.

To execute a SQL Server stored procedure in the Database Administration Painter, we would
use the following:
EXECUTE Proc2 @product_balance = 100;

Note that you don't need to supply parentheses for the parameters, when executing a stored
procedure residing in the SQL Server.

Using a Stored Procedure In PowerScript


As we've already seen, you can execute a stored procedure by specifying it as a data source to a
DataWindow. To execute a stored procedure through PowerScript, you can either use Embedded SQL or
Dynamic SQL.

There are four steps involved in using a stored procedure through Embedded SQL:

 Declaration.
 Execution.
 Retrieving rows.
 Closing.

Declaring a Stored Procedure

To declare a stored procedure, you need to click on   icon and select 'Procedure Declare' menu option.
Alternately, you can display popup menu in the script painter and select 'Paste Special > SQL > Procedure
Declare' menu option. This brings up a list of currently available procedures:
When you select the procedure you want, you'll be prompted to supply parameters that are required:

This allows you to select from a predefined list of available program variables. For example, if you have a
SingleLineEdit control on your window, you can supply the text attribute as a parameter, so that a user
could type in a value at run-time.

Once you supply the parameter values, you'll be prompted for the procedure name, which would be used
to refer to the stored procedure in PowerBuilder:

The declaration statement will then be pasted into your script:

DECLARE TestProc1 PROCEDURE FOR sp_list_item_product_balance


pProduct_balance = :sle_balance.Text ;

Executing a Stored Procedure


To execute a stored procedure, you simply use the EXECUTE command in your PowerScript:
EXECUTE TestProc1();

This is similar to the cursor's OPEN command, so it won't surprise you to know that we can also use the
FETCH command to retrieve the data.

Executing a Remote Stored Procedure


When connected to the SQL Server, you can execute a stored procedure that is located on a
remote server. To do this, you qualify the stored procedure name with the appropriate server
and database names. For example, if the SQL Server stored procedure we saw above is residing
on Server2 and our application is connected to Server1, we could execute the stored procedure
by declaring it as follows:

DECLARE lProc_0001 PROCEDURE FOR Server2.DB1.dbo.Proc2


@Product_Balance = :sle_balance.Text,
@affected_count = :laffected_count OUTPUT ;

A remote stored procedure has a different meaning when connected to a DB2 database via MDI Database
Gateway for DB2. Here, a Remote Stored Procedure (RSP) is a customer-written CICS program which can
be initiated by a client application, such as PowerBuilder. The CICS program can be written in COBOL II,
Assembler, PL/1 or C. RSPs are unique to the MDI Gateway for DB2 or MVS solution.

Here, Server1 sends the request to execute the stored procedure to Server2. Server2 then executes the
procedure and sends the results back to Server1 and Server1 returns the results to the client
(PowerBuilder application).

Retrieving Multiple Result Sets


Before talking about Multiple result sets, let's be clear about a result set. It means, the rows
returned by one SELECT statement. In SQL Server, you can have as many SELECT statements as
you want, and each of those SELECT statements can return rows. The columns that are
returned by each SELECT statement could be different. For example:

Declare ProcDummy
AS
/* Result set 1, returns everything from product_master */
select * from product_master
/* Set 2, returns everything from trans table */
select * from trans
/* Set 3, returns the no. of rows for the above SELECT */
select @@rowcount
/* Not a result set, because, we are using SELECT statement
to assign a value to a variable.*/
select @dummy = 100
/* Set 4, returns everything from units table */
select * from units
return 0

While executing multiple result set stored procedures in PowerBuilder, you can retrieve only one set at a
time. When the script fetches the first result set, SQLCODE is populated with the 100 return code. From
then onwards, the values of the second result set are available to the script.

For example, the following script would execute sp_help Stored Procedure. If you supply a
table name as parameter, it would give five result sets. The script listed below fetches the first
two result sets:

// This script assumes there are 2 DataWindow controls in


// the window with appropriate columns and data types.
String lObjectName, lOwner, lObjectType, lDataSegment
Long lNewRow
DateTime lCreationTime
lObjectName = "sysobjects"
DECLARE sp_help_proc PROCEDURE FOR dbo.sp_help
@objname = :lObjectName ;
EXECUTE sp_help_proc ;
If SQLCA.SQLcode <> 0 Then
MessageBox( "Error", SQLCA.SQLErrorText )
Close sp_help_proc ;
Return
End If
dwc_1.Title = "Result Set: #1"
Do While True
Fetch sp_help_proc
INTO :lObjectName, :lOwner, :lObjectType ;
If SQLCA.SQLcode = 100 Then Exit
lNewRow = dwc_1.InsertRow(0)
dwc_1.SetItem( lNewRow, 1, lObjectName )
dwc_1.SetItem( lNewRow, 2, lOwner )
dwc_1.SetItem( lNewRow, 3, lObjectType )
Loop
dwc_2.Title = "Result Set: #2"
Do While True
FETCH sp_help_proc
INTO :lDataSegment, :lCreationTime ;
If SQLCA.SQLCOde = 100 or SQLCA.SQLCode = -1 Then Exit
lNewRow = dwc_2.InsertRow(0)
dwc_2.SetItem( lNewRow, 1, lDataSegment )
dwc_2.SetItem( lNewRow, 2, lCreationTime )
Loop
Close sp_help_proc ;

Retrieving the Value Of OUTPUT Variable


In SQL Server, you can get the changed value of a stored procedure parameter, by declaring
the parameter as OUTPUT. For example:

Declare TestProc1 @Var1 Int=NULL OUTPUT


AS
select * from trans
select @Var1 = @@RowCount
return

In the above example, we are assigning the number (count) of rows returned by the first SELECT
statement to the @Var1 variable. @Var1 is a parameter to the stored procedure. When the client executes
this stored procedure, it gets the value of @Var1. Please note the way of assigning defaults to the stored
procedure parameter when user does not pass any value. In the above example, we are assigning NULL
value as default to the parameter @Var1. If you want to display error message when the user doesn't
pass parameter value, then do not assign a default, for ex: Declare TestProc1 @Var1 Int OUTPUT'. In this
case, Sybase automatically generates an error when parameter value is not specified at execution time.

In PowerBuilder, to retrieve the OUTPUT variable value, you need to do one more FETCH after you see
100 in the SQLCA.SQLCode, similar to the one you did in the "Multiple Result Set" stored procedure
section. Make sure that the number of variables you put in the INTO clause are equal to the number of
variables that are declared with OUTPUT keyword.
Dynamic SQL
Dynamic SQL consists of a set of embedded SQL facilities, that are specially provided for the construction
of a generalized, on-line (and possibly interactive) applications. PowerBuilder doesn't support certain SQL
statements, either through DataWindow or embedded SQL. These include:

 Data Definition Language (DDL), for example, CREATE TABLE.


 Certain forms of SELECT statements, such as, SELECT * from
#Temp1, which selects from a temporary table - a table that doesn't exist
at compile time.
 SET commands, for example, SET ROWCOUNT 100.
 Commands to GRANT and REVOKE privileges, for example, GRANT
SELECT on product_master TO PUBLIC.
You can get around these using dynamic SQL. Since PowerBuilder doesn't check for SQL syntax errors at
compile time, it is up to the database to take care of it.

We can broadly divide dynamic SQL statements into 4 categories:

 Non-result set statements with no input parameters.


 Non-result set statements with input parameters.
 Result set statements in which the input parameters and result set
columns are known at compile time.
 Result set statements in which the input parameters, the result set
columns or both are unknown at compile time.
You need to use different formats in order to execute each of these statements. Dynamic SQL introduces
two new terms, dynamic Staging Area and dynamic Description Area.

Dynamic Staging Area


Dynamic Staging Area is internally used by PowerBuilder and is the connection between execution of a
statement and Transaction Object. You can't access the information in the dynamic Staging Area.
PowerBuilder provides a global-level dynamic Staging Area named SQLSA. It contains the following
information, for use, in subsequent statements:

 The SQL statement in the PREPARE statement.


 The number of parameters.
 The Transaction Object for use in subsequent statements.

Dynamic Description Area

The dynamic description area stores information about input and output parameters and is
used with the fourth format of dynamic SQL. PowerBuilder provides a global-level
dynamic description area named SQLDA, with the following structure:
Attributes Meaning

SQLDA.NumInputs Number of input parameters

SQLDA.InParmType Array of input parameter types

SQLDA.NumOutputs Number of output parameters

SQLDA.OutParmType Array of output parameter types

The two principal statements of dynamic SQL are PREPARE and EXECUTE. The PREPARE statement takes
the base SQL commands and places them in the SQLSA command buffer for execution. The EXECUTE
statement passes these commands from the buffer to the back-end database and returns the feedback
information to SQLCA as usual.

Format 1
If a series of SQL statements contain no SELECT statements and has no parameters, it can be
executed without an explicit PREPARE. This is done with the EXECUTE IMMEDIATE statement
which has the following format:

EXECUTE IMMEDIATE : String_Variable_that_contains_SQL_Statement ;

The following example creates a table called t_product_price. This table is meant to store
history of product prices.

String ls_SqlString
ls_SqlString = 'CREATE TABLE "dba"."t_product_price"' + &
'("product_no" integer NOT NULL ,' + &
'"effective_date" date NOT NULL ,' + &
'"price" numeric(8,0) NOT NULL' + &
', PRIMARY KEY ("product_no","effective_date")' + &
', FOREIGN KEY "fkey1_to_product_master" ("product_no"'+ &
') REFERENCES "dba"."product_master"' + &
'ON DELETE RESTRICT )'
EXECUTE IMMEDIATE :ls_SqlString ;
If SQLCA.SQLCODE <> 0 Then
MessageBox( "ERROR", string( sqlca.sqlcode ) + &
SQLCA.SqlErrText )
End If
Return 0

This SQL statement doesn't take any input from the user and doesn't generate any result set - it simply
create a new table.

Format 2
While considering format 2, you need to be prepared to handle user-defined parameters, even
though you still can't get the results' set. Before executing the SQL statement, it has to be
prepared using the PREPARE command. The syntax is as follows:

PREPARE command FROM :string ;


Here, string is an expression of the host language, that yields the character string representation of an
SQL statement, and command is the name of an SQL variable, used to refer the prepared version of that
SQL statement.

The statement to be prepared can be any interactive SQL statement. An example for a Format 2
Dynamic SQL statement is shown below:

Int li_ProductNo
Date Ldt_PriceDate
Decimal Ldec_Price
Li_ProductNo = 10
Ldt_PriceDate = Today()
Ldec_Price = 120.43
PREPARE SQLSA FROM 'INSERT INTO "dba"."t_product_price1" VALUES (?,?,?)'
;
EXECUTE SQLSA USING :Li_ProductNo, :Ldt_PriceDate, :Ldec_Price ;
If sqlca.sqlcode <> 0 Then
MessageBox( "ERROR", string( sqlca.sqlcode ) + &
SQLCA.SqlErrText )
End If
Return

Statements such as DECLARE CURSOR, OPEN, FETCH, CLOSE and so on can't be the subject for PREPARE,
and the source form of a statement must not include UPDATE WHERE CURRENT OF, DELETE WHERE
CURRENT OF or a statement terminator.

The question marks in the INSERT statement are placeholders. PowerBuilder replaces these with the
values supplied in the EXECUTE SQLSA statement.

Format 3
The method of handling SELECT in dynamic SQL is different from other statements. This is because, it
returns data to the program; all other statements simply return feedback information.

A program using SELECT, needs to know something about the data values to be retrieved,
since, it has to specify a set of target variables to receive those values. In other words, it needs
to know the number of values there will be in each result row, and also the data types and
lengths of those values.

String l_Product_description ; Int Product_no


DECLARE Cursor1 DYNAMIC CURSOR FOR SQLSA ;
PREPARE SQLSA FROM "SELECT product_no, &
product_description from product_master";
OPEN DYNAMIC Cursor1 ;
FETCH Cursor1 INTO :Product_no, :l_Product_description ;
If sqlca.sqlcode <> 0 Then
MessageBox( "ERROR", string(sqlca.sqlcode) + &
SQLCA.SqlErrText)
Return
End If
DO WHILE Sqlca.Sqlcode = 0
MessageBox( "Format 3 Results", String( Product_no)+
" " + l_Product_description )
FETCH Cursor1 INTO :Product_no, :l_Product_description ;
LOOP
CLOSE Cursor1
At compilation time you know the result set, and that's why you can use the FETCH statement to
return the results.

A typical use of this command is to select information from a temporary table, a table which doesn't exist
at compile time. PowerBuilder doesn't check the existence of the table, since, it doesn't check the syntax
of the SQL statements and therefore doesn't generate any errors.

Format 4
This format is similar to Format 3, except that you don't know the result set, hence you can't
issue a FETCH statement. Typically, this format is used to accept the SQL statement from the
user (ad-hoc queries), and execute and present the results back to the user. The following
example uses a simple SELECT statement:

String lSQLStr
Integer lParm1
// We are hard-coding the SQL here. But, you can accept
// from the user and use it for ad-hoc queries.
lSQLStr = "select product_description " + &
" from product_master where product_balance > ?"
lParm1 = 10
PREPARE SQLSA from :lSQLStr ;
DESCRIBE SQLSA into SQLDA ;
DECLARE lCursor1 DYNAMIC CURSOR for SQLSA ;
// Setting value for the first variable in the
// WHERE clause.
SetDynamicParm( SQLDA, 1, lParm1 )
// Now, SQLDA has the values for the parameters.
OPEN DYNAMIC lCursor1 USING descriptor SQLDA ;
FETCH lCursor1 USING descriptor SQLDA ;
DO while SQLCA.SQLCODE = 0
// Since we hard-coded the SQL, we know the result
// set. You need to call different function depending
// on the datatype. You can check datatype by referring
// to OutParmType array of SQLDA.
lb_1.AddItem( GetDynamicString( SQLDA, 1 ) )
FETCH lCursor1 USING descriptor SQLDA ;
LOOP
close lCursor1 ;

As described earlier, the PREPARE command formats the input SQL statement using the information
specified in the FROM clause, and populates SQLSA, the object specified after the PREPARE command,
with these statements.

Handling the Results


A similar process is required for handling results. PowerBuilder needs to know where to populate the
information about the results, so, the DESCRIBE and PREPARE statements are used. Typically, this
information storage container is SQLDA, unless you create another object of type SQLDA to connect to
more data sources.

Before the actual execution of the SQL statement, you need to call SetDynamicParm() to specify
parameters. This command takes three parameters: Dynamic Staging Area, the parameter number and
the parameter itself. The FETCH command used here is different from the one used in cursors and Format
3. In this format, you don't specify the variables to store the result set, since, you don't know the number
of columns in the result set, their data type and length of each column in the result set.
You can check both NumOutputs attribute of SQLDA, to establish the number of columns in the result set,
and OutparmType array, for the datatype of each column in the result set. The data type can be any one
of the following enumerated examples:

 TypeDate!
 TypeTime!
 TypeDateTime!
 TypeString!
 TypeDecimal!
 TypeDouble!
 TypeInteger!
 TypeLong!
 TypeReal!
 TypeBoolean!
After checking for the data type, call one of the following corresponding functions, depending on the data
type, to get the actual value:

 GetDynamicDate()
 GetDynamicTime()
 GetDynamicDateTime()
 GetDynamicString()
 GetDynamicNumber()

Summary
In this session, you have learned about executing SQL statements, by embedding them in the
PowerScript. In real-world projects, you can use DataWindows 99% of the time. We don't think you will
ever need format 3 or 4. If needed, use the statement given by the user to create a dynamic
DataWindow, which is more flexible and efficient. For example, in Format 4, or in any, you were able to
find out the number of columns and their data types in the result set. This is just half of the work. How
you are going to display the result set and print it is still a big nightmare. If you use dynamic
DataWindows, it is easy to program and in fact everything else is easy. For a quick summary of this
session, browse embedded-and-dynamic-sql.ppt (a PowerPoint presentation).

External Functions
PowerBuilder offers you hundreds of functions to use, while developing applications. PowerScript suffices
in most of the situations, but there are situations where functions that are written in languages other than
PowerScript, are required to be called from within the PowerScript. For example, you may want to know
the free space left on a specific drive, memory (un)used, etc... PowerBuilder didn't provide you functions
for these purposes. Sometimes, you may want to access functions from a third-party DLL --a DLL that is
not from Powersoft and operating system. PowerBuilder provides you a way to access external functions.

In This Session You Will Learn About:

 Declaring external functions.


 PowerBuilder compatible Datatypes with C++ Datatypes.
 Local & Global External functions.
 Completing "w_about" Window.
 Examples of Multimedia.
Estimated Session Time

90+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/ Professional/ Enterprise


version ) installed on your computer.

Introduction
External functions are functions that are written in languages other than PowerScript and are stored in
dynamic link libraries (DLL's). These functions could be written in language such as C/C++, Pascal, etc...

You may want to use external functions, typically in the following situations:

 A call to MS-Windows Application Programming Interface (API) to do


low-level DOS and Windows functions and to get system information
( Like Windows system directory, free resources etc. ).
 Whenever there is a need for specialized functions such as Multimedia,
Telephony etc..
 When processing speed is an utmost requirement. PowerScript has speed
constraints. Functions written in a 3GL like C/C++ are faster.
 Reusing existing code.
A DLL is a file containing executable MS-Windows code. To use an external DLL in PowerBuilder, the DLL
must be written using the Pascal calling sequence and the datatypes required to pass to and from the DLL
must have an appropriate counterpart in PowerScript. Before you call an external function, make sure to
go through the external function's documentation to find out the function syntax, parameters and return
values. Even before you start using the external function, you need to declare the external function in
PowerBuilder.

Declaring External Functions


Like any other function you create in PowerBuilder, you can declare an external function at two different
levels, Global and Local. The scope of a "global external function" is similar to "global variables"; they can
be accessed from anywhere in the application. Local external functions are defined at window, menu, or
an user object. These functions are part of the object's definition and can always be used in the scripts for
the object itself. The accessibility of a Local external function is similar to instance variable.

You can declare a global external function by selecting Declare > Global External Functions from any
painter except Library, Structure painters in the PowerBuilder. You can declare a local external function by
selecting Declare > Local External Functions from the menu when the object (window/menu/user object)
painter is open.

Invoke the window painter. Select New from the Select Window dialog box. Save this window as
w_system_info. Make the window a Response window. Paint it as shown in the following picture.
All the controls in the picture that are displaying numbers are SingleLineEdit controls. Name them as
follows (left to right, top to bottom):

sle_tot_phy_mem, sle_ava_phy_mem, sle_tot_vir_mem, sle_ava_vir_mem, sle_tot_page_size,


sle_ava_page_size, sle_screen_width, sle_screen_height.

The Declare External Function dialog box (picture not shown here) title bar identifies the type of function
you are declaring(global or local). The text box displays the declared external functions. If no external
function is declared, the MultiLineEdit is blank.

After OK button is clicked, PowerBuilder compiles the declaration. If there is any syntax errors,
error messages are displayed, and you must correct them before PowerBuilder saves the
declaration.

{access} FUNCTION ReturnDatatype Name &


( { {REF} datatype1 arg1, ..., {REF} datatype arg } ) &
LIBRARY libname ALIAS FOR ExternalFunctionName

Ampersand is not part of the syntax. However, you need to use & (Ampersand) if the function declaration
spans multiple lines.

You can also declare external subroutines, which are same as external functions, except that
they don't return values.

{access} SUBROUTINE Name ( { {REF} datatype1 arg1, &


..., {REF} datatypen argn } ) LIBRARY libname ALIAS FOR &
ExternalFunctionName

Access Level: When declaring a local external function, you can specify its access level, i.e., you can
specify the scripts that can have access to the function. The access level can be Public, Private or
Protected. You have already learned about access levels in the "OOP - PB Implementation" session.

Global external functions always have Public access level.

FUNCTION or SUBROUTINE: A keyword that determines the way return values are handled. If there is
a return value, declare it as a FUNCTION. If it returns nothing or returns VOID, specify SUBROUTINE.

Return Datatype: The Datatype of the value returned by the function.

Name: The name of a function or subroutine that resides in a DLL.

REF: Specifies that you are passing the parameter by reference. By default, PowerBuilder passes
parameters by value.
Datatype Arg: The Datatype and name of the arguments, for the function or subroutine. The list must
match with the definition of the function in the DLL. Each datatype, argument pair can be preceded by
REF.

LIBRARY "libname": The LIBRARY keyword is followed by a string containing the name of the DLL in which
the function or subroutine is stored. Windows' DLLs usually have the extension DLL or EXE. The library
name is enclosed in quotation marks.

The full path should not be specified, but the library must be available to the application at execution
time in the MS-Windows PATH or application PATH.

ALIAS FOR "extname": This clause is optional. If the name in DLL is not the name you want to use in the
script or if the name in the database is not a valid PowerScript name, you must specify ALIAS FOR
"extname", to establish an association between the PowerScript name and the external name. This can
happen if the function name conflicts with the PowerBuilder reserved name.

Datatypes For External Function Arguments


When you declare an external function, the Datatypes of the arguments must correspond with the
Datatypes declared in the function's source definition. The following table lists C++ Datatypes and the
corresponding PowerBuilder Datatypes.

The PowerBuilder Datatype you select, depends on:

 The Datatype in the DLL for the function.


 Platform (running on a 16-bit or 32-bit platform).

The 32-bit platforms that PowerBuilder supports are Windows 95, Windows NT,
Macintosh, and UNIX (Solaris). The 16-bit platform that PowerBuilder supports is
Windows 3.1x.

Datatype in source code Platform PowerBuilder Datatype

* (any pointer) Windows 3.1 , 95, NT Long

 
Macintosh UnsignedLong.

Handle Windows 3.1 UnsignedInteger

 
32-bit Windows (95 & NT), Macintosh UnsignedLong

char * All Blob

   
Strings

char All char

string All String


Boolean Windows 3.1 , 95, NT Boolean

 
Macintosh char

short All Integer

unsigned short All Integer

int Windows 3.1 Integer

 
Windows 95, NT, Unix Long

unsigned int Windows 3.1 Integer

 
Windows 95, NT, Unix Long

long All Long

unsigned long All Long

float All Real

double Only Windows 95/NT Double

date All Not Supported

DateTime All Not Supported

time All Not Supported

External Function Arguments


Arguments to external functions can be passed by reference or by value. For string datatype arguments,
PowerBuilder always passes a pointer to the string, irrespective of whether the argument is passed by
value or by reference. The difference is that if you pass the argument by reference, any change by the
function to the string is available back to PowerBuilder.

An external function cannot return PowerBuilder a pointer. PowerBuilder cannot access memory
beyond the memory it allocates for it self.

If the function takes a string as an argument by reference and replaces it with some value, the
PowerScript string variable must be long enough to hold the returned value.

Before you call the external function, use the Space() function to fill the string with blanks, equal to
the maximum number of characters you expect the function to return.

Calling an External Function


In this example, let's find out screen dimensions in pixels. We need to call GetSystemMetrics() windows
function.

Open w_system_info window. Select Declare > Local External Functions from the menu.
Declare the following function. Here, we are calling functions that reside in User32.dll. If you
are using Windows, replace "User32.dll" with "user.dll".

Function int GetSystemMetrics (int index) Library "USER32.DLL"

Write the following code in the Window open event:

// Argument zero returns screen width in pixels


sle_screen_width.Text = &
String( GetSystemMetrics(0), "#,###" )
// Argument 1 returns screen height in pixels
sle_screen_height.Text = &
String( GetSystemMetrics(1), "#,###" )

Open the "w_about" window. Write the following code to the clicked event for the
"cb_system_info" CommandButton:

Open( w_system_info )

In the w_about window, we have code for the timer event, to close the window in ten seconds. Let's get
rid of that code. So, comment the code in the open event for the "w_about" window.

Run the application and test the code.

Calling an External Subroutine


In this section we are going to call a subroutine GlobalMemoryStatus to find out memory details.
According to the definition of subroutines, they don't return anything. So is this subroutine. It takes
structure as an argument and populates memory details in it. After calling this subroutine, we can refer to
it for the memory details. You can pass PowerBuilder structures to external C functions, as long as they
have the same definition (with equivalent datatypes) and the same order of the structure members.

Open w_system_info. Declare a structure os_Memory, as shown in the following picture, by selecting
Declare > Window Structures from the menu.

Declare the following external function, by selecting Declare > Local External Functions… from
the menu.
Subroutine GlobalMemoryStatus (ref os_Memory ls_Memory ) &
Library "KERNEL32.DLL"

Append the following code to the Open event of "w_system_info" window.

os_Memory ls_Memory
GlobalMemoryStatus( ls_Memory )
sle_tot_phy_mem.Text = &
String( (ls_Memory.ul_TotalPhysicalMemory/ &
(1024 * 1024)), "#,###.00" ) + "MB"
sle_ava_phy_mem.Text = &
String( (ls_Memory.ul_AvailablePhysicalMemory/ &
(1024 * 1024)), "#,###.00" ) + "MB"
sle_tot_vir_mem.Text = &
String( (ls_Memory.ul_TotalVirtualMemory/ &
(1024 * 1024)), "#,###.00" ) + "MB"
sle_ava_vir_mem.Text = &
String( (ls_Memory.ul_AvailableVirtualMemory/ &
(1024 * 1024)), "#,###.00" ) + "MB"
sle_tot_page_size.Text = &
String( (ls_Memory.ul_TotalPageFile/ &
(1024 * 1024)), "#,###.00" ) + "MB"
sle_ava_page_size.Text = &
String( (ls_Memory.ul_AvailablePageFile/ &
(1024 * 1024)), "#,###.00" ) + "MB"

In the above code, we are declaring a local variable "os_Memory" of structure type and sending that
variable as the argument to the subroutine. Later on we are accessing the values of the structure
members with the . (dot) notation.

Run the application and test the code.

Multimedia and PowerBuilder


Windows provides a variety of multimedia services through Media Control Interface (MCI). MCI provides
applications with device independent capabilities for controlling audio and visual peripherals. PowerBuilder
applications can use MCI to control any of the supported multimedia devices.

The important DLL for MCI communication is MMSYSTEM.DLL under 16-bit and winmm.dll under 32-but
environment. MCI includes two interfaces:

Low-Level Interface (Command Message Consists of C constants and structures.


Interface):

High-Level Interface (Command String Textual version of Low-Level Interfaces, that are in easy-to-read
Interface): format.

MS-Windows converts Command Strings into Command Messages, before sending them to the MCI Driver
for processing. MS-Windows scans both WIN.INI and SYSTEM.INI (Or registry in the 32-bit environment),
and loads the appropriate drivers into memory. It then processes the commands sent by the application
and sends them to the appropriate driver for execution.

The [MCI EXTENSIONS] section in WIN.INI contains all the required file extensions and the
related physical device names.
mid=Sequencer
rmi=Sequencer
wav=waveaudio
avi=AVIVideo
fli=Animation1
flc=Animation1

After examining this .INI file, MMSYSTEM.DLL reads the [MCI] section in SYSTEM.INI to
discover the appropriate driver and then loads it. For example, the physical device for MDI
extension is a sequencer. The driver that plays the sequencer is MCISEQ.DRV, which is listed
under [MCI] section in SYSTEM.INI.

[mci]
cdaudio=mcicda.drv
sequencer=mciseq.drv
waveaudio=mciwave.drv
avivideo=mciavi.drv
videodisc=mcipionr.drv
vcr=mcivisca.drv
Animation1=mciaap.drv

Windows comes with the following drivers:

Device Name Driver Name

sequencer MCISEQ.DRV

waveaudio MCIWAVE.DRV

There are a few others that come with the SDK, and if you need more, you can buy them from third-party
vendors. There are two important functions available at the high-level interface, which are sufficient for
any PowerBuilder application. They are mciSendString() and mciGetErrorString()under 16-bit environment
and mciSendStringA() and mciGetErrorStringA() under 32-bit environment.

We can declare them as external functions, as above, and then use them in our PowerBuilder
applications. For example, we could declare mciSendStringA() as follows:

Function Long mciSendStringA(String lpstrCommand, &


Long lpstrReturnString, &
Int uReturnLength, Int hWindCallBack) Library "winmm.dll"

In the above code, replace "winmm.dll" with "mmsystem.dll", if you are working on 16-bit windows. The
first parameter is the most used parameter and is the command string to execute, for example, Play
c:\windows\ringin.wav.

The following list illustrates the most common commands used as first parameters
ofmciSendStringA():

Open <Device ID> [Parameters] [Notify] [Wait]


Play <Device ID> [Parameters] [Notify] [Wait]
Close <Device ID> [Notify] [Wait]
The Open command loads the driver into memory (if it isn't already loaded) and assigns a Device ID that
can be used to identify the device in subsequent commands. If you don't include a Wait parameter, the
application gets control immediately after issuing the command to the driver, instead of waiting until the
completion of the operation.

The Play command has many parameters, depending on the device. For example, you can specify the first
and last track numbers to play from a CD, such as 'Play CDAUDIO from 6 to 10'. If it is a video device,
you can specify it to play fast, slow, reverse and so on. Some other useful basic commands are Record,
Pause, Resume, Stop and Seek. Finally, you need to close the device.

We have a window w_script_practice. Right? Open that window and place a CommandButton
cb_multimedia. Declare the following external functions by selecting Declare > Local External
Functions from the menu.

Function Int mciGetErrorStringA( Long ldwError, &


Ref string lpszErrorText, int cchErrorText) &
Library "winmm"
Function Long mciSendStringA( String lpstrCommand, &
Long lpstrReturnString, Int uReturnLength, &
Int hwndCallBack) Library "winmm"

Write the following code for the Clicked event of cb_multimedia:

String l_path, l_file, l_Error


Long lRetStatus
lRetStatus = GetFileOpenName( "Select File", l_path, &
l_file, "MCI", "Audio Files(*.wav),*.wav" + &
",All Files(*.*),*.*')
If lRetStatus = 0 Then Return 0
lRetStatus = MCISendStringA( "OPEN " + l_path, 0, 0, 0 )
If lRetStatus <> 0 THEN
MCIGetErrorStringA( lRetStatus, l_Error, &
len(l_Error) - 1)
MessageBox( "Open Error", l_Error )
End If
MCISendStringA( "PLAY " + l_path + " WAIT", 0, 0, 0 )
If lRetStatus <> 0 THEN
MCIGetErrorStringA( lRetStatus, l_Error, &
len( l_Error) -1)
MessageBox( "Play Error", l_Error )
End If
MCISendStringA( "CLOSE " + l_path, 0, 0, 0 )
If lRetStatus <> 0 THEN
MCIGetErrorStringA( lRetStatus, l_Error, &
len( l_Error) -1)
MessageBox( "Close Error", l_Error )
End If

In the above code, GetFileOpenName() function allows the user to select a file. So, run this window and
play "tada.wav" wave file.

If you don't play a sound file from beginning to the end and if you don't want to write the above script,
you can make use of sndPlaySoundA() function. Let's play windows start music when our application
starts.

Open w_mdi_frame window and declare the following local external function by selecting
Declare > Local External Functions from the menu.
Subroutine sndPlaySoundA( string s_wav_file, &
int uFlags ) Library "winmm"

If you are a windows 16-bit user, replace "winmm" with "mmsystem". In the open event for
"w_mdi_frame" window, write the following code.

sndPlaySoundA( "Utopia Windows Start.wav", 1 )

If you are windows 16-bit user, replace sndPlaySoundA with sndPlaySound and also replace the sound file
with the appropriate one. Run and test the application.

At run-time, DLLs must be in one of the following directories:

 The current directory.


 The Windows directory.
 The Windows System sub-directory.
 Directories in the DOS path or under the directories listed in the
AppPath for the executables in the registry, if the application is running
under Window 95.

Frequently Used MS-Windows APIs


The section explains some of the frequently needed Windows API that do not have equaling PowerBuilder
functions. Paint a window as shown below and name those controls accordingly or browse
the w_external_functions.zip that has all the source code.

Copying the Specified File


Declare a local external function as shown below.

FUNCTION boolean ufx_CopyFile(ref string cfrom, &


ref string cto, boolean flag) LIBRARY "Kernel32.dll" &
ALIAS FOR "CopyFileA"

The following code calls ufx_CopyFile() which is an alias for CopyFileA() function. This function
doesn't display any dialog box, it simply copies the file. The third argument tells to the function
that whether it should overwrite the target file if already exists or not.

// Object: cb_copy_file
// Event: Clicked
if isNull(trim(sle_SourceFile.Text)) OR &
isNull(trim(sle_TargetFile.Text)) THEN
Return
end if
boolean lb_rc
String ls_Source, ls_target
ls_Source = trim( sle_SourceFile.Text )
ls_target = trim( sle_TargetFile.Text )
lb_rc = ufx_CopyFile( ls_Source, ls_Target, &
cbx_overwrite.checked )
MessageBox("CopyFile", string(lb_rc))
Finding the Current Directory
Declare a local external function as shown below:

FUNCTION ulong ufx_GetCurrentDirectory(ulong BufferLen, &


ref string currentdir) LIBRARY "Kernel32.dll" &
ALIAS FOR "GetCurrentDirectoryA"

Make sure the buffer is enough in length to store the full path of the current directory,
otherwise, it will not work.

// Object: cb_display_current_dir
// Event: Clicked
ulong lul_buf = 1000
string ls_curdir = space( lul_buf )
ufx_GetCurrentDirectory(lul_buf, ls_curdir)
MessageBox("Current Directory:", ls_curdir)

Setting the Current Directory


Declare a local external function as shown below.

FUNCTION boolean ufx_SetCurrentDirectory(ref string &


cdir) LIBRARY "kernel32.dll" ALIAS FOR &
"SetCurrentDirectoryA"

The following code sets the current directory to the specified directory.

// Object: cb_display_current_dir
// Event: Clicked
string ls_Dir
ls_Dir = sle_CurrentDir.Text
ufx_SetCurrentDirectory( ls_Dir )

Creating a Directory
Declare a local external function as shown below.

FUNCTION boolean ufx_CreateDirectory(ref string &


pathname, int sa) LIBRARY "Kernel32.dll" &
ALIAS FOR "CreateDirectoryA"

The following code creates the specified directory. For example, if you specify "c:\WINNT\test"
as argument, it is not going to destroy whole WINNT directory and create a fresh one for you.
It doesn't touch the existing ones and creates the missing ones, in this case, it is 'test'.

// Object: cb_create_dir
// Event: Clicked
string ls_dir
ls_dir = trim(sle_directoryName.Text)

If ufx_CreateDirectory(ls_dir, 0) then
MessageBox("CreateDirectory", &
sle_directoryName.Text + " is successfully created" )
else
MessageBox("CreateDirectory", "Failed")
end if

Finding the Computer Name


Declare a local external function as shown below.

FUNCTION boolean ufx_GetComputerName(ref string cname, &


ref long nbuf) LIBRARY "Kernel32.dll" ALIAS &
FOR "GetComputerNameA"

The following code displays your computer name.

// Object: cb_display_computer_name
// Event: Clicked
long ll_buf = 25
string ls_ComputerName = space( ll_buf )
ufx_GetComputerName(ls_ComputerName, ll_buf )
MessageBox("Computer name is:", ls_ComputerName)

Setting the Computer Name


Declare a local external function as shown below.

FUNCTION boolean ufx_SetComputerName(ref string &


cname) LIBRARY "kernel32.dll" ALIAS &
FOR "SetComputerNameA"

The following code sets your computer name to the specified one and will come into effect after
you reboot your machine.

// Object: cb_set_computer_name
// Event: Clicked
String ls_ComputerName
ls_ComputerName = sle_ComputerName.Text
if ufx_SetComputerName( ls_ComputerName ) then
MessageBox("SetComputerName", "Successfull! " + &
"You'll need to reboot for it to take effect")
else
MessageBox("SetComputerName", "Failed")
end if

Reading the Value of an Environment Variable


Declare a local external function as shown below.

FUNCTION uLong ufx_GetEnvironmentVariable( String &


lpszName, REF String lpszValue, &
uLong dwcValue) library "Kernel32.dll" &
ALIAS FOR "GetEnvironmentVariableA"

The following code displays the value of the environment variable specified in sle_env
SingleLineEdit control.
// Object: cb_env
// Event: Clicked
String ls_val = Space(1000)
ufx_GetEnvironmentVariable( sle_env.text, ls_Val, 1001)
MessageBox( sle_env.text, ls_val )

Finding the Volume Information


Declare a local external function as shown below.

FUNCTION boolean ufx_GetVolumeInformation( &


ref string lpRootPathName,ref string &
lpVolumeNameBuffer,ulong nVolumeNameSize, &
ref ulong lpVolumeSerialNumber,ref ulong &
lpMaximumComponentLength,ref ulong &
lpFileSystemFlags,ref string &
lpFileSystemNameBuffer,ulong &
nFileSystemNameSize) Library "kernel32.dll" &
ALIAS FOR "GetVolumeInformationA"

The following code displays the volume information of the specified drive. If you specify 'A:', it
will say FAT file system and will show NWCOMPAT when specified a mapped Novell Network
volume/drive.

// Object: cb_display_volume_info
// Event: Clicked
String ls_RootPathName = "c:"
String ls_VolumeNameBuffer = space(256)
String ls_FileSystemNameBuffer = space(256)
ulong lul_VolumeNameSize = 256
ulong lul_MaximumComponentLength = 256
ulong lul_FileSystemNameSize = 256
ulong lul_VolumeSerialNumber
ulong lul_FileSystemFlags
setnull( lul_VolumeSerialNumber )
setnull( lul_FileSystemFlags )
ufx_GetVolumeInformation(ls_RootPathName, &
ls_VolumeNameBuffer, lul_VolumeNameSize,&
lul_VolumeSerialNumber, lul_MaximumComponentLength, &
lul_FileSystemFlags,ls_FileSystemNameBuffer, &
lul_FileSystemNameSize)
if IsNull( ls_RootPathName ) then ls_RootPathName = 'N/A'
if IsNull(ls_VolumeNameBuffer) then &
ls_VolumeNameBuffer = 'N/A'
if IsNull( lul_VolumeNameSize ) then lul_VolumeNameSize = 0
if IsNull( lul_VolumeSerialNumber ) then &
lul_VolumeSerialNumber = 0
if IsNull( lul_MaximumComponentLength ) then &
lul_MaximumComponentLength = 0
if IsNull( lul_FileSystemFlags ) then &
lul_FileSystemFlags = 0
if IsNull( ls_FileSystemNameBuffer ) then &
ls_FileSystemNameBuffer = 'N/A'
if IsNull( lul_FileSystemNameSize ) then &
lul_FileSystemNameSize = 0
MessageBox( "Volume Info", &
"Root Path Name: " + ls_RootPathName + "~n" + &
"Volume Name : " + ls_VolumeNameBuffer + "~n" + &
"Volume Size: " + String(lul_VolumeNameSize) + "~n" + &
"Volume Serial # " + String(lul_VolumeSerialNumber) + &
"~n" + "Max Component Length : " + &
String(lul_MaximumComponentLength) + "~n" + &
"File System Flags : " + String(lul_FileSystemFlags) &
+ "~n" + "File System Name : " + &
String(ls_FileSystemNameBuffer) + "~n" + &
"File System Size : " + String(lul_FileSystemNameSize) )

Writing to the Windows NT Event Log


Declare a local external function as shown below.

FUNCTION long ufx_RegisterEventSource( string &


pszServerName, string pszSourceName ) LIBRARY &
"ADVAPI32.DLL" ALIAS FOR "RegisterEventSourceA"
FUNCTION boolean ufx_DeregisterEventSource( &
long hEventLog ) LIBRARY "ADVAPI32.DLL" ALIAS &
FOR "DeregisterEventSource"
FUNCTION boolean ufx_ReportEvent( long hEventLog, &
integer wType, integer wCategory, long &
dwEventID, long pUserSID, integer wNumStrings, &
long dwDataSize, string pStringArray[], long &
pRawData ) LIBRARY "ADVAPI32.DLL" &
ALIAS FOR "ReportEventA"

Declare an instance string array variable as shown below.

String is_Msg[]

The following code adds message to the instance array variable declared above. The message
you want to log need not be a single line. You can send an array as argument. However, NT
concatenates all those subscripts into one string and logs it.

// Object: cb_addline
// Event: Clicked
int li_MaxSubscript
li_MaxSubscript = Upperbound( is_msg )
li_MaxSubscript++
is_Msg[li_MaxSubscript] = sle_Message.Text

The following code first registers the source, logs the event, un-registers the source and sets
the message array to nothing. You will see the event logged under 'Application' category and it
has no effect on Windows 95 systems. To see the log, select Start > Programs > Administrator
Tools > Event Viewer' and select Log > Applications menu option.

// Object: cb_write_to_nt_event_log
// Event: Clicked
CONSTANT integer EVENTLOG_SUCCESS = 0 // 0X0000
CONSTANT integer EVENTLOG_ERROR_TYPE = 1 // 0x0001
CONSTANT integer EVENTLOG_WARNING_TYPE = 2 // 0x0002
CONSTANT integer EVENTLOG_INFORMATION_TYPE = 4 // 0x0004
CONSTANT integer EVENTLOG_AUDIT_SUCCESS = 8 // 0x0008
CONSTANT integer EVENTLOG_AUDIT_FAILURE = 16 // 0x0010
String ls_Server
Long ll_EventLogHandle
String ls_NothingInIt[]
ll_EventLogHandle = ufx_RegisterEventSource( &
ls_Server, "PMS" )
if ll_EventLogHandle = 0 then
MessageBox( "Error", "Unable to register the source!", &
StopSign!, OK!, 1 )
return 0
end if
if ufx_ReportEvent( ll_EventLogHandle, &
EVENTLOG_INFORMATION_TYPE, &
123, /* category (anything) */ &
0, /* msg file entry */ &
0, /* SID */ &
UpperBound(is_Msg), /* qty strings */ &
0, /* qty raw data */ &
is_Msg, /* strings */ &
0 /* raw data */ ) then
MessageBox( "ReportEvent", &
"Message logged successfully!" )
else
MessageBox( "Error", &
"Unable to write to event log!", StopSign!, OK!, 1 )
end if
ufx_DeregisterEventSource( ll_EventLogHandle )
// Reset the Message array to nothing.
is_Msg[] = ls_NothingInIt[]

Displaying a URL in the Default Browser


Declare a local external function as shown below.

Function long ufx_ShellExecute(ulong hwnd, &


string lpOperation, string lpFile, &
string lpParameters, string lpDirectory, &
long nShowCmd) library "shell32.dll" &
ALIAS FOR "ShellExecuteA"

The following code opens the specified URL in the default browser.

// Object: cb_show_url
// Event: Clicked
String ls_null
SetNull (ls_null)
ufx_ShellExecute(Handle(Parent), ls_null, &
"http://www.sybase.com", ls_null, ls_null, 1)

Finding the System Directory


Declare a local external function as shown below.

FUNCTION ulong ufx_GetWindowsDirectory(ref string &


wdir, ulong buf) LIBRARY "kernel32.dll" &
ALIAS FOR "GetWindowsDirectoryA"

The following code displays the system directory.

// Object: cb_display_system_dir
// Event: Clicked
ulong lul_buf = 144
string ls_WinDir = space( lul_buf )
ufx_GetWindowsDirectory(ls_WinDir, lul_buf)
MessageBox("Windows System Directory", ls_WinDir)

Finding Where the EXE is Running From


Declare a local external function as shown below.

Function uLong ufx_GetModuleFileName( &


Int hinstModule, Ref String lpszPath, &
uLong cchPath) Library "kernel32.dll" &
ALIAS FOR "GetModuleFileNameA"

The following code displays the location of the EXE.

// Object: cb_where_I_am_running_from
// Event: Clicked
String ls_AppPath = Space(255)
int li_Null
SetNull(li_Null)
ufx_GetModuleFileName( li_Null, ls_AppPath, 256)
MessageBox( "Application Path", ls_AppPath )

Copying the Active Window to the Clipboard


Declare a local external function as shown below.

SUBROUTINE ufx_keybd_event( int bVk, int bScan, &


int dwFlags, int dwExtraInfo) LIBRARY &
"user32.dll" ALIAS FOR "keybd_event"

The following code presses the keys for you and releases them as necessary. It depresses ALT
+ Print Screen keys to copy the active to the Windows clipboard and releases them by sending
2 as the third argument. The virtual mapping keys are listed in the Appendix.

// Object: cb_clipboard_window
// Event: Clicked
ufx_keybd_event( 18, 0, 0, 0 ) // ALT key depressed
ufx_keybd_event( 44, 0, 0, 0 ) // PrintScreen key depressed
ufx_keybd_event( 18, 0, 2, 0 ) // ALT key released

ufx_keybd_event( 44, 0, 2, 0 ) // PrintScreen key released

Copying the Full Screen to the Clipboard


You do not need to declare any external function here since the ufx_keybd_event declared in
the previous topic can be used with different arguments. The only difference is that, we are not
calling function to depress and release ALT key, we are using PRINT SCREEN key only.

// Object: cb_clipboard_screen
// Event: Clicked
ufx_keybd_event( 44, 0, 0, 0 )
ufx_keybd_event( 44, 0, 2, 0 )

Browse For the Folder


We have functions to prompt the user to specify a file to open or save. But there is no direct
function call to prompt the user to select a directory. If we need to do it purely in PowerScript,
then we can do it using a ListBox, SingleLineEdit/StaticText control and DirList() function.
However, the following code displays the standard Folder Browser dialog box.

FUNCTION long ufx_BrowseForFolder( REF s_BrowseDirInfo &


lpBi ) LIBRARY "SHELL32.DLL" ALIAS FOR &
"SHBrowseForFolder"
FUNCTION boolean ufx_GetPathFromIDList( ulong pidl, REF &
string pszBuffer ) LIBRARY "shell32.dll" ALIAS &
FOR "SHGetPathFromIDList"
FUNCTION boolean ufx_GetPathFromIDList( s_itemIDList &
ppidl, REF string pszBuffer ) LIBRARY &
"shell32.dll" ALIAS FOR "SHGetPathFromIDList"

Before you start coding, create the following three structure objects in the same order.

// Structure Name: s_BrowseDirInfo


long hwndowner
long pidlroot
string pszdisplayname
string lpsztitle
unsignedinteger ulflags
long lpfn
long lparam
integer iimage
// Structure Name: s_shitemid
unsignedinteger cb
character abID[1]
// Structure Name: s_itemidlist
s_shitemid mkid
// Object: cb_browse_for_folder
// Event: Clicked
// Shell browsing flags
CONSTANT uint BIF_RETURNONLYFSDIRS = 1
CONSTANT uint BIF_DONTGOBELOWDOMAIN = 2
CONSTANT uint BIF_STATUSTEXT = 4
CONSTANT uint BIF_RETURNFSANCESTORS = 8
CONSTANT uint BIF_EDITBOX = 16
CONSTANT uint BIF_VALIDATE = 32
CONSTANT uint BIF_USENEWUI = 0
CONSTANT uint BIF_BROWSEFORCOMPUTER = 4096
CONSTANT uint BIF_BROWSEFORPRINTER = 8192
String ls_null, ls_name, ls_title, ls_path, ls_folder
long ll_pidl, ll_rc, ll_null
Integer li_image
s_BrowseDirInfo lstr_BrowseDirInfo
SetNull( ls_null )
SetNull( ll_null )
lstr_BrowseDirInfo.hWndOwner = Handle( parent )
lstr_BrowseDirInfo.pidlRoot = ll_null
lstr_BrowseDirInfo.pszDisplayName = Space( 256 )
lstr_BrowseDirInfo.lpszTitle = "Select a Folder " + char(0)
lstr_BrowseDirInfo.ulFlags = BIF_RETURNONLYFSDIRS
lstr_BrowseDirInfo.lpfn = ll_null
lstr_BrowseDirInfo.lParam = ll_null
lstr_BrowseDirInfo.iImage = li_image
ll_pidl = ufx_BrowseForFolder( lstr_BrowseDirInfo )
IF ll_pidl > 0 THEN
ls_path = Space( 256 )
IF ufx_GetPathFromIDList( ll_pidl, ls_path ) THEN
ls_folder = ls_path // Returns full path
ls_folder = lstr_BrowseDirInfo.pszDisplayName
// Returns only the directory name.
MessageBox( "Selected Folder", ls_folder )
END IF
END IF

Closing the Current Session And Display Login Screen


Declare a local external function as shown below.

FUNCTION boolean ufx_ExitWindows(uint dwReserved, uint &


uReserved) LIBRARY "User32.dll" ALIAS FOR "ExitWindowsEx"

The following code calls ufx_CopyFile() which is an alias for CopyFileA() function. This function
doesn't display any dialogbox, it simply copies the file. The third argument tells to the function
whether it should overwrite the target file if already exists or not.

// Object: cb_close_session
// Event: Clicked
ufx_ExitWindows(0,0)

Call-Back External Functions


Some of the MS-Windows API functions require call backs. A call back is a pointer to the user-defined
function, to which MS-Windows API functions should return information. This requires a lower-level
language and products like PowerBuilder is not capable of providing pointers to an user defined function.
Basically, you can call MS-Windows API functions or other DLL's from your code, but vice-versa is not
possible.

For a higher level language like PowerScript to interface with MS-Windows API functions, call back is
required. For this, a custom DLL which acts as a wrapper for that API function must be developed. This
wrapper DLL sets up its own routine for the call back and calls the MS-Windows API function for you. This
function should be able to accept parameters from you and give back data returned from the MS-Windows
API function.

Stored Procedures - External Functions


As you learned in the previous sessions, you can execute a database Stored Procedure in two different
ways, from the DataWindow, and from the PowerScript using embedded/dynamic SQL.
You can also execute stored procedures by declaring them as external functions, as long as they don't
return any result set. You can declare a stored procedure as an external function only in the transaction
type user object. In the user object's session, we created uo_transaction user object, which is of
Transaction type. If you open uo_transaction and display popup menu in the 'Local External Functions'
view and select 'Paste Special > SQL > Remote Stored Procedure(s)…' menu option, you will find "Remote
Stored Procedure(s)" button dialog box.

Let's create a stored procedure to find out the number of products in the product_master table.
Invoke the database painter and type the following in the ISQL view and execute it by pressing
CTRL + L keys.

create procedure GetProductCount


(@TotalCount integer output)
as
begin
select @TotalCount=count(*) from product_master
return(0)
end;

Now select this procedure name as described earlier before creating the stored procedure click
OK CommandButton. You will see the syntax declaration something like:

subroutine GetProductCount(ref long TotalCount) &


RPCFUNC ALIAS FOR "~"dba~".~"abc1~""

For the sake of demonstration, open the application painter and type the following code after
the Open(w_login) line:

long l_ProductCount
SQLCA.GetProductCount( l_ProductCount )
MessageBox( "Total No. of Products in the Database", &
l_ProductCount)

Run the application and test it.

While creating the stored procedure, we declared the parameter with an OUTPUT keyword. It
means, the changed value of that variable is available back to the client (PowerBuilder). Please
note that we are executing the stored procedure as if we were calling a function at an object,
using PowerBuilder's dot notation. The following is the syntax for declaring stored procedures
as external functions.

FUNCTION rtndatatype functionname &


({ { REF } datatype1 arg1, ..., { REF } datatypen argn }) &
RPCFUNC { ALIAS FOR "spname" }
SUBROUTINE functionname &
( { { REF } datatype1 arg1,...,{ REF } datatypen argn } ) &
RPCFUNC { ALIAS FOR "spname" }

The '&' symbol is not part of the syntax, however you need to use the line continuation character if the
declaration spans multiple lines. If the stored procedure returns a value, the function (FUNCTION
keyword) is declared, otherwise the subroutine (SUBROUTINE keyword) is declared. A stored procedure
should not return a result set. However, you can use OUT keyword in Watcom database and OUTPUT
keyword in Sybase for parameter declaration.

You must use the RPCFUNC keyword in the function or subroutine declaration, to indicate a remote
procedure call (RPC) for a database Stored Procedure rather than for an external function in a DLL.
Optionally, you can use the ALIAS FOR "spname" expression, to supply the name of the stored procedure
as it appears in the database, if this name differs from the one you want to use in the script.

Cross Platform Issues


If you are targeting an application for multi platforms, make sure that equivalent DLLs are available on
the target platform(s). If you are using operating system specific APIs, you need to write C functions on
other platforms.

If you are targeting the application for MS-windows 16-bit as well as 32-bit operating systems, remember
that some function and DLL names are different in them. For example, for multimedia, you need to call
mciSendString() under 16-bit, but, you need to call mciSendStringA() under 32-bit. MMSystem.dll is the
DLL for the former one where as winmm.dll is the DLL for the later one. To solve this problem, use
inheritance and user objects and virtual functions.

To make this happen, create a base class user object. In this base user object create dummy function
calls with the exact names, argument lists, and the return values you want. For example, for the above
situation, we can create a function, uf_mciSendString. These calls can be dummy, as they will never be
called. Here, the function names, argument lists and return values are important and must be defined
exactly as you need them.

Next, create two class user objects by inheriting from this base object. One for Win 3.1 (say uo_16_bit)
and one for Win NT/95 (say uo_32_bit). In each of these, encapsulate the functions with the same names,
argument lists and return values as its ancestor. Declare local external functions in uo_16_bit, say,
mciSendString(), and similarly, declare local external functions in uo_32_bit, say,
mciSendStringA().Because they are local for each object the definitions can be the same, without any
collision (in fact function names must be same)

At run time, declare a variable of the base (uo_api) user object. Then check the environment by calling
GetEnvironment function and instantiate the appropriate user object's instance. All calls to the code refer
to the variable and thus call the function in the instantiated user object.

Since the variable is of ancestor type, you only get the attributes and methods that are defined for the
ancestor. That is why it is important that you have dummy definitions in the base NVO with the same
name and argument types. At run time, they will be overridden by functions with the same name in the
instantiated descendent NVO and you get the environment functions that you need.

The following example explains this technique. (This following code snippet is from the
example application)

// Get the operating system type. This will be stored


// in a global variable. Examples will reference this
// to determine if they can perform certain
// OS specific functions.
environment le_environment
u_external_function luo_ef
GetEnvironment(le_environment)
Choose case le_environment.ostype
Case Windows!
luo_ef = create uo_16_bit
Case Windowsnt!
luo_ef = create uo_32_bit
Case Else
SetNull(luo_ef)
End Choose
Return luo_ef

Summary
There are hundreds of MS-Windows API calls. We can't show you examples for each one of them. The
following are some references for further information. External-functions.ppt—a PowerPoint presentation
contains full summary of this session.

 The WAITE Group Window API Bible.


 Visual C++ help files.
 Watcom C++ help files.
 Some code examples in the Example application

Debugging, Tracing & Profiling


Often applications don't work as you expect them to. You may come across unexpected loops,
MessageBoxes that don't appear or simply when it refuses to display the correct message. As you write
script for events, you probably will be able to follow the execution flow, as you would in traditional
programming.

However, there may be cases where you lose the thread, especially when using inheritance or multiple
instances of objects. As object oriented programs get complex, and when advanced language features are
used more often, debugging becomes exponentially complicated. When it gets to this level, it becomes
imperative to see how PowerBuilder executes the script.

This session explains program debugging in PowerBuilder and some common situations where you see
'remarkable' behaviors whilst running or debugging an application.

In this session you will learn:

 The debugging environment.


 Some common debugging problems.
 Some advanced debugging techniques.
Estimated Session Time

120+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/ Professional/ Enterprise


version) installed on your computer.

Running Your Application


When still under development, PowerBuilder allows you to run the application in one of the two modes:
Run and Debug. When using the Run mode, PowerBuilder displays the application in full glory, running
scripts when appropriate and displaying dialog boxes under appropriate conditions, allowing you to review
the overall effect.

In the Debug mode, you can add Breakpoints in the script or function at specific lines, which, when the
application is run, causes PowerBuilder to temporarily halt the execution at that point. As the program
executes, you can see how a particular variable changes, at these key points, throughout its lifetime.

Before you attempt to debug the application, it is advisable to save all currently open objects. This
ensures that the Debug is working against the most up-to-date object in the library and allows you to
close down painters you have been working with.

If you have unsaved objects open, then PowerBuilder prompts to do so. If you don't save them, you can't
invoke debugger.

If things appear to go wrong, and doesn't appear to be running smoothly, then it is the time to close down
all painters, save the objects and open the Debugger.

Invoking the Debug Painter


The Debug Painter is one of the flexible tools that PowerBuilder has to offer, combining the ability to
debug the code attached to any event or function, with the ability to dynamically interrogate the variables
the code affects, as the application runs.

When you invoke the Debug Painter, PowerBuilder starts a debugging session for the currently opened
application.
When you call the Debug Painter, you must be prepared to supply the following information:

 The type of object with which the buggy code is associated.


 Whether the code is resident in an event or a function.
 The name of the object you are interested in.
 A combination of related control or object with the appropriate event or
name of the function.
Select the event or function by expanding the object list in the 'Source Browser' view. Once you find the
event/function, then double click on it. PowerBuilder opens that function/event source code in the Source
view. The Debug painter is has several views.

Views in the Debug Painter


Each view displays certain information regarding the current status of the application. A list of the
available views is available under Views menu option. Selecting a view from the Views option opens that
view. If that view is already open in the current layout, it will open another pane with the selected view.

Call Stack View


This view displays the sequence of function calls that brought the execution to the current line of code. If
there is a breakpoint in an event script then, you will just see the location information of the current
breakpoint in the 'Call Stack' view.

Breakpoints View

This view lists the Breakpoints set so far and their status—active or inactive. Please note that, when you
close the debugger, PowerBuilder doesn't clear the breakpoints, you need to clear them manually. In this
view you can disable/enable breakpoints, set new breakpoints or clear breakpoints. Unlike the old
debugger, popup menu is available in all views and the menu options are context sensitive.

Objects In Memory View


This view lists objects present in the memory at any moment of time, and you can expand them and see
the value of each variable. For example, you can expand w_product_master in the following picture and
see the objects in that instance of the window.
Do you remember the classic problem of debugging an object, i.e., finding the value of a local
instance variable in the script, when it is out of focus. An example would be when we call
of_OpenSheet() with the window name as the argument, from the clicked event of every menu
item that needs to open a sheet in the MDI frame.

window lw_sheet
OpenSheet( lw_sheet, aw_window_name, ParentWindow,0,Cascaded! )
Return 0

In of_OpenSheet() we are declaring a variable lw_sheet of type window, and the reference to the opened
sheet is placed in it. Once the function completes execution, the opened window is visible; however, we
don't have any reference to that sheet, since, the local variable goes out of scope as soon the script
completes execution. The problem arises when you want to debug that particular instance. How are you
going to do it? Well, the first thing that crosses the mind is to place a breakpoint in the script of that
window, but, can you refer to the value of another instance of the same window? No. For that, what you
can do with this new debugger is use the 'Objects in Memory' view.

For example, the above picture has three instances of w_product_master and the current line of script is
in the third instance. Expand w_product_master and you see the three instances of the same window.
Expanding the second instance displays the values of that instance.

Source View
This view allows you to browse the source code of the selected script. When PowerBuilder suspends
program execution at a breakpoint, this view automatically displays the source code for the script that has
the breakpoint.

This part of the main debug window allows you to view, but not alter the code under review. Here, you
can inform PowerBuilder the key stages of the code, and the stages where you wish the program to stop,
while running in the Debug mode.

When you identify a problem, close down the debug session before you change the code. Re-open the
appropriate script and make the necessary changes, before running a new debug session, to check out the
changes.

Source Browser View


This view displays an expandable hierarchy of objects in the current application. Expanding to the last
level and double clicking on that level displays the source code in the Source view.

Source History View


This view lists the script names that were displayed in the Source view so far. A good facility. You need
not keep track of the scripts you saw. Selecting the script name displays the source code in the Source
view.

Variables View
This view displays variables categorized into Local, Parent, Instance, Shared and Global, and each
category occupies a tab page each. A variable value can be changed by double clicking on it. If the current
control is a CommandButton's clicked event script and if you want to see the instance variables declared
at the window, which tab do you click? Not the Instance tab. You should look under the Parent tab.

Watch View
This view displays the variables that are put on the watch list. Also you can change the variable's value
from this view.

As you can see, by moving amongst the variable listing, as the program runs, PowerBuilder displays the
changing value of each variable or control. This feature allows you to track the progress of an application,
allowing you to examine the cogs of the machine, rather than the results it produces.

What if you want to track two variables, that are located under different branches in the variables window
at the same time? Hmm, we have a problem. PowerBuilder solves this problem by allowing you to set up a
watch on a variable or property. This watch forgets about other related variables and controls, and just
spends its time focused on one attribute.
By setting watches against key elements of the application, you'll be able to track the progress in a simple
and easy-to-understand manner. To add a variable to the Watch view, display popup menu in the Watch
view and select Insert menu option and provide the variable name.

Breakpoints
When you first approach a code with the intent of using the debugger against it, you need to set up some
Stop Points. If there aren't any selected Stop Points in the application, PowerBuilder throws you straight
into the main debug window.

You can place Stop Points against any line of code simply by double clicking against the line number.

Actually, Stop Points can't be associated to every line in the code. They include commented lines,
variable definitions and blank entries. In fact, Stop Points can be set against lines that actually alter
something, as opposed to improving the readability or some preparatory work for the code to run
successfully.

You can repeat this task as many times as you like, denoting each and every key point in the program as
a point where you would like to stop and review the progress.

Conditional Breakpoints
Sometimes, you may not want to debug a particular line every time that line gets executed, instead you
may want to debug it on certain conditions. If that is the case, you can specify an expression that returns
TRUE/FALSE values in the condition prompt. PowerBuilder suspends the execution when that expression
returns true.

Breakpoint On a Counter Value


Specify a number in the Occurrence prompt, and PowerBuilder increments that number starting from
value 1, whenever that line is executed. Once the counter reaches the specified number, PowerBuilder
suspends the execution, resets that number and starts incrementing again.

Both Condition & Occurrence Breakpoints


You can specify both condition and occurrence. In this case, PowerBuilder increments the counter only
when the expression returns true—not every time it passes the statement, and when the counter reaches
the specified number, PowerBuilder suspends the execution.

Breakpoint On a Variable Change


You can also set a breakpoint based on a variable change. Make sure that the variable is in the scope, i.e.,
local, instance, static and global variables. To set this, start the debugging session and display popup
menu in the Breakpoints view and select Breakpoints menu option. From the displayed dialog box as
shown below, click on Variables tab page, click on New CommandButton and supply the variable name
and click on Apply button.

Some times you may end up in GPF. If that is the case, then you may want to set the variable when it is
in scope instead of setting it before you start the debugging.

Breakpoints in Embedded SQL


Embedded SQL has its own restrictions. For the purpose of Stop Points, PowerBuilder reads all declaration
statements as no-go areas. It means that a SQL declaration statement, which might include cursor and/or
procedure declaration is read the same way an integer declaration is read—no Stop Points allowed.
PowerBuilder also has problems with embedded SQL statements that span multiple lines.

Clearly, due to the way the SQL is structured, it doesn't make sense to stop a program half-way
through the SQL statement. You either need to read the SQL statement or none at all, and so
PowerBuilder won't allow you to place a Stop Point between the first and the last lines of a
multi-line embedded SQL statement. If you want to set such an SQL statement as a Stop Point,
double click on the last line. For example, to set a Stop Point on the following SQL statement,
you would double click on the line that contains the FROM clause, the line that has the SQL
statement terminator:

SELECT "product_master"."product_no",
"product_master"."product_balance"
INTO : lProductNo, :lBalance
FROM "Product_master" ;

Editing Breakpoints
After you set the Stop Points you require in the code, you might like to try out the Edit Stops dialog box:

If you try to open the debug session on some code that already has some Stop Points associated with it,
this dialog provides you with the ability to review and modify any or all of those Stop Points.

Enabling and Disabling Breakpoints


While debugging a program, as you try to narrow down the problem, you may set several Breakpoints. If
you try to run the debug session with all these Breakpoints in place, you might find it time-consuming to
get to the 'interesting' part of the program.

To circumvent this problem, PowerBuilder allows you to temporarily disable the earlier Breakpoints,
removing the need to pass through them. The debugger doesn't pick up on any disabled Breakpoints, only
the enabled ones. PowerBuilder displays the disabled Breakpoints in white circle.

Flow In the Debug Window


There are several options in the Debug Painterbar, which allow you to control the flow of execution and
the things that should be visible in the Debug window:

If there is a PostEvent() function in the script you are debugging, the event called through the
PostEvent function won't be displayed in the debug window, unless a Stop Point is set in the Posted
event.

Step-in
This option is available in the previous versions and was the only choice. Choosing this option executes
the current line and pauses the execution. If the current line is a function or an event firing, you will be
taken into that script. Suppose you wrote a script for the Clicked event of a CommandButton to call a
function and trigger certain events. If you set a stop for the CommandButton's clicked event, you can see
the execution of the function and the triggered event step-by-step, in the debug window. In turn if this
function calls a second function, it's also displayed in the debug window. However, unlike the Java-
debugger where you can see the execution of system classes—classes supplied by SUN Microsystems—you
can't see the source code for the PowerBuilder system classes in the debugger window. Select

'Debug/Step In' option from the menu or click on   icon from the Painterbar.

Step-over
If the current line is a function or if it is firing an event, choose this option to allow you to run the current
line script without entering into the function/event code. Select 'Debug/Step Over' menu option or click

on   icon from the Painterbar.

Step-out
This option lets you run the current script till the end of the script, without stopping at every line. Select

'Debug/Step Out' menu option or click on   icon from the Painterbar.

Running to the Cursor


In the debug session, if you don't want to place a Breakpoint, instead want to run the program till the
cursor, just place the cursor on the line till which you want PowerBuilder to execute the script and click on
'Run to Cursor' icon.
Changing Variable Values
While debugging a program that contains many changing variables, you may want to modify them. For
example, in a loop you may want to increase the value of the counter in order to jump forward in the
program; or if you have a menu selection window which gets input from the user, you may want to
change the input value to test all the options, without running the full program every time.

You can see that this displays the variable name and its current value. You can enter a new value or set
the value to Null.

Just-in-time Debugging
This new just-in-time debugging feature lets you to switch to debug mode, anytime you run the
application in the regular mode—using Run toolbar icon—without terminating the application. To enable
this feature, you need to enable just-in-time debugging option from the 'System Options' dialog box. You
may not find a toolbar icon for this option. Select 'Window/ System Options' menu option and you will see
the dialog box shown in the picture. Just turn on Just in time debugging option.

To switch to the debug mode while running the application, switch to PowerBuilder application using the
Alt + Tab key combination. PowerBuilder will prompt you (see picture below) to choose between
terminate, debug and continue options.

When you switch to the debug mode, you may not find values in the Source, Variables, 'Call Stack' and
'Objects in Memory' views since the context has not been established yet.
When running the application in the regular mode, if a system error occurs while the just-in-time
debugging option is on, PowerBuilder automatically switches to the debug mode.

Debugging Inherited Objects


When debugging inherited objects, script written for the ancestor objects will not be displayed in the
debug window. If you want to see the ancestor script while debugging, you need to set a Breakpoint in the
ancestor script.

If there are several levels of ancestors and if you set Breakpoint at the top level, you will be able to see all
the descendant scripts executing in the debug window.

Solutions To Common Debugging Problems


In general, there are times where the debugger won't solve the program's problems. This section is
dedicated to those problems and some solutions we found. We hope that the following information may
help you avoid these mines, and save you hours of struggling against unexpected PowerBuilder behavior.

In this section, we cover:

 Using the MessageBox() function in the debugging process.


 Problems with KeyDown() and GetObjectAtPointer() when debugging.
 Message Objects.

MessageBox()
Instead of using the step-by-step technique of debugging, you might decide to use the PowerBuilder
MessageBox() function to indicate the passage of some code, together with pertinent information about
the state of some variable or property.

In this case, problem arises only when the variable in display has a NULL value. In this case, PowerBuilder
neglects to display the message box. In the debug window, you can see the MessageBox() being
executed, but it never appears on the screen.

This often happens when dealing with embedded SQL commands. If the column you are selecting isn't
protected by a NOT NULL flag or a default value, you run the risk of acquiring a NULL and falling fowl to
this problem.

You can't perform a check with an equality sign, as shown below:

If Variable = NULL then ...

PowerBuilder supports an IsNull() function to check if the variable in question has a NULL
value. You can also place NULL value checks in the embedded SQL:

SELECT "product_master"."product_description"
INTO :lProductDesc:lDescInd1
FROM "product_master"
WHERE "product_master"."product_no" = :lProductNo ;

In the above example, you can check lDescInd1 for the presence of a NULL value in the lProductDesc
variable. You will be learning more about embedded SQL in the coming session.

KeyDown(), GetObjectAtPointer()
When debugging, using the KeyDown() and GetObjectAtPointer() functions pose a problem. When
debugging, the debug window is the active window and is placed in the front. During step-by-step
execution, the key pressed is sent to the active window i.e., the debug rather than the active window in
the application.

The solution to this problem is to set Stop Points above and below the line containing the KeyDown()
function, and once the first Stop Point above the KeyDown() function is reached, select the Continue icon
instead of the step icon. This allows the pressed key to be passed to the active window in the application,
and the execution stops at the next Stop Point, i.e., after the KeyDown() function.

Do the same with GetObjectAtPointer().

Messages
To pass values between windows, you often use message object. Like KeyDown(), this works well at run-
time. Typically while debugging, you go through the code step-by-step, watching the variables and trying
to figure out what's happening. Unfortunately, by using this method of debugging, you are allowing time
between execution of the lines, a result that it-self produces adverse effects.

Windows is a message-based operating system. This means every application working under Windows
sends and receives messages. When there is time left before you execute the line that checks the value of
the message object, there is a chance that other application messages may override its content. This
leaves the script without the required content of the message object.

At runtime, as there is no intervention in most cases, it works as you would want it to.

To overcome this problem, declare a local or instance variable in the called event (triggered
event) and assign to it the message object value in the first statement of the event itself. For
example:

// CommandButton cb_browse in WindowA


OpenWithParm( WindowB, "String Value" )

In the Open event of WindowB, you would then add the following line of code as the first line:

InstanceStringVariable = Message.StringParm

Closing the Debug Window


You can come out of debugging in two different ways. One, click on the Continue button and wait till the
execution completes, or click on the 'Stop Debugging' icon from the Powerbar.

Remote Debugging
Starting with PowerBuilder 7.0, you can debug your application remotely when you deploy your app into
Jaguar CTS. This is explained in detail in the later sessions since you need to have some kind of
understanding of Jaguar CTS.

Tracing PowerBuilder's Internal Execution


Till now, we have described about using the debugger interactively. If you are interested in the flow of an
application line by line, then PowerBuilder provides a way to do it. With this facility you can log the flow of
an application's execution line by line, but since PowerBuilder logs every line of execution, you will find
that the application executes slowly.
For this, after creating the executable version of the application, supply /PBDEBUG as the command line
argument for the .EXE file in the Properties dialog box. (Creating an executable is not covered yet. May be
you want to do this after that session. For now, go through the concept.)

You have no control over the log file name. It is by default the .EXE file name with the .DBG file extension.
This file is created in the working directory specified in the properties box.

PowerBuilder starts logging the application from the start and continues to do so till the
application exit. You have no control over specifying beginning and end points. A typical trace
file looks like the one shown below:

Executing event script CLICKED for class M_ABOUT,


lib entry M_MDI_MENU
Executing instruction at line 1
Executing event script CREATE for class W_ABOUT,
lib entry W_ABOUT
Executing instruction at line 2
Executing instruction at line 3
Executing instruction at line 4
Executing instruction at line 5
Executing instruction at line 6
Executing instruction at line 7
Executing instruction at line 8
Executing instruction at line 9
End event script CREATE for class W_ABOUT,
lib entry W_ABOUT
Executing event script OPEN for class W_ABOUT,
lib entry W_ABOUT
Executing instruction at line 2
End event script OPEN for class W_ABOUT,
lib entry W_ABOUT
Executing event script UE_MOUSEMOVE for class
UO_COMMANDBUTTON, lib entry UO_COMMANDBUTTON
Executing instruction at line 1
Executing instruction at line 2
End event script UE_MOUSEMOVE for class UO_COMMANDBUTTON,
lib entry UO_COMMANDBUTTON
Executing event script UE_MOUSEMOVE for class UO_COMMANDBUTTON,
lib entry UO_COMMANDBUTTON
Executing instruction at line 1
Executing instruction at line 2
End event script UE_MOUSEMOVE for class UO_COMMANDBUTTON,
lib entry UO_COMMANDBUTTON

You may find that lines appear for which code wasn't written. The reason for this is, as we have already
explained in the previous sessions, every PowerBuilder object is inherited from the base PowerClass
object. There may be certain script in these base objects, which PowerBuilder executes behind the scenes
and whom we never see. When you use the logging feature, PowerBuilder displays them just like the code
written by us. There is no way to open it or to change it or to find out exactly what the code does.

Logging SQL Statement Execution


All previously explained procedures follow the flow of execution and display the variable contents, as the
program executes. However, this doesn't include anything about what is being sent to the connected
database, the time it takes and so on.

To display this information, you can add the word 'trace' before the DBMS name in the database profile:

Remember to remove the 'trace' before distributing the application, as logging database information
seriously affects the performance.
We can see how this works, by logging onto our application as follows:

When you type the password and click on the OK button, you'll get the following message box confirming
that the log file has been created in the Windows directory:

If the log file already exists, new information will be appended to it, so, it gets big very quickly if you don't
keep an eye on it.

PowerBuilder log contains parameters that are used to connect to the database, SQL
statements, and the time taken to connect and execute the SQL statements. The following log
was created when we executed our Product Management Application with a trace flag:

LOGIN: (342 MilliSeconds)


CONNECT TO TRACE ODBC:
USERID=DBA
DATA=product
DBPARM=Connectstring='DSN=product' (0 MilliSeconds)

Immediately after it connects to the database, it starts a transaction. We retrieved item master
details into w_product_master window. When PowerBuilder encounters Retrieve() in the
script, it sends the SQL statement defined in the DataWindow to the connected database, for
parsing:

PREPARE:
SELECT "units"."unit" , "units"."unit_description"
FROM "units" (47 MilliSeconds)
PREPARE:
SELECT "product_master"."product_no",
"product_master"."product_description",
"product_master"."product_balance",
"product_master"."product_reorder_level",
"product_master"."product_measuring_unit"
FROM "product_master"
ORDER BY "product_master"."product_no" ASC (16 MilliSeconds)

Once the SQL statement is parsed, it gets the column descriptions, binds the data to the
DataWindow columns and then asks the connected database to execute the statement:

BIND SELECT OUTPUT BUFFER (DataWindow): (0 MilliSeconds)


,len=4,type=CHAR,pbt1,dbt0,ct0,dec0
,len=12,type=CHAR,pbt1,dbt0,ct0,dec0
EXECUTE: (0 MilliSeconds)
BIND SELECT OUTPUT BUFFER (DataWindow): (0 MilliSeconds)
,len=44,type=LONG,pbt22,dbt0,ct0,dec0
,len=32,type=CHAR,pbt1,dbt0,ct0,dec0
,len=44,type=DECIMAL,pbt4,dbt0,ct0,dec3
,len=44,type=DECIMAL,pbt4,dbt0,ct0,dec3
,len=4,type=CHAR,pbt1,dbt0,ct0,dec0
EXECUTE: (0 MilliSeconds)

It fetches the data row by row, until it knows there are no more rows. The last line, rc 100, is a
SQLCA.SQLCODE, which indicates that there are no more result rows for this command.

FETCH NEXT: (133 MilliSeconds)


COLUMN=1 COLUMN=Hard Disks1 COLUMN=487.000 COLUMN=20.000 COLUMN=U...
…………
FETCH NEXT: (13 MilliSeconds)
Error 1 (rc 100)

When we changed the description of product_no 1 from 'Hard Disk1' to 'Optical Disk' and
selected File/Save from the menu, the following was recorded in the log file:

PREPARE WITH BIND VARIABLES:


UPDATE "product_master"
SET "product_description" = ?
WHERE "product_no" = ? AND
"product_description" = ? AND
"product_balance" = ? AND
"product_reorder_level" = ? AND
"product_measuring_unit" = ? (47 MilliSeconds)
VCHAR Length12 ID:1 *Optical Disk*
LONG Length0 ID:2
VCHAR Length11 ID:3 *Hard Disks1*
DECIMAL Length0 ID:4 *487.000*
DECIMAL Length0 ID:5 *20.000*
VCHAR Length1 ID:6 *U* (0 MilliSeconds)
EXECUTE: (19 MilliSeconds)
GET AFFECTED ROWS: (0 MilliSeconds)
^ 1 Rows Affected

It simply sent the UPDATE statement to the database and recorded the number of rows affected by the
UPDATE statement.
Finally, exiting from the database and disconnecting from the database produces the following:

BEGIN TRANSACTION: (3 MilliSeconds)


COMMIT: (164 MilliSeconds)
DISCONNECT: (1042 MilliSeconds)
SHUTDOWN DATABASE INTERFACE: (191 MilliSeconds)

Open the database painter and remove the TRACE keyword from the DBMS prompt.

Tracing ODBC Driver Manager Calls

It is also possible to get details about ODBC driver's API calls. You need to provide
ConnectOption parameter to the DBParm option. At run-time, you can do as follows:

SQLCA.dbParm="ConnectString='DSN=product',ConnectOption=
'SQL_OPT_TRACE,SQL_OPT_TRACE_ON;SQL_OPT_TRACEFILE,
C:\workdir\prododbc.log'"

Please note that, the above file entries are in a single line. To debug in the development environment, you
need to set the same in the database painter. Invoke the database painter, select File/Connect/Setup and
select "product" option in the dialog box and click Edit button. Type the following for the DbParm prompt:

 
ConnectString='DSN=product',ConnectOption=
'SQL_OPT_TRACE,SQL_OPT_TRACE_ON;SQL_OPT_TRACEFILE,
C:\workdir\prododbc.log'

Again, all the above lines are on one line. Run the application and exit the application. If you open
c:\workdir\prododbc.log file, you will see the ODBC calls PowerBuilder sent to the database.

The following are some of the commands given, when running through a procedure similar to
the one above:

SQLError(henv004E3B7C, hdbc004D5120, hstmt00000000, szSqlState,


pfNativeError, szErrorMsg, 513, pcbErrorMsg);
SQLDriverConnect(hdbc004D542C, hwnd000001A4, "DSN=product", -3,
szConnStrOut, 513, pcbConnStrOut, 1);
SQLGetInfo(hdbc004D542C, 2, rgbInfoValue, 256, pcbInfoValue);
SQLGetInfo(hdbc004D542C, 17, rgbInfoValue, 256, pcbInfoValue);
SQLGetConnectOption(hdbc004D542C, 102, pvParam);
SQLGetInfo(hdbc004D542C, 8, rgbInfoValue, 4, pcbInfoValue);
SQLGetInfo(hdbc004D542C, 43, rgbInfoValue, 4, pcbInfoValue);
SQLAllocStmt(hdbc004D542C, phstmt004D573C);
SQLGetTypeInfo(hstmt004D573C, 0);
SQLGetFunctions(hdbc004D542C, 59, pfExists);
SQLSetStmtOption(hstmt004D573C, 9, 00000010);
SQLBindCol(hstmt004D573C, 1, 1, rgbValue, 130, pcbValue);
SQLBindCol(hstmt004D573C, 2, 5, rgbValue, 2, pcbValue);
SQLExtendedFetch(hstmt004D573C, 1, 1, pcrow, rgfRowStatus);
SQLFreeStmt(hstmt004D573C, 1);
SQLAllocStmt(hdbc004D542C, phstmt004D573C);
SQLTables(hstmt004D573C, "(null)", 0, "dba", 3, "pbcattbl", -3,
"(null)", 0);
SQLFetch(hstmt004D573C);
SQLFreeStmt(hstmt004D573C, 1);
Do you realize how much PowerBuilder works behind the scenes ? Go back to the database
painter, and set back the DbParm option to its original value:

ConnectString='DSN=product'

If you don't do this, PowerBuilder logs all those ODBC API calls, in turn making the application to run
slowly.

Tracing and Performance Analysis


The tracing and profiling feature introduced with PowerBuilder 6.0 allows you to capture your PowerBuilder
application's trace information at run time and analyze the captured information. The data that you can
capture about your PowerBuilder application includes:

 Object creation and destruction


 Routine (event or function) entry/exit
 Routine line hits
 System errors
 Garbage collection
 Embedded SQL
 User defined activities such as error & status messages
Once you collect the trace information in a file, you can analyze the tracing data using either the Profiler
and Trace tools (they were shipped together as a separate apps in v6.0) or objects and functions that
were introduced in PowerBuilder 6.0 for this purpose. Tracing and performance analysis tools objects and
APIs are available for enterprise PowerBuilder edition only.

Collecting Trace Data - An Easy Way


The easiest way to collect tracing data for the entire application is to enable tracing in the 'System
Options' dialog box. If you do not find 'System Options' icon on the Powerbar, select 'Window/System
Options' menu option. You may want to add this icon to the Powerbar by customizing the Powerbar.
 

Turn on all the trace activities you want to capture and specify the trace file. The extension of the trace
file may be anything, however, the default prefix PBP is preferred. The clock timer measures time in
microseconds and the resolution (the smallest unit of time a timer can measure) can be less than one
microsecond depending on the speed of your computer CPU. Thread or process timers measure time in
microseconds with reference to when a threaded or process execution started. This type of measuring is
good in tracing distributed applications. On windows 3.x machines, the selection of timer has no effect
since windows 3.x does not support threading any way. On Unix machines, thread timer is always used
which measures time in nanoseconds.

That's all you need to do. Now, run your application. For the example purpose, we ran
product_management_system, opened product maintenance screen, retrieved data, added a record,
saved the changes & retrieved data gain. Then we closed the window and exited from the application.
 

The generated trace file is stored in binary format that means you can't view the data with any editor
unless you use tracing & profiling APIs. The binary format makes the file compact and gives better
performance. Because of the binary format this file is not portable between different operating systems.

Performance Analysis - An Easy Way


Version 6.x Enterprise PowerBuilder edition comes with a 'Profiler' application along with the source code.
You can use the source code for learning about how to use the tracing & profiling objects and methods,
however, don't do any changes, since they are not supported by Powersoft. However, v7.0 changed this
app and built this into PowerBuilder itself. If you don't see 'Profiling … ' icons under the Tools tab page
when you select File/New menu option, that means you don't have it installed on your machine or your
copy of PowerBuilder is not an enterprise edition.

The Profiler has three views, Class view, Routine View and Trace View. Let's start with the Class View. To
start the Profiler tool, select File/New menu option and double click on 'Profiler Class View' icon located
under Tools tab page.

When you open it for the first time, PowerBuilder prompts you for the trace file and later on it uses that
file by default. Specify the file you provided in the 'System Options' dialog box. If you don't see any error
that means the file is opened successfully. Set your preferences in the 'Preferences' dialog box (as shown
in the picture) by selecting 'Options/Preferences' menu option.
The tracing and profiling APIs & objects depend on the your application's executable file and your
application's PowerBuilder libraries. For example, to display the line numbers and the source code in the
analysis, it needs to read your .PBL, your source code is not stored in the tracing file. Let's see what will
happen if you rename product.pbl to new_product.pbl and launch profiler application? You will get the
error as shown below:

Actually, the error is not explanatory at all. To coninue, make sure you close the 'Profiler xxxxx view' tool
and rename the PowerBuilder library back to product.pbl.

Class View
In this view you can view calls, hits and timing information for each class in the application extracted from
the call graph model. Recall that we added a new record in the product master window and saved
changes, that means it fires the ue_save event. The following picture display statistics about ue_save
event.

Profiler displays <ESQL> class for all embedded SQL statements. From the picture you can see inserting
into the database and committing the transaction took 3.78 milliseconds. Wondering a MessageBox()
function took more time than anything else? Right, nothing we can do about it, since it is a built-in
function, we can't tune that command any way.

Clicking on the 'Graph' tag page will display the above statistics in graphical form as shown in the picture.
Clicking on the 'Source' tab page will display the actual source code executed to create the trace file
including line numbers. You can see the statistics for each line executed.
What if you changed the source code between the trace file generation and analyzing the trace
file in the Profiler application. Well, you will see wrong statistics. To show this in practice, I
inserted the following comments after the line that is starting with "lUserAnswer = " in the
ue_save event in w_product_master window and run the profiler application.

//Testing Trace &


//Profiling API
//Delete These comments

You will see the following picture which says, executing comments took some time (actually comments are
stripped out from the PBL before PBD or executables are made) and also shows, the transaction was never
committed which is wrong, since we find the newly added product in the database. What did you learn
from this? Do the trace file analysis before you do any changes to the source code or keep a copy of your
PBLs if you want to do analysis later.
Routine View
In this view you can view calls, hits and timing information for each routine in the application extracted
from the call graph model.

You can view all calls by a routine, a command/function and all routines that called it and performance
information.

Trace View
In this view you can view calls, hits and timing information for each class in the application extracted from
the call graph model. This picture looks similar to the 'Source' tab page view in the 'Class View', however,
you can see the time taken for the garbage collection also.

Collecting Trace Data using Tracing API


Enabling the tracing from the 'System Options' dialog box is the easiest and fastest way to capture
performance info and analyze it. However, if you need, you can do custom models using the PowerScript
function calls provided for this purpose.

First, you need to open the trace file using TraceOpen() function and TraceEnableActivity() function for
each activity that you want to trace. The possible values for this function are:

 ActError!
 ActESQL!
 ActGarbageCollect!
 ActLine!
 ActRoutine!
 ActObjectCreate!
 ActObjectDestroy!
 ActProfile!
 ActRoutine!
 ActTrace!
Once you enable things you want to trace, then call TraceBegin() function to start tracing. At the end of
the code where you want to stop tracing, call TraceEnd() function to stop tracing. This function does not
close the trace file; You need to call TraceClose() to close the trace file. Once you open the trace file, you
can call TraceBegin(),TraceEnd() functions any number of times to trace different blocks of code. You can
even write your own messages and error messages using TraceUser() and TraceError() functions.

Capturing doesn't take much coding, but analysis needs a lot of coding.

Performance Modelling Objects


Profiling Object Description
Name

Profiling Provides a performance analysis model listing all the routines (both functions and
events) logged in a given trace file.

ProfileCall Provides information about called and calling routine, elapsed time, etc.

ProfileClass Provides information about all the routines in a class.

ProfileRoutine Provides information about a routine. Information includes time spent in the routine,
number of times it is called, etc.

ProfileLine Provides information about a line in the performance model.

Trace Modelling Objects

The following table lists object to model trace data and description of each object.

Trace Object Name Description

TraceFile Used to access the content of a trace file and is used along with other Trace
objects.

TraceTree Provides a tree model listing all the nodes logged in a given trace file. Has
functions to analyze trace file, build trace model and list top-level entries in the
tree model.

TraceTreeNode Contains information about the nodes in the tree model.

TraceTreeError Used to analyze a Tree node of type ActError!. While analyzing, if you find a tree
node with ActError! type, then you need to assign to this object to further
analyze that node.

TraceTreeESQL Used to analyze a Tree node of type ActESQL!. While analyzing, if you find a tree
node with ActESQL! type, then you need to assign to this object to further
analyze that node.
TraceTreeGarbageCollect Used to analyze a Tree node of type ActGarbageCollect!. While analyzing, if you
find a tree node with ActGarbageCollect! type, then you need to assign to this
object to further analyze that node.

TraceTreeLine Used to analyze a Tree node of type ActLine!. While analyzing, if you find a tree
node with ActLine! type, then you need to assign to this object to further analyze
that node.

TraceTreeRoutine Used to analyze a Tree node of type ActRoutine!. While analyzing, if you find a
tree node with ActRoutine! type, then you need to assign to this object to further
analyze that node.

TraceTreeUser Used to analyze a Tree node of type ActUser!. While analyzing, if you find a tree
node with ActUser! type, then you need to assign to this object to further analyze
that node.

Summary
We've shown you how easy it is to navigate around the Debug Painter and to debug the scripts. We've
also seen some of the other methods you can use to log the execution of the application and the database
connections used by it. You should now be able to pick problems easily and quickly.

Dynamic Data Exchange


This session is the first of the two, concerned with using PowerBuilder to communicate with other software
packages. If PowerBuilder can't perform a task, ship the data to another package, and let it use its
functionality to perform the required task and wait for the return of the end result.

For example, suppose you wanted to spell check the textual information in the database. As Powersoft did
not build a spell checker into PowerBuilder, normally this task wouldn't be performed. However, by using
some form of communication between packages, you can use a word-processor to perform this check for
you.

The form of communication we are going to investigate here is called 'Dynamic Data Exchange (DDE)',
one of the first Client/Server ways of exchanging data.

In This Session You Will Learn:

 The concepts of DDE.


 DDE related events in PowerBuilder.
 Communicating with Excel using DDE.
 Mail Merge Using Word and DDE.
Estimated Session Time

150+ minutes

Prerequisites:

 You should have PowerBuilder (Desktop/ Professional/ Enterprise


version) installed on your computer.
Introduction
MS-Windows allows you to run more than one application, and communicate between them on your own
computer at any moment of time. DDE allows you to do this by setting up a conversation between the two
applications. When you start to think about it, the benefits of this are obvious.

Each software package has been designed for some clear, specific purpose. Word-processors are designed
to manipulate text, spreadsheets for number crunching and databases for handling large amounts of
information. What happens if you want an application to retrieve numerical information from a database,
perform some summary statistics on it, graph the results and print out a textual report based on the
information revealed by the statistics and the graphs?

Without the use of some communication technology between these packages, the design of the software
would defeat you. However, with the introduction of DDE, the light appeared at the end of the MIS tunnel.
Okay, DDE can't write the report for you, but it certainly can move the data around to achieve the results
you need!

DDE Concepts
To use DDE, the process demands that at least two applications be involved. The application that requests
data or sends executable commands is called the Client, while the application that executes the requested
command or returns the requested data is called the Server.

Depending on how the DDE is implemented, an application may be able to act both as the client and a
server. PowerBuilder has been developed to support DDE for both.

The Registration Database


Applications that wish to participate in DDE must be registered in the Windows registry database. In this
database, the operating system stores, in the binary format, the information it requires to organize the
conversations. Generally, this registration is done automatically when you install a product, but if you
encounter problems, you can view/edit the application's DDE properties.

To do this, click on the Windows '95 Start button and select Run and type in REGEDIT. This runs
REGEDIT.EXE, bringing up the Registry Editor window.

Unless you understand exactly how the registry works, you can cause your system to lose functionality.
Let's see what entries we have for the DDEExec parameter, for the MS-Word program. Double-click on
HKEY_CLASSES_ROOT/Word.Document6/Protocol/Shell/Open/DDEExec. You will see the value of this
variable in the right hand side window, as shown in the following picture. The value there says, open the
specified file:

The 'Application' and 'Topic' options are used to refer to the application, you want to communicate with.
Let's take a look at each of these entries in detail, while also introducing Item, a subdivision of topic.

Application
The Application is the name under which the server software has been registered in MS-Windows.
Typically, it is the name of the executable file without the .EXE extension, but it may be a different name.

Topic
The nature of the Topic depends on the server. For Word, a 'topic' may be a document name; for Excel, it
may be a name of a spreadsheet. There is also a special topic called 'System', which is supplied by most
applications that support DDE.

The 'System' topic is used when there are several topics available - for example, when you are accessing
data from multiple spreadsheets. The 'System' topic is also used to perform certain tasks at the
application level.

Item
You can see this option only if you are using Windows 3.1. Under a Topic, you may want to access more
than one location. For example, Word could use an 'Item Name' to refer a bookmark, while Excel could
use an 'Item Name' to access a range of rows and cells.

For a DDE conversation to begin, the client application requests a conversation channel to communicate.
This message is broadcast throughout the Windows environment and if the server application is running, a
channel, identified by a numerical return, is assigned and then the conversation can begin.

If the server application isn't running, MS-Windows takes control and sends a message to the client letting
it know that the server isn't running.

The server application must be running before a DDE conversation can begin. Some applications
(when acting as a client) automatically load the server, if it isn't already running. PowerBuilder
doesn't support this feature.

You can specify a time out period for a client, for the channel, by specifying value for the DDETimeout
parameter.

Links
There are two types of DDE links: "Hot" and "Cold". The distinction is made depending on when the data
will be sent to the client from the server.

Hot Link
When operating a hot link DDE conversation, as soon as the data changes in the server application, it is
automatically sent to the client. The client requests the data only once: when it initially establishes a link
to the server.

A hot link is used when you constantly want updated information from the server application.
The following PowerBuilder functions are related to the hot link:

StartHotLink(location, appl_name, topic)


GetDataDDEOrigin(which_appl, what_topic, what_loc)
GetDataDDE(string)
RespondRemote(Boolean)
StopHotLink(location, appl_name, topic)

Cold Link
In a cold link conversation, data is sent from the server to the client only when the client
requests it, rather than whenever the data is changed. This is useful only when the client
doesn't need to be aware of the changed data immediately. However, it is processor intensive
than the hot link because, the client must request data from the server every time it needs. The
following PowerBuilder functions are related to the cold link:

ExecRemote(command, appl_name, topic)


GetRemote(location, target, appl_name, topic)
SetRemote(location, value, appl_name, topic)

DDE Events In PowerBuilder


The table given below lists all DDE related PowerBuilder events. These events are by default, declared at
the window level. This makes sense because, we can think of window as a DDE Topic and the data or
controls on the window as DDE Items.
The table shows all the events together with their corresponding Client actions and
commands used to get the required results:

Client Action Event Commands Used


Client initiates a Hot Link RemoteHotLinkStart StartHotLink()
Client sends a command to the RemoteExec GetCommandDDEOrigin() GetCommandDDE()
server
Client sends data to the server RemoteSend GetDataDDEOrigin() GetDataDDE()
Client requests data from the RemoteRequest GetDataDDEOrigin() SetDataDDE()
server RespondRemote()
Client terminates a Hot Link RemoteHotLinkStop StopServerDDE()

You can observe from the above table, there are hot link related events, but there are no events for cold
link because in cold link the client requests for the latest data, that means the request can be made from
any event. When data is received as the result of cold link that need to be handled in RemoteSend event.

Using DDE With PowerBuilder


Now that we've explained some of the concepts behind DDE, let's learn about coding to use DDE in
PowerBuilder. We will be using Excel and MS-Word in this DDE conversation.

Paint a window w_dde as shown below.

The left control is a DataWindow and all others are CommandButtons. Leave the names to their defaults.
We expect you to paint CommandButtons from top to bottom, the names would be cb_1 to cb_6
respectively.

Create a DataWindow d_dde_demo1 with External data source and Tabular presentation style. Give border
to the columns in DataWindow. The following picture displays the data source definition.
We then assign the DataWindow object to the DataWindow control through script, depending on the
CommandButton pressed.

Note that for all Excel examples, we must have Excel running in the background with the required
spreadsheet open. You can open Excel from PowerBuilder using the Run() command, but you may
experience problems while doing it. It's worth experimenting but, for guaranteed success, we'd
recommend you to have Excel running.

Importing Excel Spreadsheet Using DDE


In this topic, we start a cold link DDE conversation from PowerBuilder (DDE Client) to Excel (DDE Server).
Let PowerBuilder talk to Excel and get the data from Excel Spread sheet and insert it in the DataWindow
dw_1.

Invoke Excel and create a spread sheet "ddetst1.xls", as shown in the picture below. Don't worry about
colors and other fancy stuff, we used colors to make sure that you enter data in the correct place. We
purposely left the first row and column blank, as they normally contain headings and labels. Save the
spread sheet and keep the Excel and spread sheet open.
Write the following code to the Clicked event for the Get Data From Excel CommandButton.

// Object: cb_1(Get Data From Excel)in window w_dde


// Event: Clicked
integer i, j, lExcelChannelNumber, lDDEGetRetVal
string lRetValFromExcel
long lNewRowNo
dw_1.DataObject = "d_dde_demo1"
// Actually you don't need the following one line,
// because we are using external data source.
dw_1.SetTransObject( SQLCA )
lExcelChannelNumber = OpenChannel( "Excel","ddetst1.xls")
For i = 2 to 5
lNewRowNo = dw_1.InsertRow(0)
For j = 2 to 4
lDDEGetRetVal = GetRemote( "R" + string(i) + &
"C" + string(j), lRetValFromExcel,&
lExcelChannelNumber )
dw_1.setitem( lNewRowNo,(j - 1), &
integer( lRetValFromExcel ))
next
next
ExecRemote( '[File.Close()]', lExcelChannelNumber )

Invoke the application painter and comment the code present in the 'Open' event and just
writeOpen( w_dde). Run the application and click Get Data From Excel button. You will see the
spreadsheet data in the DataWindow, as shown in the following picture. Notice that "ddetst1.xls" is closed
in Excel.
Since the same DataWindow object can be associated to many DataWindow controls, our first task would
be to re-assign DataWindow object 'd_dde_demo1' to DataWindow control 'dw_1' and then set up the
default Transaction Object for data exchange.

A channel must be opened between PowerBuilder and Excel - remember that, for a DDE conversation to
take place, a DDE channel must have already been allocated as a carrier. The OpenChannel() function
accepts two parameters; the name of the application with which we want to converse and the specific
Topic that contains the data we are interested in. In our case, the Application is Excel and the Topic is the
name of the appropriate spreadsheet, DDETST1.XLS.

In a commercial application, you would need to add an error handling routine to check if a positive
number channel was returned. If not, notify the user about the problem or keep trying for a certain
number of time, in a loop.

We then use the GetRemote() function, passing the specific cell reference, we are interested in retrieving.
For example, 'R1C1' would refer to row one and column one. Once the required data has been retrieved,
we close the DDETST1.XLS file by sending the Close() function. If you are not familiar with Excel macros
and functions, refer to the Excel documentation.

Note that the formula used to locate the data in the server application's current Topic may differ from
server to server. The 'R1C1' formula is specific to Excel. If you are using any other type of
spreadsheet, or indeed any other type of DDE server, refer to the documentation for that software,
for more information.

Importing a Spreadsheet Using Clipboard


This example uses the same spreadsheet as the previous one and basically achieves the same result. The
difference between the examples is the way in which the data is transferred. In the first example, the data
was pulled straight into the DataWindow from the DDE server, but this example uses Windows clipboard
as a temporary storage point.

An advantage of using this method of data transfer is, once the data is copied to the clipboard on the
orders of one client, any number of clients can access that data. No more server calls are required,
and also, we use only one line of code to insert the spread sheet data in the DataWindow, instead of
the loop in the above example.

To achieve this functionality, we need to record a macro in Excel. A macro records the things being done
in the spread sheet and can be saved with a name, so that it can later be played with the name.
The macro displayed in the above picture is an Excel 4.0 macro. If you are familiar with Excel 5, you might
be familiar with macros written in "Visual Basic for Applications". The VBA version looks different, even
though both do the same.

Switch to Excel and open "ddetst1.xls". Select 'Tools > Macros > Record New Macro' from the menu.

Leave everything to defaults. Now select spread sheet cells as shown in the following picture (R2C2
..R5C4) and select Edit > Copy from the menu (Cell R2C2 is selected, even though it's back ground is
shown as white.). Stop the macro by clicking on the stop button. Save the spread sheet.
Both versions of this macro simply select the required range of cells and copy them to the clipboard. If
you select the Module1 tab page that is located at the bottom of the spread sheet, you will see the macro
definition as shown in the following picture.

Now, switch to PowerBuilder and write the following code to the "Copy Data From Excel
Through ClipBoard" CommandButton.
// Object: cb_2(Copy Data From Excel Through ClipBoard)
// Event: Clicked
integer lExcelChannelNumber
dw_1.DataObject = "d_dde_demo1"
dw_1.SetTransObject( SQLCA )
lExcelChannelNumber = OpenChannel( "Excel","ddetst1.xls")
ExecRemote('[Run("Macro1")]', lExcelChannelNumber )
dw_1.ImportClipboard()
ExecRemote( '[File.Close()]', lExcelChannelNumber )

Save the window and run the application. Click on the Copy Data From Excel Through ClipBoard
CommandButton. You will see the result you saw in the Copy Data From Excel topic. However, this
method has less code in PowerBuilder.

As you can see, the first few lines are similar to the previous example; we specify the DataWindow, set
the Transaction Object and open a channel. This time we are not calling GetData() function, instead we
are asking Excel to run the macro by calling the ExecRemote command with the macro name as an
argument. ImportClipboard() function, which we haven't used till now, imports the data into the
DataWindow control. If the clipboard contains more data than needed, you can specify the data you want
to import as an argument to this function. The last line closes the spreadsheet.

This example has considerable performance improvement over the previous method, as copying cell by
cell involves sending a request to the server for each cell, which requires both time and resources. You
probably wouldn't notice the difference with such a small amount of data, but in a full-blown application, it
would be considerably quicker.

This DDE-based technique is very useful if you want to save a spreadsheet data into a Sybase
database.

The FileClose() function closes the currently active spreadsheet or macro sheet. However, if
you make changes to a file and then attempt to close, Excel will prompt you so as to save the
changes or not. This requires user intervention and therefore halts the flow of our application.
To prevent this, use the following code before the final line:

ExecRemote('[Run("Macro1")]', lExcelChannelNumber )
dw_1.ImportClipboard()
ExecRemote( '[ERROR(FALSE)]', lExcelChannelNumber )
ExecRemote( '[File.Close()]', lExcelChannelNumber )

This automatically answers 'no' to Excel's 'Save Changes?' prompt. By replacing FALSE with TRUE, you
would be asking Excel to save the changes, instead of throwing them away with FALSE.

Using Excel Names


As with all user-friendly applications, there are many ways to perform the same task. Instead of referring
to the cell references explicitly, you can name the range you are interested in and then use that name in
the macro.

It takes three steps to define a name in Excel:

 Select the range of cells (R2C2 to R5C4).


 Select "Insert > Name > Define...".
 Supply a name "ImportDataArea" and click OK button...
Select Tools > Macros from the menu. Select Macro1 and click Edit button. Change the
following line:

Range("B2:D5").Select

to the following:

Application.Goto Reference:="ImportDataArea"

Save the spreadsheet. Now if you run the w_dde window and click on the second CommandButton, you'll
get the same data in the DataWindow control.

You may wonder how beneficial it is to use names - our next example will show you. Open the
DDETST1.XLS spreadsheet again and go to sheet1, which contains the data. Now, insert a row before the
fifth row and add the extra data:
If you run the application now and click on the second CommandButton, the data shown in the
DataWindow control includes the inserted row:

This is an example of how we can take advantage of Excel's relative addressing. When you insert a row in
the spreadsheet, Excel notices that new information has been inserted inside the boundaries of a
predefined data set (denoted by the name criteria) and alters the cell range to take it into account the
new boundary of the data set. This feature contributes to less code changes in PowerBuilder and also less
hard coding of the cell names. If the user defines the name at a different location in the spreadsheet, our
application still works without any code change.

If you see the Name ImportDataArea definition, you'll find that the cell range has changed to reflect the
inserted row.

Things won't run as smoothly as you would expect if you use our code as it stands at the moment. You
may notice that Excel's title bar starts flashing in the background when the data is retrieved. This
indicates that Excel is waiting for a response. If you use the Alt+Tab keys to switch to Excel, you'll see the
problem:
We've added data to the spreadsheet, so, Excel prompts us whether to save the changes to
DDETST1.XLS or not. As we have said earlier, the only way to solve this problem is to add the
[ERROR()] command, as shown below, to our script with the parameter, depending on what we
wish to do with the changes.

ExecRemote('[Run("Macro1")]', lExcelChannelNumber)
dw_1.ImportClipboard()
ExecRemote( '[ERROR(FALSE)]', lExcelChannelNumber )
ExecRemote( '[File.Close()]', lExcelChannelNumber )

Hot Link With Excel


The examples we looked so far used cold links. That means, the client obtains a new set of data only when
it asks for it. On the other hand, in case of a hot link, whenever data changes in the server, the client
application is automatically informed. To organize this type of connection, a client must establish link to
the cells it want to track, but you don't have to worry about opening or using a channel.

The command to start a hot link is StartHotLink(). This function accepts three parameters: 'location of
data', Application and Topic. As an example of how to use this type of data transfer, we'll use the
spreadsheet we used in the previous examples.

First, we have to set up the necessary hot links. This is the code for the "Start Hot Link with
Excel" CommandButton:

// Object: cb_3(Start Hotlink with Excel)


// Event: Clicked
string lRetValFromExcel
StartHotLink( "R2C2","Excel","ddetst1.xls" )
StartHotLink( "R3C2","Excel","ddetst1.xls" )
StartHotLink( "R4C2","Excel","ddetst1.xls" )
StartHotLink( "R5C2","Excel","ddetst1.xls" )
StartHotLink( "R2C3","Excel","ddetst1.xls" )
StartHotLink( "R3C3","Excel","ddetst1.xls" )
StartHotLink( "R4C3","Excel","ddetst1.xls" )
StartHotLink( "R5C3","Excel","ddetst1.xls" )
StartHotLink( "R2C4","Excel","ddetst1.xls" )
StartHotLink( "R3C4","Excel","ddetst1.xls" )
StartHotLink( "R4C4","Excel","ddetst1.xls" )
StartHotLink( "R5C4","Excel","ddetst1.xls" )

The function to stop a hot link is StopHotLink(); its arguments are same as StartHotLink(). You need to
call StopHotLink() for the cells which have hot links established. To optimize an application, start the hot
link when ever required, and stopping it as soon as possible.

This is the code for the Stop Hot Link with Excel CommandButton:

// Object: cb_4(Stop Hotlink with Excel)


// Event: Clicked
StopHotLink( "R2C2","Excel","ddetst1.xls" )
StopHotLink( "R3C2","Excel","ddetst1.xls" )
StopHotLink( "R4C2","Excel","ddetst1.xls" )
StopHotLink( "R5C2","Excel","ddetst1.xls" )
StopHotLink( "R2C3","Excel","ddetst1.xls" )
StopHotLink( "R3C3","Excel","ddetst1.xls" )
StopHotLink( "R4C3","Excel","ddetst1.xls" )
StopHotLink( "R5C3","Excel","ddetst1.xls" )
StopHotLink( "R2C4","Excel","ddetst1.xls" )
StopHotLink( "R3C4","Excel","ddetst1.xls" )
StopHotLink( "R4C4","Excel","ddetst1.xls" )
StopHotLink( "R5C4","Excel","ddetst1.xls" )

When a DDE conversation is running as a hot link, whenever data changes, the server informs the client.
It is the client's responsibility to accept the data or not, and if accepted, how to process it.

Whenever the server talks to the client during a hot link, a HotLinkAlarm event occurs at the
window level. Thus, any code for the client's reaction to a hot link broadcast should be
assigned to this event. One important function that can be very useful for this event is
GetDataDDEOrigin(). This function informs the client of the Application, Topic and location of
data from the server:

// Object: w_dde
// Event: HotLinkAlarm
int lRowNo, lColNo, lTotRows, i, lRowPos, lColPos, lLen
string lApplication, lTopic, lLocation, lDDEDataValue
long lInsertedRow
If dw_1.DataObject <> "d_dde_demo1" Then
dw_1.DataObject = "d_dde_demo1"
dw_1.SetTransObject( SQLCA )
End If
GetDataDDEOrigin( lApplication, lTopic, lLocation )
lRowPos = Pos( lLocation, "R")
lColPos = Pos( lLocation, "C")
lLen = Len(lLocation )
lRowNo = integer( mid( lLocation,(lRowPos + 1), &
(lColPos -(lRowPos +1))))
lColNo = integer( mid( lLocation,(lColPos + 1), &
(lLen - lColPos)))
lRowNo = lRowNo - 1
lColNo = lColNo - 1
lTotRows = dw_1.RowCount()
If lTotRows < lRowNo Then
For i = 1 to( lRowNo - lTotRows ) STEP 1
dw_1.InsertRow(0)
Next
End If
GetDataDDE( lDDEDataValue )
RespondRemote( TRUE )
dw_1.SetItem( lRowNo, lColNo, integer( lDDEDataValue ))

As we described in the DDE Concepts section, GetDataDDEOrigin() function returns three values. First is
the application name (Excel), topic name (ddetst1.xls) and the item location (Row no and Col No for
example, R2C4). From the item (location) we are calculating the row no and the col no. If there aren't
enough rows in the DataWindow, we are inserting rows in the DataWindow. Since the spreadsheet used in
this example has row 1 and column 1 as blank, so, subtract 1 from the row and column number we
received.

GetDataDDEOrigin() doesn't return the actual value received from the client, GetDataDDE() does, and it is
used here to pass the value to a local variable, defined in the PowerBuilder script.

Run the application again and open DDETST1.XLS spreadsheet in Excel. Size the windows so that you can
see what's happening in both Excel and PowerBuilder application. Get the data from Excel by clicking on
the first or second CommandButton.

Data is retrieved from Excel as before and the spreadsheet is closed down. To see hot link in action, we'll
have to open the spreadsheet again. Do this and then click on the "Start Hotlink with Excel"
CommandButton. You won't find anything different, but, in the background hot link has been set up.

To see it work, go to the spreadsheet and change one of the values. When you hit the Enter/tab/arrow
key, the change is reflected back in the DataWindow control.

We set up hot links to watch the first and second columns, so that you can change the values in columns
B, C and D of the spreadsheet, and those changes will be seen in the DataWindow control. To close the
hot links, click on the "Stop Hotlink with Excel" button.

Mail Merge
Very often, you need to send the same letter or memo to more than one person. This can be done using
the Mail Merge, by filling in the personal information into a standard letter. The personal information is
being drawn from the database or from the PowerScript.

Note that you could hard code any extra information needed into PowerScript, but this would be
inflexible. However, you can throw up a dialog box to the user to add that information to the specific
letters.

This task automatically falls under the realms of DDE, when you consider the 'rules of software', illustrated
in a previous section. By default, the personal information will be held in a database, but the standard
letter has to be probably prepared using a word-processor. Moving the personal information from the
database to word-processor is simple, if you use DDE.

Mail Merge is a common business requirement. In the increasing Client/Server environment, it is typical
for the data to be stored in different applications.

In this example, we will use PowerBuilder to send data from a DataWindow to a text file, that our word-
processor Word 6.0 can read, before using a Word macro to import the data and merge it with an existing
document. The logic for this can be seen in the following figure:

We'll also copy a DataWindow Graph object to the clipboard and paste it into the merged document using
the same Word macro.

Create a DataWindow object d_mailmerge_addresses from the DataWindow painter. Use


tabular presentation style and external data source. Use the following definition.

Field Name Datatype Length

FirstName String 20

LastName String 20
Title String 4

JobTitle String 20

Address1 String 20

Address2 String 20

City String 20

State String 10

PostalCode String 10

Country String 20

HomePhone String 10

WorkPhone String 10

When in the design mode, select Rows > Data from the menu and add couple of records and click OK
button. Go to the preview mode and export the DataWindow as "c:\workdir\address.txt" by selecting "Text
With Headers" style. Save and close the DataWindow.

Create one more DataWindow object d_mailmerge_graph with Graph presentation style and SQL SELECT
data source. Select product_description and product_balance from the product_master table. Switch to
the design mode by clicking on the "SQL" icon from the Painterbar. Provide product_description for the
Category prompt and product_balance for the "value axis" and click OK button. Save and close the
DataWindow painter.

Open the w_dde window and declare a local external function.

Function int GetModuleHandle(String App_Name) Library "kernel32.exe"

We use this Windows SDK function to check if the application is running or not. Write the
following code for the "Copy Graph & MailMerge in MS-Word" CommandButton.

// Object: "Copy Graph & MailMerge in MS-Word"


// Event: Clicked
integer lChannelNumber
string lExeFile, lDocFile
dw_1.DataObject = "d_mailmerge_addresses"
dw_1.SetTransObject( SQLCA )
dw_1.SaveAs( "c:\workdir\address.txt", Text!, TRUE )
dw_1.DataObject = "d_mailmerge-graph"
dw_1.SetTransObject( SQLCA )
dw_1.Clipboard( "gr_1" )
lExeFile = "c:\msoffice\winword\winword.exe"
// Replace with actual path in your computer
lDocFile = "c:\workdir\main.doc"
If GetModuleHandle( lExeFile ) < 1 Then
Run( lExeFile + " " + lDocFile )
lChannelNumber = OpenChannel( "winword", lDocFile )
Else
lChannelNumber = OpenChannel( "winword", "System" )
ExecRemote('[FileOpen.Name=~"c:\workdir\main.doc~"]', &
lChannelNumber)
lChannelNumber = OpenChannel( "winword", &
"c:\workdir\main.doc" )
End If
ExecRemote( '[ToolsMacro.name=~"InsertGraphAndMailMerge~",.run]', &
lChannelNumber )
CloseChannel( lChannelNumber )
ExecRemote( '[FileExit,.no]', lChannelNumber )

The SaveAs() function exports data from the d_mailmerge_addresses object as a tab-delimited text file
with headers.

Next, we get the Graph object from d_mailmerge-graph and copy it to the clipboard. The next few lines
simply set up the directory for Word and the name of the document to open.

Note that if you have Word installed in a directory other than C:\MSOFFICE\WINWORD you will have
to alter the line accordingly. You can delete this path reference altogether if the correct reference
appears in the DOS path.

The GetModuleHandle() function is a Windows SDK function, which returns the handle of the specified
application. If the handle is zero, it means that the application isn't running, and so we need to run Word,
open the document and open a channel to it.

If Word is already running, i.e. the handle isn't zero, then we open a channel to the System Topic, to
enable us to send appropriate commands to open the document and channel.

This may be confusing, but, it makes sense. Remember, you can't start a DDE conversation until you
have opened a channel. This means that we can't send a FileOpen command until we start a
conversation - in this case with the System Topic. If Word isn't running, we can open the document
straight away using the Run command and allocate a channel to it, in one go.

The command to run the macro is different from the one sent to Excel in the previous examples, simply
because of the differences between Excel and Word macros.

Mail Merge Setup In MS-Word


Invoke MS-Word and create a new document. Select Tools > MailMerge from the menu.
In the dialog box, select 'Form Letters' under the 'Main Document' heading by clicking on the Create
button. Select 'Open DataSource' from the 'Data Source' heading and select "c:\workdir\address.txt" file.

MS-Word displays a prompt as shown in the above picture. Now, Select 'Edit Main Document' option.
Place the mail merge fields in the document by selecting from the popup menu of 'Insert Field' button.
Insert a frame in the document. Make a book mark "GraphHolder" for the frame by selecting 'Edit >
BookMark' from the menu. Now, we need to define a macro we want to run from PowerBuilder to do the
actual mail merging.

Select 'Tools > Macro' from the menu. Type InsertGraphAndMailMerge and select Record


option. Click OK for the next prompt. Now, MS-Word displays a small window with Stop and

Pause icons  . Stop recording the macro. Now, select Tools > Macro from the menu.
Select "InsertGraphAndMailMerge" and click on Edit button. Type the following code.

Sub InsertGraphAndMailMerge
EditGoTo .Destination = "GraphHolder"
EditPaste
MailMergeToDoc
FileSaveAs .Name = "C:\workdir\mail-out.doc", .Format = 0,
.LockAnnot = 0, .Password = ""
DocClose
EditGoTo .Destination = "GraphHolder"
CharRight 2, 1
EditClear
FileSave
DocClose
FileExit
End Sub
Save the document as "c:\workdir\main.doc" and exit word. Open MS-Word again and open
"c:\workdir\main.doc".

This macro moves to the bookmark called GraphHolder, which we've defined in the document, and pastes
the graph at that position and then merges the data in address.txt into the document. It then saves the
merged document as c:\workdir\mail-out.doc. Finally it tidies up the existing document by deleting the
graph and clearing all merged text for the next use.

Running the Example


Run the application and click on the 'Copy Graph & Mail Merge in MS-Word' CommandButton. Note that
you don't have to have Word running in the background for this.

You'll see data flash in the DataWindow control and then the graph appears. Word opens in the
background and you'll see data coming in, windows being opened and closed, as the macro does its stuff.

You can see the final result by opening the c:\workdir\mail-out.doc document.

You can see the first record and the graph inserted in the correct place. If you scroll down the document,
you'll see that the next page contains data for the second record.

Our example, though complex to set up, was fairly easy to execute. As we said before, you could very
easily alter the code to give more functionality.

PowerBuilder As a DDE Server


The previous examples have used PowerBuilder as a client in the DDE conversation. Our final example
shows how to use PowerBuilder as the server and extract information from it with Word.

For this example, we'll use a different window and another Word macro to initiate the conversation.
There's quite a bit of setting up necessary, so once again, we'll go through the various components.

Invoke MS-Word and create a new document PBSERVER.DOC. We need to record a macro
TalkToPBServer by selecting Tools > Macro.… Once you record that empty macro, edit the
macro and replace the macro definition with the following code:

Sub MAIN
channel = DDEInitiate("PBApp", "DDEServerModule")
DDEPoke channel, "3", "Testing data,(DDE from Word)"
a$ = DDERequest$(channel,
"product_no,product_description,product_balance")
Insert a$
DDETerminate channel
End Sub

There are four DDE specific commands:

 The DDEInitiate command takes two parameters: Application and the


Topic, and initiates the conversation.
 The DDEPoke command sends values to the application.
 The DDERequest$ gets information from the application.
 The DDETerminate stops the conversation.
An Insert command simply inserts the retrieved data into the Word document. The data deals with
product information, so we want to retrieve the product_no, product_description and product_balance
fields from the database.

Paint a window w_dde_server as shown below. The top one is the DataWindow control dw_1. Assign
d_products_maint DataWindow object to dw_1. Other controls are CommandButtons. Leave the names to
their defaults. Change the window title to "PB as DDE Server".

Write the following code in the w_dde_server's open event.

dw_1.SetTransObject( SQLCA )
dw_1.Retrieve()

Write the following code to the clicked event for the "Start PowerBuilder DDE Server"
CommandButton.

integer lRetVal
lRetVal = StartServerDDE( "PBApp", "DDEServerModule")
If lRetVal <> 1 Then
MessageBox( "Starting DDE server Error", lRetVal )
End If

Even before we can use PowerBuilder as a DDE server, we have to set up an Application and Topic. The
code in the clicked event does this.

We use the StartServerDDE() function to set up the Application and Topic. When PowerBuilder executes
this command, it informs the operating system, "I am ready to serve as a DDE server. My (Server) name
is <First Parameter> and clients can ask data from the <second parameter>".

When PowerBuilder acts as a server, it is necessary to check the commands sent by the client and trigger
events explicitly. For this, we have to write code for three window level events:

 RemoteExec
 RemoteSend
 RemoteRequest
Let's look at the code for them and see what they do.

The RemoteExec Event


When PowerBuilder receives a request from a client, a RemoteExec event occurs. We use this
event to determine which client has sent the commands:

// Object: w_dde_server
// Event: Remoteexec
String lApplication
int lRetVal
lRetVal = GetCommandDDEOrigin( lApplication )
If lRetVal = 1 then
RespondRemote( TRUE )
End if

The GetCommandDDEOrigin() function determines the client, and if it is successful, we set


RespondRemote to TRUE. In this example, we aren't doing any checks for the application name. Add an IF
statement to check it.

The RemoteSend Event


This event occurs whenever the client sends data to PowerBuilder. In the Word macro, we used
the DDEPoke command, so we have to add code here to handle it.

String lCommand, lApplication, lTopic, lItem, lData, lDWArg


int lRetVal
Long lRowNoFound
lRetVal = GetDataDDEOrigin( lApplication, lTopic, lItem )
If lRetVal = 1 Then
GetDataDDE( lData )
lDWArg = "product_no = " + lItem
lRowNoFound = dw_1.Find( lDWArg, 0, dw_1.RowCount())
If lRowNoFound > 0 Then
dw_1.SetItem(lRowNoFound,"product_description",lData)
RespondRemote( TRUE )
Else
RespondRemote( FALSE )
End If
End If

Just to remind you, the DDEPoke command was:

 
DDEPoke channel, "3", "Testing data,(DDE from Word)"

The second parameter is the Item we are interested in and the third parameter is the data we want to
send. The translation would be 'Find the item with product_no 3 and replace the product_description with
"Testing data, (DDE from Word)''.

The GetDataDDE() function strips the value of the third parameter and places it in a local variable, to be
ready when we successfully locate the Item. We then simply replace the current Title with the data in the
local variable.

The RemoteRequest Event


This event occurs whenever a client requests data from PowerBuilder. PowerBuilder doesn't
have easy referable data locations, such as the cells in a spreadsheet, so, we have to customize
the data that is requested in the DDERequest command from the client:
String lApplication, ltopic, litem, lDataToclient
long lTotRows, i
int lRetVal
lRetVal = GetDataDDEOrigin( lApplication, lTopic, lItem )
lItem = Upper( lItem )
lTotRows = dw_1.RowCount()
For i = 1 to lTotRows
If Pos( lItem, "PRODUCT_NO" ) > 0 Then
If i = 1 Then
lDataToclient = lDataToclient + string( &
dw_1.GetItemNumber(i,"product_no"))
Else
lDataToclient = lDataToclient + char(5) + &
string( dw_1.GetItemNumber(i,"product_no"))
End If
End If
If Pos( lItem, "PRODUCT_DESCRIPTION" ) > 0 Then
lDataToclient = lDataToclient + "~t" + &
dw_1.GetItemString(i,"product_description")
End If
If Pos( lItem, "PRODUCT_BALANCE" ) > 0 Then
lDataToclient = lDataToclient + "~t" + &
String(dw_1.GetItemNumber(i,"product_balance"))
End If
Next
SetDataDDE( lDataToclient )
RespondRemote( TRUE )

We simply loop through the data in the DataWindow control using the GetItemNumber() or
GetItemString() functions, to retrieve the required data and add it to lDataToClient.

The ~t is a special character that is used to place a tab between each entry - this is the manual creation
of a tab-delimited list. When we've gone through all the data, we send lDataToClient to the client using
the SetDataDDE command.

Replace Open(w_dde) with Open(w_dde_server) in the application's open event. Run the application. Click
on "PB As Server" button.

Now open up the PBSERVER.DOC document and run the TalkToPBServer macro. You should see the
following information inserted into the Word document:
You can see that the description of product number 3 has been changed to display the test message, but
was it changed in PowerBuilder? Switch back to the w_dde_server window and you'll see that the value in
the DataWindow control has also been changed:

The value in the DataWindow object hasn't been changed, just the display in the DataWindow control.
However, as in the last example, it would be easy to extend the code to retrieve and update the actual
information in a database.

DDE can be temperamental, so, if you do experience problems getting any of the examples to work,
don't despair, try experimenting with different commands or the logic used in other examples. The
first step in finding problems is to check the references to file names used in the code and macros to
ensure that they correspond to your set up.

Summary
In this session, we've seen the theory and concepts behind DDE, and we've looked at few examples, which
illustrated how PowerBuilder can be used both as DDE client and server.
Whilst DDE is a useful technology. At times it can be complex, as you may have seen in some examples.
In the next session, we'll look at another method of inter-process communication; Object Linking and
Embedding (OLE). We'll look at the concept behind this fairly new technology and again provide you with
some examples to illustrate just how easy it is to use OLE compared to the old standard of DDE.

For a complete summary of this session, browse 'dde.ppt' , a PowerPoint presentation.

OLE — Part I
The new communication standard that everyone is interested in is called Object Linking and Embedding.
By using OLE, you are working at the object level to share data by either physically embedding it into a
working document or simply by providing the working document with the appropriate links to the source
file.

This methodology transcends software boundaries, allowing you to pass information between different
applications without the need for a third party file format that both the source and the target understand.

Objectives

 The concepts behind OLE


 The differences between linking and embedding
 Difference between DDE and OLE
 OLE Control attributes
 OLE Server menus
 OLE Automation

Introduction
The theory behind OLE is based upon one application using the functionality provided by another. Objects
are linked to or embedded in your application and when activated, the application that originally created
the alien object is used to handle any interactions that the user wishes to perform.

The application that has the object embedded or linked into it is called the OLE Client or OLE Container
Object, while the application that created the embedded object is called OLE Server. OLE Client application
provides space to place the object to display, while OLE Server application provides the functionality, i.e.,
methods to manipulate the data created by it.
For example, let us take a write document that has a PaintBrush picture placed in it. In this example,
Write is OLE Client which provides space to place the OLE Object and displays it on the screen and
PaintBrush is the OLE Server which actually created the bitmap and knows how to manipulate the bitmap
file.

An application can be either an OLE Container or an OLE Server, or indeed both.

What Is Linking And Embedding?


When an object is incorporated into a document, it maintains an association with the object application
that created it. Linking and embedding are two different ways to associate objects in a compound
document with their object applications. The differences between linking and embedding lie in how and
where the actual source data that comprises the object is stored; this in turn affects the object's
portability, its methods of activation, and the size of the compound document.

Linking
When an object is linked, the source data, or link source, continues to physically resides wherever it was
initially created, either at another point within the document or within a different document altogether.
Only a reference, or link, to the object and appropriate visual representation of the data is kept with the
compound document.

Linking is efficient and keeps the size of the compound document small. Users may choose to link when
the source object is owned or maintained by someone else because a single instance of the object's data
can serve many documents. Changes made to the source object are automatically reflected in any
compound documents that have a link to the object. From the user's point of view, a linked object appears
to be wholly contained within the document.
One-Way Links
One-Way Links start with a source document sharing its data with the destination document and when the
data in the source document changes, the destination automatically changes. A One-Way Link can have
multiple destination documents. For example, the company logo could be linked into a spreadsheet, the
spreadsheet data linked into a word processor, the word processor data linked into a presentation. Any
time the company logo changes, all points along the One-Way Link will get automatically updated.

Two-Way Links
Two-Way Links with a source document sharing its data with the destination document and the destination
document sharing that data (object) back to another location within the source document. An example of
this might be where data originates in the word processor. This data might be marketing forecast data, for
example. This data is linked to a spreadsheet where a chart is created based on the linked data. The chart
is then linked back into the source document right below the forecast data. Once this Two-Way Link is
established, a what-if analysis can be performed on the original source data such that if the data changes
in the source, it is updated in the spreadsheet. The chart is then updated and finally, the chart is updated
in the source document. So the ability to share data in a destination document and receive linked data
back into the source document is a Two-Way Link.

In addition to simple links, it is possible to get arbitrarily complex by nesting links and combining linked
and embedded objects.

Adaptable Links
OLE provides the mechanism for objects to be linked in a variety of ways and have those links adapt to
changing situations. An object may be linked to a complete or partial object, referred to as a pseudo
object. An example of a pseudo object is a range of spreadsheet cells. The source of a link may reside
inside either the same compound document as the linked object or in a separate compound document.
Both the compound documents containing the linked object and the link source may be stored in a
standard disk file or in a file managed by the OLE storage system.

If the linked object is copied to a new location while the link source stays in place, the link remains intact
and the linked object points correctly to the source. Similarly, if both the compound documents containing
the linked object and the link source are moved to a new location in the same relative path, the link
remains intact.

Embedding
With an embedded object, a copy of the original object is physically stored in the compound document as
is all of the information needed to manage the object. As a result, the object becomes a physical part of
the document. A compound document containing embedded objects will be larger than one containing the
same objects as links. However, embedding offers several advantages that may outweigh the
disadvantages of the extra storage overhead. For example, compound documents with embedded objects
may be transferred to another computer and be edited there. The new user of the document need not
know where the original data resides since a copy of the objects' source (native) data travels with the
compound document.

Embedded objects can be edited in-place; that is, all maintenance to the object can be done without ever
leaving the compound document. Since each user has a copy of the object's source data, changes made to
an embedded object by one user will not effect other compound documents containing an embedding of
the same original object. However, if there are links to this object, changes to it will be reflected in each
document containing a link.

Linking Or Embedding
There are several things to consider when deciding between linking or embedding an object:
 Will anyone else have access to data contained in objects outside of your
application?
 Is the data contained in your objects of a static nature?
 Is the size of your application important?
 Is there a chance of someone moving files containing objects?
 Is speed important?

The following table highlights some of the differences between linking and embedding
when considering these questions:

Linking Embedding

If objects are changed outside your application, then the An embedded object can only be edited
object in your application will be updated automatically. from within your application.

Using linked objects will keep the size of your application Embedded objects are stored within your
down, as the objects are not stored within your application. application, so the file size is bigger.

When you use linked objects, your application contains the This isn't a problem with embedded objects.
reference to the linked file, so if the file is moved, the link will
be severed.

Linked objects are only loaded into your application when they Start-up speed will be slower because the
are required, so start-up speed should be quicker. However, file is bigger, but work with the embedded
working with a linked object reduces the speed of your objects is quicker because the object
application, because the container has to link to the file. already exists in your application.

Linked objects can't be activated in-place, for editing. Only off- In-Place and Off-site activation is allowed.
site activation is allowed.

Traditional Model, Component Object Model


When creating compound documents, whether you use OLE or a monolithic application, the results can
appear to be the same from a high-level view. But if you look underneath, they are really quite different.

Traditional Model
As software vendors tried to add more functionality to their software programs, EXEs got very large. Take
the example of a word processing program that does baseline word processing and more features need to
be added such as graphing, equation editors, spreadsheet functionality, voice annotation, and more.
Typically a developer would add this functionality to the base word processor, thus making the executable
quite large. Another example is a presentation program that offers baseline presentation functionality and
more features need to be added, such as graphing, equation editors, spreadsheet functionality, voice
annotation, and more. Again, typically a developer would add this functionality to the base presentation
program, thus making it's executable large as well.
In the above figure, both the WP.EXE and Present.exe contain similar functionality. As an end-user, if you
know how to use drawing tools in the word processor, you would hope that it might be similar to the
drawing tools in the presentation package, but this may not be the case. If two different developers work
on the drawing tools and don't share the code, the User Interface (UI) might be totally different and
furthermore, since the functionality is built into each executable, the hard drive space requirements will go
up and you will have multiple tools installed that basically perform the same functionality.

There is clearly a better model, the Component Object Model (COM).

Component Object Model (COM)


The Component Object Model takes some of the core functionality out of the executables and make them
stand-alone objects that can be called from any application that supports OLE.

Above figure shows, the added functionality that was discussed in the Traditional Model is now broken out
into separate objects. The advantages are numerous. First, if you need to add charts into a container
document, the same charting engine will be used and the same User Interface is applied, thus reducing
the learning curve significantly. Secondly, each baseline executable will be smaller and the separate
objects will not be duplicated on the hard disk, just simply installed in a common place so they can be
commonly called from each baseline application. Lastly, this promotes the notion of "best of breed"
objects, meaning that if you don't like the drawing tools, simply install a better set of drawing tools and
the newly installed drawing tools will be available in each application.

OLE Features
The following sections provide an overview of each of the OLE features from the user's perspective.

Visual Editing
With visual editing, the user can double-click an object in a compound document and interact with the
object right there, without switching to a different application window. The menus, toolbars, palettes, and
other controls necessary to interact with the object temporarily replace the existing menus and controls of
the active window. In effect, the object application appears to "take over" the compound document
window. When the user returns to the compound document application, its menus and controls are
restored.

Visual editing represents a fundamental change in the way users interact with personal computers. It
offers a more "document-centric" approach by allowing the user to focus primarily on the creation and
manipulation of information rather than on the operation of the environment and its applications. This
approach allows users to work within a single context - the compound document.

Visual editing can include a variety of operations depending on the capabilities of the object. Embedded
objects can be edited, played, displayed, and recorded in-place. Linked objects can be activated in place
for operations such as playback and display, but they cannot be edited in-place. When a linked object is
opened for editing, the object application is activated in a separate window. To return to the compound
document, the user must either close the object application or switch windows.

The following figure shows what happens to an application when the user decides to activate an embedded
object in-place for editing. In the top window, the embedded document appears well integrated into the
PowerBuilder application window. However, when the user double-clicks the document to start in-place
editing, the PowerBuilder application undergoes several changes as is demonstrated in the bottom window
in the above figure.

The title bar changes to reflect the name of the object application; that is, the word processor program.
The menu is the result of the PowerBuilder application and the word processor applications merging their
individual menus. The word processor changes in appearance; hatch marks surround its border to indicate
its activated state. In the above picture, you can also see the red underlines in the text which is the result
of MS-Word automatic spell-checking. When the user has finished editing and clicks outside the
document's border, the window once again looks like it belongs to a PowerBuilder application.

Drag And Drop


The most widely used method for transferring data between applications has been the Clipboard. With the
Clipboard method, the user chooses the Copy operation while in the object application, moves to the
target application, and chooses Paste to put the object in place. Although effective, a more natural way to
exchange data between applications is simply to click an object, drag it to its destination, and drop it in
place. OLE supports this "drag and drop" functionality of objects in addition to the traditional Clipboard
functionality.

Drag and drop eliminates the traditional barriers between applications. Instead of perceiving window
frames as walls surrounding data, users are able to freely drag information to and from a variety of
applications. Drag and drop makes compound documents easier to create and manage because it provides
an interactive model that more closely resembles how people interact with physical objects.

 Inter-window dragging: Dragging data is from one application window


into another application window, is called inter-window dragging.
 Inter-object dragging: Objects nested within other objects can be
dragged out of their containing objects to another window or to another
container object. Conversely, objects can be dragged to other objects and
dropped inside them.
 Dropping over icons: Objects can be dragged over the desktop to system
resource icons such as printers and mailboxes. The appropriate action
will be taken with the object, depending on the type of resource the icon
represents.

Optimized Object Storage


Objects remain on disk until needed and are not loaded into memory each time the container application
is opened. OLE also supports a transaction type storage system, providing the user with the ability to
commit or rollback any changes that they make to the object. This ensures that data integrity is
maintained as objects are stored in the file system.

Nested Object Support


Objects can be linked to or embedded in another object (or even part of an object) in the same compound
document. For example, a graph showing sales figures for the month of January may be linked to the part
of an embedded spreadsheet which contains January's figures. A change in the spreadsheet figures will
automatically affect the linked graph. Nested object linking provides for more efficient use of memory.
Users can directly manipulate the nested object. There is no need to launch multiple applications to arrive
at the object to be edited.
Above picture illustrates Microsoft PowerPoint with a Word 6.0 document embedded in it. Word 6.0
document has a Spreadsheet object and Spreadsheet graph embeds in it. Nested object support gives
users additional freedom to manipulate objects in limitless combinations and work with compound
documents more productively.

Automation
Automation refers to the ability of an application to define a set of properties and commands and make
them accessible to other applications to enable programmability. OLE provides a mechanism through
which this access is achieved. Automation increases application interoperability without the need for
human intervention.

The main purpose of this can be best described in an example. Let's say that you have some data in a
database you would like to perform a regression analysis. Typically, the database developer would have to
create a function or sub-routine that contains the regression analysis formula and then pass the data to
the function. The development of this function could take a considerable amount of time. In fact, why
create a routine when spreadsheets do regression analysis quite well? So, the better approach would be to
"pass" the data to the spreadsheet program, "instruct" the spreadsheet program to perform the regression
analysis on the data, and "pass" back the results. This allows a macro developer to open up entirely whole
new worlds. In fact, you could even start to consider a spreadsheet program as one large compilation of
subroutines and functions that are easily callable and accessible.

This was previously achieved through a mechanism called Dynamic Data Exchange or DDE. DDE would
give application macro developers the ability to control one application from another, but the methods to
achieve this were not trivial. DDE required that a significant amount of time be devoted to error-handling
and only a limited amount of data could be automatically passed from one application to another at one
time. Consider the example of performing a regression analysis on data in a database. The following steps
quickly map out what commands would be necessary if DDE calls were made:

Establish a connection from the controlling application (source) to the application (destination) that will be
controlled.

 Test to make sure that the destination application can receive the
commands and is not busy.
 From the source application, tell the destination application what is
trying to be accomplished.
 Test to make sure the destination application can handle what is trying to
be accomplished.
 Take one data point at a time from the source application and pass it to
the destination.
 Test each data point to ensure that the destination received it and that the
connection is still live.
 Once all the data points have been sent to the destination, instruct the
destination to perform the regression analysis on the entire data set (this
is even another command).
 Prepare the source to now receive the result of the regression analysis
and pass the result from the destination to the source.
 Test to ensure that the source received the result.
 Close the connection.
 This example appears quite simplistic, but from a DDE programming
standpoint this would likely take around 60 lines of code.
With OLE and OLE Automation, the process to achieve this is significantly easier. Applications written to
support OLE expose properties and commands that can be controlled from other applications. The public
exposure of these properties and commands allows one application to contain code that manipulates
another application. It also allows developers to create new applications that can interact with existing
applications.

 Using OLE automation in the above example, we will see how the same
results can be achieved with minimum effort:
 Establish a connection from the controlling application (source) to the
application (destination) that will be controlled.
 Define the entire data set in the source as an object and pass the object to
the destination.
 Instruct the destination to perform the regression analysis on the object.
 Instruct the destination application to return the result of the regression
analysis to the source application.

Version Management
In OLE, objects can contain information about the application that created them, including the name of the
application and the version of that application. This allows applications to know more about the objects
they are dealing with, and allows them to handle objects based on the applications or versions of
applications that created them.

For example, when an object created by version 1.0 of application X is embedded into a document that is
moved to a system having only version 2.0 of that same application and a user tries to edit the object,
version 2.0 of the application can determine that the object is from an older version and can take any
action it desires. It can, for example, prompt the user to upgrade the object to a new format.

Objects Defined
Objects can be almost any type of information, including text, bitmap images, vector graphics, and even
voice annotation and video clips. The objects themselves must be created from an application that
supports OLE 1.0 or 2.0. Once data is copied to the clipboard from the OLE application, the data is now
considered as an object (data with intelligence) such that when you paste it into a container document
(this must also support OLE 1.0 or 2.0), the object "knows" what source application created it and what it
should do if a user double-clicks on the object.

OLE associates two major types of data with an object: visual representation data and native data. An
object's visual representation data is information needed to render the object on a display device, while its
native data is all the information needed for an application to edit the object. The visual representation
will typically always be present, but the native data depends on what method of OLE was used: linking or
embedding. In linking, as stated above, you will see the data (visual representation), but you may not be
able to do anything with the data unless the source document is available. In embedding, you will also see
the data (visual representation), but the native data will activate the object's service assuming that the
application that created the object is available.

Why Implement OLE?


OLE provides great benefits to both users and developers. OLE is the foundation of a new model of
computing, which is more "document-centric" and less "application-centric." Users can focus on the data
needed to create their compound documents rather than on the applications responsible for the data. As
the user focuses on a particular data object, the tools needed to interact with that object become available
directly within the document. Data objects can be transferred within and across documents without the
loss of functionality. Documents can share data so that one copy of an object can serve many users. Users
can therefore be more productive and manipulate information in a manner that is much more intuitive.

The use of OLE objects and interfaces provides developers with the tools they need to create flexible
applications that can be easily maintained and enhanced. OLE applications can specialize in one area,
taking advantage of features implemented in other OLE applications to increase their usability.

Since all interfaces are derived from one base interface, applications can learn at runtime about the
capabilities of other applications that relate to object services. Through this dynamic binding, an
application need not know anything about the objects it will use at run time. Support for additional objects
and interfaces can be added without affecting either the current application or those applications that
interact with it. Because the use of interfaces allows applications to access objects as black boxes,
changes to an object's member functions do not affect the code accessing them.

The extendibility that OLE provides will continue to benefit the developer as the computing environment
moves more toward an object-oriented design. The OLE architecture provides a first step in presenting
applications as a collection of independently installable components.

Object Linking and Embedding (OLE) is a mechanism that allows applications to inter-operate more
effectively, thereby allowing users to work more productively. End users of OLE container applications
create and manage compound documents. Compound documents in this context do not necessarily refer
to word processing documents. A compound document could be any container of objects such as a
database containing word processing document (object) stored in the database, or a presentation
containing financial forecasts which is an object from a spreadsheet.

Therefore, a compound document refers to a container holding objects that were created using another
application. These are the documents that seamlessly incorporate data, or objects, of different formats.
Sound clips, spreadsheets, text, and bitmaps are some examples of objects commonly found in compound
documents. Each object is created and maintained by its object application, but through OLE, the services
of the different object applications are integrated. End users feel as if a single application, with all the
functionality of each of the object applications, is being used. End users of OLE applications don't need to
be concerned with managing and switching between the various object applications; they focus solely on
the compound document and the task being performed.
DDE Versus OLE
OLE and DDE allow you to perform similar actions. Both enable you to send commands to another
application, perform actions in that application, and return data to your PowerBuilder application. There
are, however, some fundamental differences between OLE and DDE.

OLE is currently implemented using the DDE protocol. Applications using OLE are not aware that DDE is
being used, nor should they rely on this, because the implementation mechanism will not be there in
future releases of the OLE components.

To illustrate some of the differences, consider a PowerBuilder application you've designed in which the
user has access to a particular Microsoft Excel worksheet. Your PowerBuilder application contains the
worksheet. In OLE operations, program control is actually temporarily transferred to Microsoft Excel for
the purpose of manipulating the worksheet data. With DDE, operations occur when PowerBuilder sends a
command to Microsoft Excel to start communication between the two applications. PowerBuilder, however,
always has program control.

Another difference, which is an advantage of OLE, is that OLE automatically starts the object application
when program control is transferred to the object application. When you use DDE, you must check to see
if the source application (the DDE server) is started, and start it if necessary.

In addition, with OLE the data always is displayed in a bound or unbound object frame as it appears in the
application that created the object. For example, if the object application is Microsoft Excel, a bound or
unbound object frame displays worksheet data in your PowerBuilder application (the container application)
as it appears in Microsoft Excel itself. DDE doesn't allow you to view the worksheet as it appears in the
application.

Lastly, with OLE you can also allow the user to edit data in another application (activate that application)
when the user double-clicks on the bound or unbound object frame containing the OLE object. DDE
doesn't provide this feature because DDE can only activate other applications.

In versions of OLE prior to version 2.0, it was difficult to access linked or embedded data using
PowerScript. Well, PowerBuilder implemented OLE in Window object from OLE only. For example, if you
created a linked object from a Microsoft Excel spreadsheet, you could allow the user to edit the
spreadsheet's data, but it was difficult for you to access that data using PowerScript. With the advent of
OLE Automation, its easy to access and manipulate data in an OLE object, as long as the application that
supplies the object also supports OLE Automation. If you want programmatic access to data in an
application that doesn't support OLE Automation (Paintbrush), you may still prefer to use DDE.

Which One To Choose, DDE Or OLE ?


Determining a need for DDE and/or OLE support is perhaps less clear. DDE provides an excellent method
of importing live data to an application or exporting live data from it. If two applications want to share and
manipulate a piece of data without the direct involvement of the user, DDE is the choice.

If an application wants to include facilities to show data types such as Microsoft Excel graphs or sound files
provided by OLE servers, OLE client support should be added. If an application has data-rendering
capability useful to other applications, it should become an OLE server.

Let us consider the following example: The live data comes to the application A, has the capability of
managing communications and receiving and displaying the data. Application B has the capability of
plotting the data into variety of graphs. Application D has the capability of recording audio and playing
back. Application C is the word processor.

The customer requirement is to produce a document that gives information about the company, with a
graph for the recent stock market data and chairman's speech in audio format. The purpose of this
example is to help illustrate how to choose the most appropriate type of communication support to include
in an application.

In the case here, we begin with a live data feed attached to a communications port, convert it to a stream
of DDE data, and finally end up with a linked OLE object.

Deciding Where To Use DDE Or OLE


Does it make sense to add OLE support to the Application A as either a client or a server? Not really. We
have data that we can provide to another application, but the application could easily render it for itself,
so we wouldn't need to be an OLE server. We have no need to incorporate other data in our display, so we
don't need to be an OLE client either.

You might say that we could be an OLE server and display the data, but what purpose would this serve? If
we didn't provide the data via DDE as well, the client application would only be able to show an image of
the data and not access it in any other way. That means that it couldn't plot a graph, set trigger alarms,
or whatever because an OLE client treats the data as a black box and has no knowledge of its format.

The Graph application need to be at least DDE client application, to accept data from the Application A.
You might add DDE Server support, but, it is not required; this is because, for plotting the graph, it needs
the data from other applications, but, it has no need of sending the data. Making this as OLE Server
makes sense, because other applications can embed or link to this graph. Since the graph is in a special
format, other application can invoke this application whenever it needs to be manipulated. Similarly sound
recording application also should be at least OLE Server enabled.

Word-Processor application doesn't have much use for live, changing data that it needs to manipulate, so
we aren't really interested in becoming a DDE client. Making this application into an OLE client makes a lot
of sense. It enables other data types to be attached to the text being displayed, i.e., attaching a graph of
our stock market prices and a sound recording of the CEO. We don't need to be able to render the images
of these data types because that's the responsibility of the OLE server. We also get a free editor for our
graph because the OLE server provides that support.

We could also add OLE server support for the word-processor. That way, the entire document could be
linked to or embedded in another application document.
Registration Database
The Registration Database is a source of information regarding applications and their OLE and association
capabilities. This information is used in three cases:

 by applications that support object linking and embedding


 when you open or print a file from File Manager
 for associating file name extensions with applications.
The registration database is set up and maintained by Windows and Windows applications. The database
"REG.DAT" is located in the Windows directory (Under Windows 16-bit version). This file should not be
moved or deleted. If the file is moved or deleted, loss of functionality in File Manager, Program Manager,
and applications that support object linking and embedding, may occur. The Registration Database is
modified by running the Registration Editor, REGEDIT.EXE.

The Advanced Interface Of REGEDIT


The advanced interface of REGEDIT is accessed by running REGEDIT with the /V option (REGEDIT /V). The
advanced interface is designed to be used by application developers and system administrators, who
modify the Registration Database to manipulate the OLE and Association properties in Windows.

When you run the Registration Information Editor with the /V option, the advanced Registration Editor
screen is shown with all of the information in the database. The contents of the Registration database are
branched in a tree format like the directory tree in Explorer. Each branch of the tree is identified by its key
name which is Window's internal name for the program.

What Can We Do With RegEdit?


RegEdit contains the commands that are available to our application when dealing with OLE objects. For
instance, let's look at a sample of the registration editor (Under Windows 16-bit):
We can see from the above picture, that SoundRec has a class id. An OLE Object is identified uniquely by
its class id which is 128-bit long. You will learn more about class ids in "registering PowerBuilder OLE
controls in the registration database" in "Distributing OLE Objects". The SoundRec OLE server (which plays
sounds) has two possible actions (called VERBs in Windows): Play and Edit. You will learn about verbs in
"OLE Objects" topic.

Whenever you install software, those packages will automatically updates the registry database. Some of
the packages allows you not to update the registry and write all those updates to a file. In this case you
can merge the update file for that particular software with the registry by using 'S' option, i.e., REGEDIT
-S FileName.
OLE - PowerBuilder Implementation
From version 4.0 onwards PowerBuilder supports OLE. With version 5.0, PowerBuilder supports OLE in the
DataWindows also. The following are the places where you can use OLE in PowerBuilder.

 OLE Control in the window


 Database OLE Object in the DataWindow
 OLE Object in the DataWindow (New in v5.0)
 OLE Presentation Style in the DataWindow (New in v5.0)
 OCX Control in the DataWindow (New in v5.0)
 Using OLE Object, OLE Storage, OLE Stream data types in the script
 Creating OLE Controls (New in v5.0)
New let's move on one by one.

OLE Control
OLE is best integrated in PowerBuilder through OLE in the window painter. This control allows you to
embed or link an OLE object and gives OLE container support. All OLE clients support container
functionality. The container basically provides some space to place the OLE Server object and invokes the
OLE Server for editing the object, either in-place (OLE servers, except for linked objects) or Off-site(OLE
1.0 Servers). OLE client knows nothing about the content of the OLE object and is like a black box for it;
That's why OLE client invokes the OLE server for data manipulation, since the OLE Server knows how to
manipulate its own data.
Once you place the OLE control in the window it will prompt for the OLE server name. You can either
create a new object or create from an existing file. When you create a new object, it will be embedded in
the PowerBuilder window and saved as part of the window definition. We don't recommend this method,
since, it will lead to long time to open the window, either at the development time or run-time. Other
problem is that, when you export this window and import the window definition back either into the same
PBL or other PBL, you will loose the content of the OLE control. We do recommend to select cancel while
selecting the object type and create the control, and select cancel again when prompted for the object
type; this will create the control in the window. Basically it will appear as empty. This makes the window
opening very faster.

When you create from the existing file, you will have the option to embed or link. In this case,
PowerBuilder decides the server type depending on the information available in the object.

OLE Control Attributes


To see an OLE control's attributes, double-click on it in the development environment:

These options allow you to control how the OLE object behaves and how the user interacts with it at run-
time. Let's take a look at the interesting ones.

Contents
This attribute defines the content of the OLE control. This attribute only applies to
circumstances where the content is dynamically allocated at run-time using the
InsertObject() function. There are three possibilities:

Option Description

Any: Allows linked or embedded objects to be inserted

Embedded: Allows only embedded objects to be inserted

Linked: Allows only linked objects to be inserted

This allows you to assign an OLE object to the OLE control by clicking on the Add OLE Object button.
Doing this brings up the standard Insert Object dialog box, allowing you to select the type of object you
want to add.

If you select the Create from File option, you'll get the usual screen except that the link option isn't
available.

This is because we've specified that the contents should be embedded. If you close the window, change
the Contents attribute of the OLE control to linked and run the window again, clicking on the Add OLE
Object button will then launch the select file dialog with the Link option already selected:

The Contents attribute is generally used to specify a default behavior as it can still be overwritten at
run-time.

Display Type
This is used to specify whether you want to see the actual contents of the OLE object or an icon
representing the server application, in the OLE container application. When you activate the OLE server
application, the server application is invoked and allows you editing. When you comeback to the container
application, either the icon or content would be displayed depending on the selected 'Display type'.

Activation
This allows you to specify how the OLE Server application is activated. The default method is
Double Click, but you can change it to Get Focus or Manual. If you specify Manual, then you
have to take care of activating the server programmatically.

result = ole_1.Activate(Offsite!)

Activate is the function you need to call to invoke the OLE server. The Offsite! enumerated variable opens
the server in its own window. You can choose to have the server open in its own window or in the same
window as your PowerBuilder application, by specifying appropriate argument to Activate function. You
can invoke the OLE server in-place only when the content is embedded. While placing the OLE control, if
you choose either 'Create From File' without selecting 'Link' option or 'Create New', the content is
embedded. Linked objects can be edited only offsite always. When you choose 'Link' while painting the
OLE control and call the Activate() with InPlace! argument, PowerBuilder simply ignores the argument and
invokes the OLE server off-site. When OLE Server invokes for in-place editing, the existing PowerBuilder
application menus and OLE server menus would merge. This is explained in detail in the next section 'OLE
Server Menus'.

Link Update
This option is applicable only for 'linked' types. If you choose 'Automatic', the content in the OLE container
will be updated automatically whenever the content of the linked object changes. If you choose 'Manual',
you need to update the link in the script by calling UpdateLinksDialog().

OLE Control Attributes


The following table lists all important OLE Control attributes:

Attribute Name Data type Description Allowed Values


 
DocFileName String The name of the OLE storage file or
a data file of the server application  
that has been opened for the
control. Using either InsertFile(),
Open() (opening the file, i.e., using
the first format) doesn't populate
this attribute. This attribute would
be populated only when you open
the object from the sub-storage of
the storage file. Ex:
ole_control.Open( StorageVariableN
ame, "SubStorageName")
 
ClassLongName String The long name for the server
associated with the OLE control. Ex:  
"Microsoft Word Document". Either
Open() format 1, or InsertFile()
doesn't populate this attribute. This
is populated when you use Open()
other formats, InsertClass() and
InsertObject().
 
ClassShortName String The short name for the server
associated with the OLE control. Ex:  
"Document". Other behaviour is
similar to "ClassLongName".
 
LinkItem String The file name that is linked with the
OLE control.  
 
ObjectData BLOB This attribute is populated when the
object is embedded. The object it  
self is stored as BLOB in this
attribute. This attribute is useful
when you want to read from the
database and load the OLE control
and/or to update the database
column.

Activation OmActivation Specifies the type of OLE Object ActivateOnDoubleClick!,


activation. ActivateOnGetFocus!,
ActivateManually!
DisplayType OmDisplayType Specifies whether to display the OLE DisplayAsIcon!,
Object as an icon or the content of DisplayAsContent!
the OLE Object.

ContentsAllowed OmContentsAllowed Speicifies whether the OLE Object ContainsAny!,


can embedded in the OLE control or ContainsEmbeddedOnly!,
linked. ContainsLinkedOnly!

LinkUpdatreOptions OmLinkUpdateOptions Specifies whether the OLE Server LinkUpdateAutomatically!,


automatically updates the linked LinkUpdatemanually!
object or you update
programatically.
 
IsDragTarget Boolean Specifies whether another OLE
Control can be dropped on this  
control or not. Don't confuse with
other Drag and Drop attributes that
are available for other controls.
 
Object OmObject Stores the link information.
 

Opening a File In a OLE Control


This function comes in lot of flavors. Only the first format is discussed here and other topics
are explained in the "OLE Storages and Streams" session. Open opens the specified file in the
OLE control. The following example opens an OLE file, however you can open any OLE server
file such as a word document, excel spread sheet, etc. and save as a OLE file. All the examples
shown in this section are available in the w_ole_20 window in the ole2.pbl library.

All the code in the library uses a directory called "c:\workdir". Make sure you have the
directory on your computer and unzip the zip file you download into the "c:\workdir" directory.
You can run the window and test the functionality, since all the support files are also available
with this.

Open the application "object_embedding_n_linking-2_0" from "ole2.pbl" library.


Run the application. Select 'Module > OLE Control Examples' menu option. Click on
the Open Doc CommandButton.

String lFileNameWithFullPath, lFileName


Integer lResult
lResult = GetFileOpenName( "Select Document File " + &
"to Open in OLE Control", lFileNameWithFullPath, &
lFileName, "DOC", "DOC Files (*.doc), *.doc" )
If lResult <> 1 Then Return
lResult = ole_1.Open( lFileNameWithFullPath )
If lResult <> 0 Then
MessageBox( "Error while opening the " + &
specified File", "Error No: " + String( lResult ))
Return -1000
End If
We are asking the user to select the file to open by calling GetFileOpenName() function. We
are then opening that document in the OLE control using Open() function.

Opening a Sub-Storage
Another flavor of Open() allows you open a sub-storage into the OLE control. OLE supports
storages and sub-storages. In simple terms, Storages are like DOS file structures. From the
operating file structure, the storage file looks like a single file, however, you can have lot other
sub-storages within a storage, even though you can't see from the operating system. To open
a sub-storage in the OLE control, first we need to open the storage file into a variable of type
OLEStorage and you can use that variable as the argument for the Open() for the OLE control.

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Click on the 'Open SubStorage' CommandButton.

The following example, opens the storage file OLE_TEST.OLE into the ole storage file
iOLEStorage. The next line opens OLE_TEST.XLS which is stored in the storage file
OLE_STORAGE.OLE into the OLE control. More on storages, sub-storages and
streams in the next session.

Integer lResult, I
lResult = iOLEStorage.Open( "c:\workdir\ole_test.ole" )
lResult = ole_1.Open( iOLEStorage, "ole_test.xls" )
If lResult <> 0 Then
MessageBox( "File Open Error", lResult )
Destroy iOLEStorage
Return -1
Else
MessageBox( "Open", "Opened Successfully. " + &
"Double-click on the OLE control " + &
" to edit the Excel spread sheet" )
End If

Insert Class In the OLE Control


This function creates a new object of the specified class and embeds in the control.
After inserting the class, if you activate the OLE server, it will activate appropriate
OLE server depending on the class you specified in this function. The following
example inserts a word document class and also opens another file. After inserting
the class, when you open any file that belongs to the specified class in the OLE
control, you will see OLE control empty, even though it opened successfully. By
calling InsertClass function, the OLE class is set to the specified class and
subsequent OLE server activation invokes the right server (MS Word in this
example). You might ask, where can I find these classes. You can find them in the
Registration database.

String lFileNameWithFullPath, lFileName


Integer lResult
lResult = GetFileOpenName( "Select Document to " + &
"Open in OLE Control", lFileNameWithFullPath, &
lFileName, "Doc", "Documents (*.doc), *.doc" )
If lResult <> 1 Then Return
lResult = ole_1.InsertClass( "word.document" )
If lResult <> 0 Then
MessageBox( "Error while inserting Class", &
"Error No: " + String( lResult ) )
Return -1000
End If
lResult = ole_1.Open( lFileNameWithFullPath )
If lResult <> 0 Then
MessageBox( "Error while opening the specified" + &
"Class File", "Error No: " + String( lResult ))
Return -1000
End If

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Click on the Insert Class CommandButton.

Inserting an Existing File In the OLE Control


Calling the InsertFile() embeds a copy of the existing object into the control. The
following example inserts the specified object.

String lFileNameWithFullPath, lFileName


Integer lResult
lResult = GetFileOpenName( "Select File to Open " + &
"in OLE Control", lFileNameWithFullPath,lFileName)
If lResult = 0 Then Return
lResult = ole_1.InsertFile( lFileNameWithFullPath )
If lResult <> 0 Then
MessageBox( "File Insert Error", lResult )
Return -1000
End If
ole_1.Activate( InPlace! )

Activate() function activates the inserted file, i.e., invokes the OLE Server (The program that
created the file).

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Click on the Insert File CommandButton.

Inserting an Object In the OLE Control


Calling InsertObject() prompts allows you to choose an object and allows to either
to create from a new object or from an existing object. If you choose to create from
create from the existing file, you can also link to that file. If the "content " attribute
is set to "Any" then only you are allowed to link the existing object. If the content
type is set to "Linked", you choices are narrowed down to linking to the existing
object.

ole_1.InsertObject()

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Click on the Insert Object CommandButton.

Saving the Object From the OLE Control


Calling the Save() saves the content of the OLE control to the opened file. If you
open the file by using Open() format 1 or InsertObject() functions, file is saved to
the opened file. If the file is opened from the OLE storage (explained in the next
session), it will be saved to the OLE storage.

ole_1.Save()

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Insert a file by clicking on the Insert File CommandButton and click on the Save
CommandButton.

Saving the OLE Control as Another File


Calling SaveAs() allows you to save the file to the specified file. You can either save
to a file or a storage or sub-storage. This function also comes in different flavors.

String lFileNameWithFullPath, lFileName


Integer lResult
lResult = GetFileSaveName( &
"Select OLE File to save OLE Control Content", &
lFileNameWithFullPath, lFileName, "OLE", &
"OLE Files (*.ole), *.ole" )
If lResult <> 0 Then Return
ole_1.SaveAs( lFileNameWithFullPath )

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Open a file in the OLE Control by clicking on the Open Doc CommandButton and click
on the Save As CommandButton.

Linking the OLE Control's Object To a File


Calling LinkTo() allows you to link the OLE object to another object.

String lFileNameWithFullPath, lFileName


Integer lResult
lResult = GetFileOpenName( "Select Document to " + &
"link from OLE Control", + &
lFileNameWithFullPath, lFileName, "Doc", &
"Documents (*.doc), *.doc" )
If lResult <> 1 Then Return
lResult = ole_1.LinkTo( lFileNameWithFullPath )
If lResult <> 0 Then
MessageBox( "File Link Error", lResult )
Return -1000
End If

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Open a OLE Storage file in the OLE Control by clicking on the Open SubStorage
CommandButton and click on the Link CommandButton.

Getting Image From the Database To OLE Control


OLE Control has an attribute ObjectData. When an object is embedded in the control,
the actual content of the object is stored in this attribute. We can assign the value of
this attribute (object content) to a BLOB (Binary Large Object) variable and update
the database or write to a file. The following example is using "easdemodb.db" that
comes along with the PowerBuilder software for example application. It reads the
"screen" column from the "examples_previews" table and loads in the OLE control.

SELECTBLOB "examples_previews"."screen"
into :iImageBlob
from "examples_previews"
where "examples_previews"."window"= 'w_dialer' ;
If SQLCA.SQLCode <> 0 Then
MessageBox( "Error", SQLCA.uf_GetError() )
Return -1
End If
ole_1.ObjectData = iImageBlob

PowerBuilder has a datatype BLOB which means Binary Large Object. If the database contains
binary objects such as sound files, image files, we can't retrieve in PowerBuilder in the normal
way. We need to use the BLOB type variable. We have declared a BLOB type variable
"iImageBlob" as an instance variable and we are retrieving the "screen" column into this
variable. Since we are using embedded SELECT statement, we have to make sure the SELECT
statement returns one row only. We have only one row for the "w_dialer". The SELECT
statement is also not a regular SELECT statement, it is SELECTBLOB statement.

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Click on the Open Database Object CommandButton.

Updating the Database BLOB Column From OLE Control


Similar to populating the object in the OLE Control, we can also update the database
column once user updates the object in the OLE control. The following code updates
"screen" column in the examples_previews table in the database.

Blob lBlob
Integer lResult
lBlob = ole_1.ObjectData
SqlCa.AutoCommit = True
UPDATEBLOB "examples_previews"
set "examples_previews"."screen" = :lBlob
where "examples_previews"."window" = 'w_dialer';
If SqlCa.SqlCode <> 0 or SQLCA.SQLNRows = 0 Then
Rollback using Sqlca ;
MessageBox( "Error", Sqlca.uf_GetError())
Return -1
Else
Commit using Sqlca ;
End If

Similar to the SELECTBLOB, we are calling UPDATEBLOB, not a regular UPDATE database
statement.

To see it in the action, run the application. Select 'Module > OLE Control Examples' menu
option. Click on the Save to Database Using BLOB CommandButton.
Copying the OLE Control
This function copies the content of the OLE control to the clipboard. Remember, if
you are using a SinleLine/MultiLine edit control and use this function, it copies only
the selected text to the clipboard. But, the same function here copies the entire
content of the OLE control to the clipboard. For example, a word document is placed
in this control and you selected first three lines of the document. Calling this
function copies entire document to the clipboard, not the only first three lines.

Ole_control1.Copy()

Cutting the OLE Control Object


The behavior of this function is similar to Copy(). This function not only copies the
content to the clipboard, but also clears the OLE Control. The syntax is:

OLEControlName.Cut()

Pasting an Object In the OLE Control


This function pastes into the control from the clipboard. If you have place some text
and call this function, it will not paste into the control. It will paste if you are pasting
into the edit control, but, not in the OLE control. To paste in the OLE control by
calling this function, the content on the clipboard should be a OLE object. The syntax
is:

OLEControlName.Paste()

PasteSpecial()
It presents a dialog box to select the format of the content to paste. The syntax is:

OLEControlName.PasteSpecial()

PasteLink()
Pastes the link to the content in the clipboard to the OLE control. The syntax is:

ole_control2.PasteLink()
Editing/Printing the Object In the OLE Control
Executes the verb that is supported by the server. The verbs supported by the OLE
server you can find out from two sources. One from the OLE Server documentation
and other is from the Registration database. For example, Paintbrush supports one
verb, that is "Edit". DoVerb() takes a number as the argument. Generally argument 1
is for "Edit". For example, you placed a bitmap in the OLE control and called:

OLEControlName.DoVerb(1)

This will invoke PaintBrush in its own window. Calling this function with 1 as argument is equal
to activating the OLE Server, however, there is a small difference. When you activate the
server with this function, the OLE Server always activates off site, even though the server
supports OLE (For example: Word 6.0). This function is basically intended for OLE 1.0 servers.

There are few more functions related to the OLE control. These are explained in detail in the
advanced section of OLE, i.e., next session. Now let us see how the menus affect when we
activate the OLE Server in-place.

In-Place Editing & OLE Server Menus


When you invoke the OLE Server to edit the OLE object that is placed in the PowerBuilder
window, OLE Server will either invoke in-place or in its window. When the document is
embedded, OLE Server starts in its window. When this happens, OLE Server will display its
own menu in its own window. When the OLE Object is embedded, OLE Server invokes in the
PowerBuilder application window, of course, the OLE Server should support OLE. When OLE
Server starts, it will display its menu and toolbar by replacing the PowerBuilder application's
menu and toolbar by default.

PowerBuilder gives some control on PowerBuilder menus, i.e., whether to display or not, if you
decide to display, then how do you adjust them with the OLE Server menu items. These
options you can set in the menu painter. If you select "Style" tab, when the cursor is on the
menu bar items(You can't set these for the menu items, which makes sense), you can see few
options in the 'Menu Merge Options' DropDownListBox.
The In Place attribute is set only for menu bar items, not menu items. Some useful
options are:

Option Description

Exclude: When the menu bar item is set to Exclude, it will not be displayed in the resulting
menu.

Merge: The menu bar item will be merged with the server application menu.

File: This will display the menu bar item in the place occupied by the "File" menu option,
that is, the first option on the menu.

Window: This will display the menu bar item in the place usually occupied by the "Window"
menu option, that is, the second option from the right.

Help: This will display the menu bar item in the place usually occupied by the "Help" menu
option.

Edit: This will display the menu bar item in the place usually occupied by the "Edit" menu
option.

To see this working, we've included several windows in the OLE2.PBL library, which contain an
OLE control containing an embedded Word 6.0 document.

Now let's see how to use OLE combining with the DataWindow.

OLE Columns In the DataWindow


Data types that store large amount of data can't be used in the DataWindow in the same way
you use traditional data types such as string, integer, etc.. These special data types are called
BLOB (Binary/Text Large Objects) and are used to store large amount text like sample session
of a book, image, audio, video, etc. PowerBuilder handles these BLOB data types in a different
way, in the DataWindow as well as in embedded SQL. In case of embedded SQL you can use
SELECTBLOB and UPDATEBLOB statements. In case of DataWindows, you need use "OLE
database Blob" columns and use OLE technology. In product.db database, we have a column
of 'long binary' data type, product_image column in the product_images table. Let us see how
to select data from this column to display in the DataWindow and how to update this column.

Invoke the DataWindow painter and select 'SQL Select' data source and Tabular presentation
style. Select product_images table. Select product_no column, you are not allowed to select
product_image column and if you try you will see an error message as shown in the following
picture, since you have to handle BLOB data types differently. Return to the design mode.

Select 'Objects > OLE Database BLOB' from the menu and place in the detail band next to
window column. You will be prompted for the BLOB definition as shown in the following
picture. You will find a different dialog box, but don't worry about it. Just concentrate on the
values.

You can leave the Class Name, Client Name to the defaults. These two values are used by
some OLE servers to display in the OLE Server windows. If the primary key is defined,
PowerBuilder automatically populates Table and Large Binary/Text columns for you, otherwise
select product_images for the Table and product_image for the other prompt. Remember, if
the primary key is not defined to the table, you are not able to update the DataWindow. The
value in the Key Clause is used by the PowerBuilder in the WHERE clause in the
UPDATE/DELETE statement to update the database table. If you want to use a default
template every time you invoke the OLE server, you can provide one or leave it empty.

Select the OLE server name from the drop-down ListBox. You will see the OLE Server Class
long name, once you select the class name, only the short name will be displayed. The value
in the Client Name Expression is used to identify the row and column number in the
DataWindow from the OLE Server. When you invoke the OLE Server from the DataWindow,
the result of the Client Name Expression is displayed.

To see how it works, go to preview mode and insert a row and provide a value for the
product_no and double-click on the database BLOB column.

If you see in the SQLPreview event, you can see data for the "window" only, not for
the "screen" column. PowerBuilder makes some internal calls to update this column.
If you are using SQL Server, make sure you set AutoCommit to True, before you
update this DataWindow and change it back to False once update is done. When the
use double clicks on this column, PowerBuilder automatically invokes the OLE
Server. If you want to invoke programmatically, you can use OLEActivate function.

Dw_1.OLEActivate( GetRow(), 2, 0)

The last argument is the OLE Server verb. Typically 0 is activate the server. As
explained above, to find other supported verbs, use RegEdit/V. You can change the
OLE Server dynamically by setting "Class" attribute as shown below:

dw_1.Modify( 'item_image_blob.oleclass="word.document"' )

Make sure to name the column when you create However, when you change the OLE Server, it
is applicable for new rows only. For example, there is one row in the DataWindow and it has a
word document. Now you change the OLE Server to PaintBrush and insert a row and invoking
the server invokes the PaintBrush. If you invoke the OLE Server for the first row, it will invoke
the Word not the PaintBrush.

OLE Presentation Style DataWindow


As explained earlier, OLE Database BLOB is used to retrieve date from BLOB data type column
and save back to the database. But, it doesn't allow you to use OLE for other data type
columns. You may want to allow users editing the database data in their favorite application,
say, Word, Excel and apply all the validation in PowerScript and save the changes back to the
database. You can accomplish this with DDE, but it needs lot of programming and error
handling.
With version 5.0, PowerBuilder allows you to present data in the OLE presentation style, i.e.,
retrieve data from the database and present the data to the user in one of the OLE server,
say, Word, Excel, PowerPoint, etc. PowerBuilder takes care of formatting the data according to
the OLE Server requirement and sending it to the OLE Server and getting the data back and
formatting back to the PowerBuilder format.

As a programmer, what you need to do is, just simply select OLE presentation style, instead of
one of the traditional presentation styles. That's all, you are done. For example, let us present
all the product information to the user in Word 6.0. To do this, invoke the DataWindow painter
and select 'SQL Select' data source and OLE Presentation Style.

Select product_master table and select all the columns and click on SQL toolbar icon. Select
'Microsoft Word Document' from the "Insert Object" dialog box. Drag-and-Drop all the columns
from the "Source Data" List Box into the "Group by" List Box. PowerBuilder automatically
groups the data into the specified groups before sending to the OLE Server. PowerBuilder
automatically adds appropriate aggregate function for each column in the 'Target Data'
ListBox. Double click on each column and remove the aggregate function definition for each
column. PowerBuilder automatically opens the OLE Server, MS Word in this example. Type
what ever you want to see such as headings, format it and select 'File > Close &Return Data
to Untitled' menu option and you will be back to PB. If you ever want to edit the data you
typed in OLE Server, display popup menu in the DataWindow design view and select Open
menu option.
OLE Control In the DataWindow
OLE Presentation style allows you to display entire data in the OLE Server. Sometimes you
may want to display data in the DataWindow and when the user selects, then you want to
display in the OLE Server, say, MicroSoft PowerPoint. This can be accomplished by placing OLE
control in any one of the traditional presentation style DataWindow.

Once you paint the DataWindow, select 'Insert > Cotrol > OLE Object' menu option and click
wherever you want to place in the DataWindow control work place. Select the OLE server from
the resulting prompt and create content in the OLE control and return back to PB. Click with
the right mouse button on the placed OLE control and select Properties from popup menu and
specify which columns data you want to display in the OLE Server from the 'data' tab.

Summary
In this session we explained OLE basics in detail and PowerBuilder implementation of OLE in
the window control as well as DataWindow. In the next session we will explain Advanced OLE
programming for OLE storages & Streams and also we will explain OCX controls and their
programming, and OLE automation.

For a complete summary of this session, browse 'ole1.ppt' , a PowerPoint presentation.

También podría gustarte