Table of Contents
Overview
Using
GetChild()
Retrieving
a DDDW Based on Another Columns
Using
a Multi Result Set DDDW in a Multirow DataWindow
Undocumented
DDDW Events
Stopping
the User from Scrolling to Invalid Rows
Adding
Color to a DataWindow
Speeding
Up DDDWs With Caching Techniques
Conclusions
Overview
The DropDownDataWindow (DDDW) edit style is one of PowerBuilder's most useful
features. It is essentially a DataWindow within a DataWindow. Its most common
use is as a listbox type control where the DataWindow that is actually dropped
down looks and acts like a listbox control allowing the user to select a row.
However, this is just the beginning of what can be done with dropdowns. Since
the DDDW is another DataWindow, your imagination is the limit. Instead of
looking like a listbox, we can make it look like a data entry form, or a help
form, even a calendar!
You Will Learn:
- When PowerBuilder retrieves your DDDW
- Using retrieval arguments in a DDDW
- How to use DataWindow sharing to minimize the SQL calls to your database
- Retrieving DDDW as the user enters data
- How to use multiple-result set DDDWs in a multiple-row environment
- DataWindow events used with DDDWs
- When is a DropDownDataWindow Retrieved?
When you place a DDDW on a DataWindow, PowerBuilder follows a simple rule
that governs the retrieval of the DDDW itself. When the parent DataWindow is
retrieved or has the initial row inserted, the following sequence takes place
for each DDDW
DropDownDataWindow Rule Number 1:
If
there are 0 rows on the DDDW then issue a retrieve() for the DDDW.
What this means is that if you place a DDDW that has retrieval arguments,
PowerBuilder will prompt the user for those arguments when you retrieve the
parent DataWindow. I suspect this is not the normal behavior you want your
application to have.
Unless your DDDW is a plain, ordinary, no arguments type DDDW, you'll need to
populate the DDDW before you retrieve the primary DataWindow. To do so, you'll
need an object reference to the DDDW so you can issue a Retrieve() or
InsertRow() function. To get the object handle to the DDDW, you'll need to use
the GetChild() function.
Tip: To ensure that a DropDownDataWindow isn't retrieved automatically,
save the DDDW with a blank row by adding a row from the Rows --> Data menu in
the DataWindow Painter. This saves the DataWindow with one blank row preloaded
for you. Since PowerBuilder will see at least one row in the DDDW, it won't try
to issue its own retrieve. You are then free to Retrieve() the DataWindow
yourself.
Using GetChild()
Normally when you interact with a DDDW, you just use the column name to get
the current value and set the current value. You don't actually issue functions
directly to the DDDW itself. However, to issue your own retrieves or to
manipulate the DDDW in any way, you'll need a way to reference the DDDW. To get
this reference, you'll use the GetChild() function. What GetChild() does is
provide a reference variable that points to the actual DataWindow in memory. You
can use this variable to issue functions against the DDDW such as Retrieve(),
InsertRow(), Modify(), etc.
The GetChild() function is declared as follows:
integer GetChild( string column_name, datawindowchild dwc)
Notice that GetChild returns an integer. This integer tells you whether
GetChild was able to return a reference to child DataWindow into the variable
dwc. GetChild looks on your parent DataWindow for the column you specify. It
then assigns the DataWindow control used to retrieve that column to the variable
dwc.
Here's a sample of how to use GetChild():
DataWindowChild dwc
IF dw_1.GetChild( "color_id", dwc ) > 0 THEN
dwc.SetTransObject( SQLCA )
dwc.Retrieve()
END IF
If the column, "color_id" is a DDDW then dwc should hold a reference to the
DDDW. If we had misspelled the name of the column or the column did not exist on
the DataWindow, GetChild() would return -1.
Here's another sample:
DataWindowChild dwc
IF dw_1.GetChild( "color_id", dwc ) > 0 THEN
dwc.InsertRow(0)
dwc.SetItem( "cust_id", "( New )" )
END IF
In this case, we added a row to the DDDW. Once we've got our reference to the
dropdown, we can do almost anything with it that we can do to a normal
DataWindow including using Modify, Describe, SetTransObject, Find, Sort,
Filter...anything!
Retrieving a DDDW based on the value of another column

Figure 1 : Database Schema from S-Designor
In Figure 1, we see the database schema for the database used in these
examples. Take a look at the color, product, valid_product_color, and
sales_order_items tables. Here's a description of those tables:
| Table |
Description |
| color |
These are all the possible colors that the
company can produce. |
|
product |
These are the products that the company
manufactures |
| valid_product_color |
This is a cross-reference table that contains
the valid colors for each given product. |
| sales_order_item |
These are items that were ordered. When an
product is ordered, the customer will specify which color she wants.
|
What we'd like to have is a data entry window that lists the colors for the
product currently being entered into the system. We could just create a DDDW off
of the color table and let the user select the proper color. However, we'd then
have to do some sort of validation. What we'd like to do is bring up a DDDW off
of the valid_product_color table right after the user types in the product_id.
To do so, our DDDW would have a retrieval argument for the product_id. This way
the DDDW would only retrieve colors for a given product.
Here are the steps to follow to get our DDDW populated based on data we
enter:
- Create our DDDW based on the valid_product_color_table
- Create a data entry DataWindow that will have our DDDW on it. This
DataWindow will be based on the sales_order_item_table
- Create a function, wf_get_colors() to retrieve the colors for a given
product.
- Code the DataWindow events, RowFocusChanged and ItemChanged to call
wf_get_colors()
Create our DDDW based on the valid_product_color_table
Create a DataWindow which will be our drop down. This DataWindow is based on
the valid_product_color table and has one retrieval argument: ai_product_id.
This DataWindow is named d_product_color_list. Notice that this DataWindow is
already saved with one empty row of data preloaded. (You can see this from the
Rows --> Data menu in the DataWindow painter.)
Also, this DDDW
should be sorted on product_id and color. This way, colors for a given product
are grouped together which is required for our DDDW filtering technique to work.
Create a data entry DataWindow that will have our DDDW on it
Create a free form DataWindow named d_sales_item_entry from the sales_item
table.
One of the columns on d_sales_item_entry is color_id. This column represents
the color of the particular item the user will be entering. It has the DDDW edit
style defined in Figure 2.

Figure 2 : DDDW Edit Style Definition for the
column color_id.
What we'd like the user to do is pick from a valid list of colors for that
product. To get this behavior, we're going to have to retrieve the
d_product_color_list DDDW after we've populated d_sales_item_entry.
Create a function, wf_get_colors() to retrieve the colors for a given product
To populate the DDDW, we're going to need to know the product id of the
current row. Also, if the user changes a product id, we need to repopulate the
DDDW as soon as that change is made. Which events do we need to code script for?
The events are rowfocuschanged and itemchanged. These fire off when the user
changes rows ( I know, I know, there's only one row on this DataWindow right
now...we'll get to that later.) or changes the product_id column.
Before we go out and code those functions, let's create a function to
populate the dropdown. This way, we'll only write the code once. Then we'll just
call that function from the events.
Here's the function. This function will go on our window. If you bring up the
window w_sales_item_entry, you'll see the following function defined:
public integer wf_get_colors( int ai_product_id )
DataWindowChild dwc
Long ll_return
IF dw_1.GetChild("color_id", dwc) > 0 THEN
dwc.SetTransObject( SQLCA )
ll_return = dwc.Retrieve( ai_product_id )
IF dwc.RowCount() = 0 THEN
dwc.InsertRow( 0 )
END IF
RETURN ll_return
ELSE
Return -1
END IF
Code the DataWindow events, RowFocusChanged and ItemChanged
This function takes a product ID and retrieves its valid colors. You'll see
that it is called from the rowfocuschanged and the itemchanged events: event:
DataWindow rowfocuschanged
// Ensure that there is a valid row available
IF GetRow() < 1 THEN RETURN
wf_get_colors( GetItemNumber( GetRow() , "product_id" ) )
event: DataWindow itemchanged
CHOOSE CASE GetColumnName()
CASE "product_id"
wf_get_colors( Integer( GetText () ))
END CHOOSE
Well, there we are. We've got a dropdown that responds to changes in our
data. Pretty snazzy, eh? Well, lets take a closer look at some behavior we
have-some behavior that we may not like. Take a look at Figures 3 and 4:

Figure 3: Data as it initially appears. Notice the color is Black.

Figure 4: User changes Product ID to 301. Notice color is now 'B'
What is happening here? Where did the description for Black go? Well, it went
away. Why? Because black isn't a valid color for product ID 301. Only Green,
Orange, and Pink are (must be a lava lamp). Because PowerBuilder could not find
a row that matched the data 'B', it can't display the word Black anymore. This
brings us to our second major rule for DropDownDataWindows.
DropDownDataWindow Rule Number 2:
The
dropdown only shows description data for rows that are in the primary buffer.
Rule two means that if you filter out rows or delete rows from the dropdown,
you're display could change if the DataWindow can no longer find the needed data
in the DDDW.
So how do you overcome the problem we just encountered? Well there are
several tactics you could try. You could simple set the color to null when the
product id changes. This forces the user to select a new color. You could also
check to see if the old color is still valid for the current one. A common way I
see people do this is through embedded SQL. When the itemchanged event fires, a
SELECT would be performed against the valid_product_color for the new product id
and color. If there is a match, the color is still valid. Here's how you might
code that in your itemchanged event:
event: DataWindow itemchanged
// This shows how to use embedded SQL to validate data
// It should only be used when necessary
INTEGER i_new_product_id
INTEGER i_count
STRING s_color_id
CHOOSE CASE GetColumnName()
CASE "product_id"
// What is the current color?
s_color_id = GetItemString( GetRow(), "color_id" )
// What is the new product ID?
i_new_product_id = Integer( GetText() )
// Retrieve the valid colors for this product_id
wf_get_colors( i_new_product_id )
SELECT count(*)
INTO :i_count
FROM valid_product_color
WHERE product_id = :i_new_product_id
AND color_id = :s_color_id ;
IF i_count <= 0 THEN
// If not, give a warning telling the user that
// the color is invalid and set the color to null
MessageBox("Warning", "The current color is " + &
"no longer valid. Enter a new one.")
SetNull(s_color_id)
SetItem( GetRow(), "color_id", s_color_id)
END IF
END CHOOSE
Notice that I said it is how you might code it. One of my goals in
development is to sent as little SQL to the database as possible and to minimize
network traffic. Why should I got back to the server for data I already have.
I've got the list of valid colors in my dropdown right? Well then, why don't we
just search for our color ('B') in the dropdown. If it there, we've got a valid
color. To do such a search, we'll use the Find() function of the DataWindow.
Here's the code:
event: DataWindow itemchanged
// This shows how to use embedded SQL to validate data
// It should only be used when necessary
INTEGER i_new_product_id
INTEGER i_count
STRING s_color_id
CHOOSE CASE GetColumnName()
CASE "product_id"
// What is the current color?
s_color_id = GetItemString( GetRow(), "color_id" )
// What is the new product ID?
i_new_product_id = Integer( GetText() )
// Retrieve the valid colors for this product_id
wf_get_colors( i_new_product_id )
IF IsValid( idwc ) THEN
// Is the old color in the new result set?
i_count = idwc.Find( "color_id = '" + s_color_id + "'", 1, idwc.RowCount())
IF i_count <= 0 THEN
// If not, give a warning telling the user that
// the color is invalid and set the color to null
MessageBox("Warning","The current color is " + &
"no longer valid. Enter a new one.")
SetNull( s_color_id )
SetItem( GetRow(), "color_id", s_color_id)
END IF
END IF
END CHOOSE
Using the DDDW in a multiple-row DW
In the preceding example, we used the dropdown in a single row situation. If
your DDDW doesn't have retrieval arguments, you should have no problems using it
in a multi-row situation. Take a look at the DataWindow sequence listed in
Figure 5:



Figure 5 : Effect of Changing a Result Set with
Multiple-Rows
You'll notice immediately that the color DDDW is not behaving properly. As we
move to different rows, the DDDWs are showing different data. This is because we
coded a call to wf_get_colors in our rowfocuschanged event. As we move rows, the
DDDW is getting new data.
In the case of row 1, the colors Orange and Red are not valid colors for
product ID 300. As for row 2, Black and Red are not valid colors for 301.
However, the third case looks fine! This is because black, orange, and red are
all valid colors for product ID 302.
Well, what can we do about this? Several things might spring to mind. Why
don't we just bring all the possible rows back from the Database. Then we can
just filter out the rows that don't belong when we change products. Well, on the
surface this might sound fine. However, after running the filter on the rows we
don't want, we'd still get the same behavior shown in Figure 5. Why? Because
PowerBuilder only shows descriptions for the rows in the dropdown's primary
buffer. When those rows are filtered out, they won't be in the primary buffer,
they'll be in the filtered buffer. Therefore, we'll get "bad" behavior from our
dropdown.
So, what can we do? All rows for all products must be in the dropdown's
primary buffer but when we expand the dropdown, all those extra colors will
appear as in the dropdown in Figure 6.

Figure 6 : Bad Results When Selecting all Rows into
DDDW
So how can we make the dropdown not show all of the colors for all of the
products? The answer is a function introduced in PowerBuilder 4.0. So what is
this grand function? SetDetailHeight(). SetDetailHeight lets you change the
detail height of certain rows in your DataWindow. Here's how SetDetailHeight()
is declared:
integer SetDetailHeight( long start, long end, int height )
What we're going to do is set the height of every row in the DDDW that
doesn't belong to the current product to 0. This will effectively make those
rows invisible. So when the dropdown is expanded, all the user will see are the
rows for the current product. The dropdowns in the other rows will still show
their descriptions because all of the rows are still in the primary buffer.
They're just can't be seen because of the detail height. So all we'll do is
change the heights in the dropdown each time the user changes rows or
adds/changes the product_id. Now, where are we going to put this code? Here's
how I did it:
First of all, we're going to be making extensive calls to the dropdown.
Because of this, I decided to create an instance variable to hold the reference
to the DDDW.
instance variable: DataWindowChild idwc
The best place to initialize this idwc variable is in the constructor of the
primary DataWindow. Why? Because the variable only needs to be initialized once.
event: DataWindow::constructor
// Setup a handle to the color dropdownlistbox
IF GetChild( "color_id", idwc ) THEN
MessageBox( parent.title, "Could not initialize color DDDW")
END IF
Also, when we retrieve the dropdown, we're going to need to retrieve all
rows-not just the ones for a given product. Remember, to have the dropdown's in
rows other than the current row show descriptions, you'll have to have all
colors in your DDDW. So, how are we going to retrieve all the rows? We'll, when
I created the source DataWindow for the dropdown, I used a trick to allow me to
get all rows back. Take a look at the SQL Select statement for the DataWindow
object d_product_color_list. You'll see the following SELECT:
SELECT product_id, color_id, description,
FROM valid_product_color, color
WHERE ( color.color_id = valid_product_color.color_id )
AND ( ( product_id = :ai_product_id )
OR ( :ai_product_id = -999 ) )
ORDER BY product_id ASC, description ASC
Tip: Putting a phrase like ' OR :retrieval_argument = -999 ' allows
you to call Retrieve(-999) and retrieve every row in your selection while
ignoring other where phrases. I use this trick a lot when I'm developing
systems. However, in a performance intensive production system, I'd most
likely not have the OR there because it can cause some query optimizers to act
strangely and retrieve rows slower than needed. This slowdown is something we
should obviously avoid in lengthy or complex queries.
If you call idwc.Retrieve( -999 ), you'll get all the colors for every
product. So, let's take a look at the dw_retrieve event for our DataWindow:
event: DataWindow::dw_retrieve
idwc.SetTransObject(SQLCA)
idwc.Retrieve( -999 ) // Load the DDDW with all rows
Retrieve( 2001 )
When the parent DataWindow is retrieved, the DDDW will be retrieved first.
I've got the order number 2001 hardcoded just for sample purposes.
What have we done so far?
- We've created an instance variable, idwc, to hold a reference to our DDDW
- We initialized idwc in the constructor event.
- We retrieved all the possible product and color combinations into the DDDW
in the dw_retrieve event.
So what's left? Well, whenever the row changes, we need to set the detail
heights of each unwanted DDDW row to 0. Where can we do this? Well, we've
already got a function called wf_get_colors() which is called by our
rowfocuschanged event. Why not just change wf_get_colors() to work the way we
want and we'll minimize the changes to other code that we've already written.
So, let's take a look at our new wf_get_colors() function on our window:
public integer wf_get_colors( int ai_product_id )
/* wf_get_colors( int ai_product_id )
Populates the DDDW on dw_1 with the valid colors for the product_id.
Returns the number of colors found.
Assumes that the transaction object has been set.
*/
STRING s_old_filter
IF IsValid (idwc) THEN
IF ii_dddw_detail_height = 0 THEN
// Set the detail height the first time through
ii_dddw_detail_height = Integer( idwc.Describe ( "Datawindow.Detail.Height"))
END IF
idwc.SetRedraw(FALSE)
// Reset the height to the default
idwc.SetDetailHeight(1, idwc.RowCount(), ii_dddw_detail_height )
// Filter out all rows belonging to the current product
IF IsNull( ai_product_id ) THEN
idwc.SetFilter( '0 = 1' ) // Filter out all rows
ELSE
s_old_filter = idwc.Describe(Datawindow.Table.Filter)
idwc.SetFilter("product_id <> " + String( ai_product_id ) )
END IF
idwc.Filter()
// Set the height to all these rows 0
idwc.SetDetailHeight( 1, idwc.RowCount(), 0 )
// Reset the filter back to its previous setting.
idwc.SetFilter(s_old_filter)
idwc.Filter()
idwc.SetRedraw(TRUE)
RETURN 1
ELSE // Invalid DWC
RETURN -1
END IF
With this new, improved wf_get_colors() function, we get the behavior we
want. When the user clicks on the dropdown, he'll get the colors that go with
the current product. And, all the other dropdowns contain the proper description
information. Wahoo we're done.
OK, not really.
The only drawback with this method is that although the user will only see
the valid colors for a given product when the dropdown is pulled down, the user
can still get access to the other values which are not valid by using the up and
down arrows. The reason for this is because these values are still in the
primary buffer, they just aren't visible in the DataWindow itself. So when the
user clicks the up and down arrow, PowerBuilder moves them through the rows in
the DDDW one at a time-including the ones that aren't normally visible when the
dropdown is expanded.
Here's where we employ some arcane DDDW techniques.
So we're going to take a break from the pesky dropdown problem we have right
now and learn something completely different.
Undocumented Events for DDDWs
When you use a DDDW there are only two events that you can normally use with
PowerBuilder out of the box. They are: pbm_dwclosedropdown and pbm_dwndropdown.
What if you want to know when the user changes rows in your DDDW? Or right
clicks on the DDDW? Well, you can't program for the events normally because the
DDDW isn't a separate DWC that you place on the window and only the DWC is the
only place to put code for a DataWindow. So, how can we overcome this?
The thing that saves us is that as you work with a DDDW, PowerBuilder sends
windows events to the primary DataWindow Control. I'm sure PowerBuilder is using
these events internally, however, they aren't documented anywhere that mere
mortals can find them. In 1995, Mark Brown wrote an article in the PowerBuilder
Developer's Journal documenting these DDDW events. Mark discovered that, in
PowerBuilder 4.0, PowerBuilder triggers WM_COMMAND events when the DDDW is doing
its work. By defining a user event mapped to pbm_command, a programmer can,
among other things, determine when the user changes a row in a DDDW.
There's one big catch to using this technique as published, as of
PowerBuilder 5.0.2, the pbm_command event does not work. PowerBuilder will
actually trigger the event but gives no way to get access to the row or DDDW
that is being processed. This appears to be a bug since in every other event,
PowerBuilder will pass the needed information. Luckily there is another way: the
other event. If PowerBuilder fires an event that isn't coded or defined for a
DataWindow, it will fire the "other" event. Since we won't declare anything that
handles pbm_command, PowerBuilder will fire the "other" event instead. So we're
going to write some code in the "other" event.
About the "other" Event
The "other" event
fires many times as the user works with the DataWindow. As the user moves the
mouse, clicks on the DataWindow, and types things, the other event is fired
many, many times. Conventional wisdom in the PowerBuilder community says that
putting anything in the "other" event is a severe no-no. This bit of wisdom was
originally developed when PowerBuilder was in its 2.0 and 3.0 versions. At that
time, the machines we used were 386s and 486s. Today, we're using Pentiums and
fast 486s. With care the other event can be used; the trick is to do the minimum
amount of work as possible so the system doesn't slow down as the other event is
being fired over and over.
Each internal Windows event has two pieces of information called the longparm
and wordparm. These are numbers that are used to pass useful information to the
event. We're going to use the longparm and wordparm to determine which DDDW is
getting action and which event has occured. When the user moves within a DDDW or
when the DDDW is performing some action (such as retrieving data, changing
RowFocus, etc.) the "other" event will trigger. PowerBuilder will include the
DDDW which is currently being processed in the longparm. It includes the action
that is being performed in the wordparm.
The longparm and wordparm actually contain two integers each; a high and a
low integer. This allows you to actually pass four numbers to an event by
combining them. PowerBuilder passes the handle to the DDDW in the Low integer of
the longparm and passes the event in the High integer of the wordparm. To get
the longparm and wordparm, we'll use two attributes that are passed to the
"other" event: lparam and wparam. We'll use the IntHigh and IntLow functions to
extract the integers from these arguments.
Here's the code to get the values we need:
Event: DataWindow::other
INTEGER i_DWC_Handle, i_DWC_Event
i_DWC_Handle = IntLow( lparam )
i_DWC_Event = IntHigh( wparam )
Here's a list of some of the messages that PowerBuilder uses:
|
Number |
Event |
Number |
Event |
|
768 |
RetrieveStart |
2049 |
SelectionChanged |
|
769 |
RetrieveEnd |
2311 |
MouseMove |
|
1024 |
UpdateStart |
2313 |
LeftButtonUp |
|
1025 |
UpdateEnd |
2314 |
RightClicked |
|
1280 |
DoubleClicked |
2315 |
RightButtonUp |
|
1281 |
Clicked |
2317 |
ScrollHorizontal |
|
1281 |
RightDoubleClicked |
2318 |
ScrollVertical |
|
1536 |
DBError |
2319 |
SQLPreview |
|
1792 |
ItemFocusChanged |
2320 |
Resize |
|
2048 |
RowFocusChanged |
2321 |
PositionChanged |
So, here's how you would capture the RowFocusChanged event in your DDDW for
color:
Event: DataWindow::other
INTEGER i_DWC_Handle, i_DWC_Event
i_DWC_Handle = IntLow( lparam )
i_DWC_Event = IntHigh( wparam )
// Make sure that the message received is for the DDDW
IF i_DWC_Handle = Handle( idwc ) THEN
// Determine which event on the DDDW has occured
CHOOSE CASE i_DWC_Event
CASE 2048 // RowFocusChanged on DDDW
l_cur_row = idwc.GetRow()
END CHOOSE
END IF
Notes for PowerBuilder 4.0
You should still be able to use the other event in PowerBuilder 4.0. However,
you can also map a user event to pbm_command and get the same results-without
having to program the "other" event. The pbm_command event doesn't fire as much
as the other event does.
In PowerBuilder 4.0, the longparm and wordparm aren't passed as arguments to
events. Instead, you must use the message object to get the long and wordparm.
One thing though, in 4.0, you don't need the wordparm because PowerBuilder 4.0
passes the DDDW handle and event in the longparm itself.
To get these two values out of longparm, we'll use the message object since
the message object includes the longparm of the current event. A long variable
is twice as long as an integer; that is, it contains two integers. PowerBuilder
puts the DDDW being worked on in the low integer and the message being sent in
the high integer.
Here's the code to get those two integers out
of the longparm:
Event: DataWindow::we_command mapped to pbm_command
INTEGER i_DWC_Handle, i_DWC_Event
LONG l_longparm
l_longparm = Message.LongParm
i_DWC_Handle = IntLow( l_longparm )
i_DWC_Event = IntHigh( l_longparm )
Stopping Users from Scrolling to Invalid
DDDW Rows
Since we can now tell when the user changes rows on the DDDW, we can write
code that determines when the user changes to a row that isn't valid for this
occurrence of a product ID. To stop the user from scrolling to a color that
isn't valid for the current item, we're going to write code for the
RowFocusChanged event of the DDDW. If the user has moved to a row that isn't
valid, we'll just move the user back to where he belongs.
For this technique of moving the user back to where he belongs to work, we're
going to have to know the direction that the user was moving when the
rowfocuschanged. If the current row is less than the previous row then we know
the user is moving "up" toward the top of the DataWindow. Otherwise the user is
moving down the DataWindow.
To keep track of the previous row, we'll need an instance variable to hold
the row number as we move. Set this instance variable equal to the current row
of the DDDW when the user enters the DDDW column. Here's the code:
Instance Variable of Window or DWC: LONG il_prev_dd_row
Event: DataWindow::itemfocuschanged
// This code needed to track the previous row the
// DDDW was on. With it we can determine which direction
// the user is moving within the DDDW
IF GetColumnName() = "color" THEN
il_prev_dd_row = idwc.GetRow()
END IF
To complete the DDDW example, let's look at the code for the we_command
event:
Event: DataWindow::other
INTEGER i_DWC_Handle, i_DWC_Event
i_DWC_Handle = IntLow( lparam )
i_DWC_Event = IntHigh( wparam )
LONG l_cur_row
LONG l_next_row
INTEGER i_product_id
STRING s_find
// Make sure this is only run when the user is in the "color_id" column
IF This.GetColumnName() <> "color_id" THEN
RETURN
END IF
// Make sure that the message received is for the DDDW
IF i_DWC_Handle = Handle( idwc ) THEN
CHOOSE CASE i_DWC_Event
CASE 2048 // RowFocusChanged on DDDW
i_product_id = this.GetItemNumber( this.Getrow(), "product_id" )
l_cur_row = idwc.GetRow()
/*
If the current row is for the current product then
do nothing except set the last row pointer
*/
IF idwc.GetItemNumber( l_cur_row, "product_id" )= i_product_id THEN
il_prev_dd_row= l_cur_row
RETURN
END IF
// Set up Find String for product_id search
s_find = "product_id = " + string( i_product_id )
/*
Determine which direction the user moving:
If current > previous row then moving to the end
If current < previous row then moving to the top
*/
IF l_cur_row > il_prev_dd_row THEN
// Search Forward
l_next_row = idwc.Find( s_find , l_cur_row + 1 , idwc.RowCount() )
ELSEIF l_cur_row < il_prev_dd_row THEN
// Search Backwards
l_next_row = idwc.Find( s_find,l_cur_row - 1,1 )
ELSE
RETURN
END IF
/*
If the searched row was not found that means we
are at the top or the bottom of the list.
Therefore move the user back to the previous row.
Otherwise allow the user to move to the next row
*/
IF l_next_row = 0 THEN
idwc.SetRow( il_prev_dd_row )
SetRedraw( TRUE )
RETURN
ELSE
idwc.Setrow( l_next_row )
SetRedraw( TRUE )
END IF
END CHOOSE
END IF
The code just shown prevents the user from scrolling with the up and down
arrow to a value that doesn't match the current item number.
Adding Color to your Dropdown
 |
Most dropdowns tend to be on the boring side-with good reason. Most of
the time we just want a simple list from which to choose. However, we've
got a list to choose from with colors. Why not show the colors?
Here are the steps taken to add color to the dropdown:
- The columns, Red, Green, and Blue were added to the color table.
- The DDDW, d_product_color_list, has the red, green, and blue columns
added to the result set. (They are not displayed on the DDDW, however.)
- A rectangle named 'color_box' was added to the DataWindow.
- The brush_color attribute for the color_box was set to the correct
RGB value.
|
RGB Color Values
Colors in PowerBuilder (and almost all Windows programs) is set using RGB
color values. In Windows a color is nothing more than a long value. For example,
black is 0, red is 255, green is 65280, and blue is 16711680. Easy, right? Well,
just remember that there is a function called RGB in PowerBuilder which will
take values from 0 to 255 for the three color components. Red is RGB( 255, 0, 0
), Green is RGB( 0, 255, 0 ), and Blue is ( 0, 0, 255 ). Finally, black is RGB (
0, 0, 0 ) and White is RGB ( 255, 255, 255 ). From here you can choose over
sixteen million colors (256 times 256 times 256).
Here's a listing of the color table in the sample:
|
Color ID |
Description |
Red |
Green |
Blue |
|
W |
White |
255 |
255 |
255 |
|
O |
Orange |
200 |
40 |
0 |
|
B |
Black |
0 |
0 |
0 |
|
G |
Green |
0 |
255 |
0 |
|
Y |
Yellow |
255 |
255 |
0 |
|
R |
Red |
255 |
0 |
0 |
|
P |
Pink |
232 |
91 |
126 |
|
Bl |
Blue |
0 |
0 |
255 |
Create a rectangle called color_box to the DataWindow.
 |
The third step is to add a rectangle to our color DDDW. Here's what
your DataWindow should look like once you've added and sized your
rectangle. Name the rectangle color_box. |
Code the brush color attribute for the color_box to show the correct
color.
Now that you've got the rectangle on the DDDW, lets colorize it. To do so,
right click on the rectangle and choose attributes. In the attribute for
brush_color, enter the following line:
RGB ( color_red, color_green, color_blue )
That is all there is to adding color.
Speeding up DDDW Usage Through Caching
Dropdowns are very flexible and allow us to create data driven applications.
However, they have a down side also: performance. Imagine an order entry form
with about ten dropdowns on it. Most of the dropdowns only have few rows on
them. Well, each time you bring up an order, these ten dropdowns will need to be
repopulated. I can tell you that ten small retrievals on a local SQL Anywhere
engine on a fast machine will hardly be noticed. However, those same retrievals
over a WAN after lunchtime at the end of the month is going to be quite slow. If
one of the prime goals of client/server computing is to reduce the needed
network traffic, why then, do we need those ten to fifteen selects for our order
entry window?
The answer is that we probably don't.
There's at least two techniques to our rescue. DataWindow sharing and
importing strings.
Here's how the sharing technique works:
Sharing Dropdown DataWindows
[Note: If you are using PowerBuilder 5.0, you can use the DataWindow Cache
Service of the PowerBuilder Foundation Class Library (PFC) to achieve DDDW
Sharing. See our DDDW Extension for
the PFC ]
First, we assume that most dropdown data is fairly static throughout an
application sessions. Dropdowns are mostly used for code lookups and they don't
change much during the execution of an application. Because of these
assumptions, we can build a master storage window that will hold the dropdown
data throughout the life of the session. When we first retrieve a DDDW, we'll
open in a special window. From then on, anytime we use the DDDW we'll just issue
a ShareData() against the preloaded DDDW.
Here are the steps needed to use shared dropdowns to speed up our
application:
- Create a reusable user object DataWindow control called u_dw_plain.
- Create a "master" dropdown DataWindow holder. (w_dddw)
- When a window opens, have it register its dropdowns with w_dddw.
- When using a window that has dropdowns that are already on w_dddw,
we'll merely use ShareData() to share the two DataWindows.
- If the dropdown isn't already loaded, we'll dynamically create a
DataWindow user object and retrieve the data. Then we'll share it.
Using this technique, we'll only share dropdowns as we use them. The w_dddw
will be completely dynamic and only open the minimum number of DataWindows
needed.
Create a reusable user object DataWindow control called u_dw_plain.
Go into the user object painter and choose New. Then choose Visual Standard
user object. You'll then be asked the Standard Visual Type; choose DataWindow.
Save your DataWindow object as u_dw_plain.
Create a "master" dropdown DataWindow holder. (w_dddw)
Create a new window. Make this window fairly small and set its visible style
to FALSE. This window should not appear during normal processing. It will act as
a DataWindow storage device. In this new window, create one function:
/*
wf_sharedddw takes a datawindow and column name as arguments.
It loops through the available datawindows on the DDDW looking for a match.
If it finds one it will share the two datawindows together. If no datawindow is
found, then the w_dddw adds a new one to the window using an OpenUserObject() function.
The function returns 1 if the two DataWindows are shared successfully.
A -1 occurs of there was not a successful share or no rows were found.
Return Codes:
1 Function performed correctly
-1 Column Passed is not a valid DDDW column
-2 Cannot perform GetChild on the column
-3 Sharing with pre-existing DataWindow failed
-4 Failed to Open New User Object DataWindow
-5 Sharing with newly created DataWindow failed
*/
INTEGER i, i_num_controls
STRING s_dataobject
DATAWINDOWCHILD ldw_child
DATAWINDOW ldw
u_dw_plain ldw_plain
/*
You can't just use DataWindowChild.dataobject, this won't work.
We have to use the Describe column to determine what the DataWindow is for a given column.
*/
s_dataobject = adw.Describe( as_column + ".dddw.name" )
IF s_dataobject = "!" or s_dataobject = "?" THEN
RETURN -1
END IF
IF adw.GetChild( as_column, ldw_child ) < 0 THEN
RETURN -1
END IF
// Loop through each control, if it is a DataWindow then share
i_num_controls = UpperBound( control )
FOR i = 1 TO i_num_controls
IF control[i].TypeOf() = DataWindow! THEN
ldw = control[i]
IF ldw.dataobject = s_dataobject THEN
IF ldw.ShareData( ldw_Child ) > 0 THEN
RETURN 1
ELSE
RETURN -3
END IF
END IF
END IF
NEXT
// No Matching DataWindow was found, add one.
// Only way to dynamically add a DataWindow Control
IF OpenUserObject( ldw_plain ) < 0 THEN
RETURN -4
END IF
Control[ i_num_controls + 1 ] = ldw_plain
ldw_plain.dataobject = s_dataobject
ldw_plain.SetTransObject( SQLCA )
ldw_plain.Retrieve()
IF ldw_plain.ShareData( ldw_Child ) > 0 THEN
RETURN 1
ELSE
RETURN -5
END IF
RETURN 1
When a window opens, have it register its dropdowns with w_dddw.
This is the easy part! Right before you do your retrieve, just call the
function we defined. Here's how we'd do it for the d_sales_order_entry
DataWindow included in the sample:
Event: DataWindow ue_dwretrieve
IF w_dddw.wf_sharedddw( this, "cust_id" ) < 0 THEN
MessageBox( title, "Could not share cust_id.")
END IF
IF w_dddw.wf_sharedddw( this, "region" ) < 0 THEN
MessageBox( title, "Could not share region.")
END IF
//... finish normal DataWindow retrieval
That's all there is to it. Now you've got a window that will always keep
track of dropdowns. If you want, you can embellish this example with additional
functionality such as refresh requests or timed refreshes. For example, you
might want a certain dropdown to be re-retrieved if it has been over thirty
minutes since the last retrieve.
You can test the w_dddw sample by running the Sales Order Entry sample
application. Click the "Show Storage Window" button and type into the DataWindow
on the storage window. After you type data in the description, you should see
the dropdown data in the open Windows change.
Using ImportString for DDDW Storage
This technique is a variation of the DataWindow sharing technique. The
advantage to this technique is that it doesn't involve any invisible windows or
multiple DataWindow controls. It generally consumes less memory than the prior
technique. However, the drawback is that during runtime, it could, use more
memory as well as require more processing time as data is Imported. Let's take a
look at the technique.
This technique involves a custom class user object (aka nonvisual.) This
object has the same function, uf_sharedddw, that w_dddw had. However, there will
be no DataWindow user objects created. Instead, the data for each dropdown will
be stored in a string array. Then when a dropdown is needed, the array will be
searched. If an entry is found then we use ImportString() to import the data
into our dropdown. If an entry is not found then the dropdown is retrieved and
the data is exported and stored into the string array.
The custom class object is called unv_sharedddw. Here is the object
definition:
Instance Variables:
// Holds the dataobject name of the DDDWs
STRING isa_dataobject[]
// Holds actual data from the DDDW
STRING isa_data[]
// Holds the number of DDDWs loaded
INTEGER ii_num_dw
Function: integer uf_sharedddw( DataWindow adw, String as_column )
/*
uf_sharedddw takes a datawindow as an argument.
It loops through an internal array of DataWindows that it has data for.
If it finds an entry in the array, data is imported into the DDDW.
Otherwise a new entry is added and the DDDW is retrieved from the DataBase.
Finally, any new DDDWs retrieved are added to the array.
A -1 occurs of there was not a successful import.
Return Codes:
1 Function performed correctly
-1 Column Passed is not a valid DDDW column
-2 Cannot perform GetChild on the column
-3 Cannot Import Data
*/
INTEGER i
STRING s_dataobject
DATAWINDOWCHILD ldw_child
s_dataobject = adw.Describe( as_column + ".dddw.name" )
IF s_dataobject = "!" or s_dataobject = "?" THEN
RETURN -1
END IF
IF adw.GetChild( as_column, ldw_child ) < 0 THEN
RETURN -2
END IF
// Search the DataWindow Array
FOR i = 1 TO ii_num_dw
IF isa_dataobject[ i ] = s_dataobject THEN
ldw_child.ImportString ( isa_data[ i ] )
RETURN 1
END IF
NEXT
// No Matching DataWindow was found, add one.
ii_num_dw = ii_num_dw + 1
isa_dataobject[ ii_num_dw ] = s_dataobject
ldw_child.SetTransObject( SQLCA )
IF ldw_child.Retrieve() < 0 THEN
RETURN -3
END IF
isa_data[ ii_num_dw ] = ldw_child.Describe("DataWindow.Data") RETURN 1
Of the two approaches, I prefer the Shared DataWindow technique. The reason
is that it is completely flexible. Regardless of the retrieval arguments, the
our wf_sharedddw function of w_dddw won't ever have to issue a Retrieve()
statement. All that happens is that two DataWindow controls are shared. The
retrieval can come later. However, in the ImportString() example, a Retrieve()
must be performed so the non-visual object can capture the data into the
isa_data[] array. I included the ImportString sample to make sure you understood
that a DDDW can be used almost like any other DataWindow. One use of
ImportString() for DDDWs might be the ability to save lookup data into an INI
file. Then when a DDDW is loaded it might go to the local hard disk instead of
the network. The bottom line is that the possibilities are endless and limited
only by your imagination.
Conclusions
We've seen several techniques involving DropDownDataWindows. We've learned
the following:
- If there are no rows on a DDDW then the DataWindow will issue a Retrieve()
for it when the first row is created on the primary DataWindow.
- Use the GetChild( column_name, DataWindowChild variable ) function
to get a DataWindow handle that can be manipulated almost exactly like a
normal DataWindow.
- We can use the ShareData() functions on DDDWs.
- We can use the ImportString() functions on a DDDW.
- We can use almost any PowerBuilder DataWindow function on a DDDW
including Retrieve, Update, GetSQLPreview, GetRow, InsertRow, SetItem,
etc.Retrieving data in a DDDW based on the value of another column is coded in
the ItemChanged event and involved a Retrieve( GetText() ).
- The DataWindow only displays descriptions for data in the DDDW's
primary buffer. Nothing in the filtered buffer will show.
- We can use SetDetailHeight() to make rows in the Primary buffer
invisible from view.
- As the user hits the down arrow in a DDDW, PowerBuilder scrolls
through the primary buffer regardless of whether the row is visible or not
because of SetDetailHeight()
- PowerBuilder triggers the OTHER event when important things happen
to a DDDW such a RowFocusChanged, ItemFocusChanged, or a SelectionChanged. (In
4.0 this event is the WM_COMMAND which can be mapped to PBM_COMMAND)
- By using the IntHigh() and IntLow() function on the OTHER event's
longparm and wordparm, you can determine which DDDW is being acted upon and
which event of the DDDW was being triggered.
IntHigh( wordparm ) = event
triggered
IntLow( longparm ) = DDDW that is being acted upon
In PowerBuilder 4.0, IntHigh( longparm ) gives the event triggered.