PowerBuilder – Datawindow Drag/Drop Rows with Business Rules
This time I’m demonstrating a basic bit of functionality common to many Windows based applications – drag and drop. In this case I am talking about the ability to drag a row of data to a new location in a list. However, to make it more interesting I am throwing in some business rules which control where exactly the dragged row is inserted.
For the purposes of this exercise my data is hierarchical in nature, that is, there are Parent, Child, and Grandchild records. For any given Parent there can be an unlimited number of Children which are of two types. The first type are always listed first after the Parent and may have an unlimited number of Grandchildren associated with them. The second type are always listed after the first type. See the following example:

Row Manipulation (Business) Rules
Now for the specifics regarding how the rows are manipulated when moved.
Dragging a parent record will move all of its associated Children and GrandChildren records.
Parent records can be placed in any order.
Dragging a Child Type 1 record will move all of its associated Grandchildren records. Child Type 1 records can be placed under any Parent records.
Child Type 2 records can be dragged under any Parent record.
Grandchild records can be dragged only within its assigned Child Type 1 record; they cannot be moved to any other record type.
A visual indicator appears on the rows as a record is dragged. This consists of two lines showing where the record would be placed if none of the business rules applied. Since there are business rules, this indicator serves mainly as a visual cue to the approximate location (based on the type of row being dragged and the type of row the target is) where you are dragging the selected record.
Setting up for Drag/Drop on a Datawindow
Clicked event of the datawindow
this.selectrow(0, FALSE) // unselect all selected rows this.selectrow(row, TRUE) // select the clicked row ib_drag = (row > 0) // are we capable of dragging? il_dragged_row = row // source row which is dragged (used for visual indicator on dw) IF ib_drag THEN ib_mouse_down = TRUE // button is clicked il_mouse_down_x = xpos // original coordinates of pointer il_mouse_down_y = ypos ELSE ib_mouse_down = FALSE END IF
User defined event on datawindow ‘ue_dwnmousemove’
This is mapped to the event pbm_dwnmousemove
IF ib_drag THEN // we are capable of being dragged
IF ib_mouse_down THEN // mouse is down
IF (Abs(PointerX() - il_mouse_down_x) > 50) OR (Abs(PointerY() - il_mouse_down_y) > 50) OR (PointerX() = 0) OR (PointerY() = 0) THEN
// we have moved the pointer more than 50 powerbuilder units so we are dragging the row
Drag(Begin!)
END IF
END IF
END IF
Dragwithin event on the datawindow
// scroll the datawindow if not all row fit
long l_i, ll_firstrow, ll_lastrow
ll_firstrow = long(this.Object.DataWindow.FirstRowOnPage)
ll_lastrow = long(this.Object.DataWindow.LastRowOnPage)
IF (row = ll_firstrow OR row = ll_firstrow + 1) AND ll_firstrow > 1 THEN
this.scrollpriorrow( )
ELSEIF (row = ll_lastrow OR row = ll_lastrow - 1) AND ll_lastrow < this.rowcount( ) THEN
this.scrollnextrow( )
END IF
// set the visual indicator for the drop location
FOR l_i = 1 TO this.RowCount()
this.SetItem(l_i, "row_drag", " ")
NEXT
// set up for visual indicators
IF il_dragged_row > Row THEN
this.SetItem(Row, "row_drag", "T")
IF Row <> 1 THEN
this.SetItem(Row - 1, "row_drag", "B")
END IF
ELSE
this.SetItem(Row, "row_drag", "B")
IF Row <> this.RowCount() THEN
this.SetItem(Row + 1, "row_drag", "T")
END IF
END IF
User defined event on the datawindow ue_lbuttonup
This is mapped to the event pbm_dwnlbuttonup
long l_i ib_Mouse_Down = False IF ib_drag THEN // are we dragging? ib_drag = False // stop dragging This.Drag(End!) END IF // reset the visual indicator on the datawindow FOR l_i = 1 TO this.RowCount() this.SetItem(l_i, "row_drag", " ") NEXT
Dragdrop event on the datawindow
// Green rows are the Parents
// Yellow rows are Child Type 1 and are always just after the Parent
// Orange rows are Grandchildren and are always associated with Child Type 1 records
// Red rows are Child Type 2 records and are always after Child Type 1 and any associated Grandchildren
long ll_row, ll_rowcount, ll_dragrows
long ll_new_row, ll_prev_row, ll_i, ll_parent_row
long ll_diagrow, ll_child1_row, ll_child2_row, ll_original_parent, ll_new_parent, ll_find_row
long ll_start_child1, ll_end_child1
string ls_rowtype, ls_dest_rowtype
string ls_find
CHOOSE CASE source
CASE dw_1
ll_row = getselectedrow(0) // should be same as il_dragged_row
IF ll_row = 0 THEN RETURN
IF ll_row = row THEN RETURN
ls_rowtype = getitemstring( ll_row, 'row_type')
IF row > 0 THEN
ls_dest_rowtype = getitemstring(row, 'row_type')
ELSE
ls_dest_rowtype = getitemstring(this.rowcount(), 'row_type')
END IF
CHOOSE CASE ls_rowtype // what is being dragged
CASE 'Parent'
ll_original_parent = this.object.row_group_id[ll_row]
IF row = 0 THEN
ll_new_parent = this.object.row_group_id[this.rowcount()]
ELSE
ll_new_parent = this.object.row_group_id[row]
END IF
IF ll_new_parent = ll_original_parent THEN RETURN // not moving since it's been dragged onto itself
// get the range of rows which will move
IF ll_row = this.rowcount() THEN // Parent at end of list
ll_dragrows = ll_row
ELSE
// get all the other stuff under this Parent
ls_find = "row_type = 'Parent'"
ll_dragrows = this.find(ls_find, ll_row + 1, this.rowcount() + 1) // find the next Parent
IF ll_dragrows = 0 THEN // dragged green was last on the list
ll_dragrows = this.rowcount( ) // get all rows under the Parent
ELSE
ll_dragrows = ll_dragrows - 1
END IF
END IF
// find the row of the next Parent before which the dragged items will be placed.
CHOOSE CASE ls_dest_rowtype
CASE 'Parent'
ll_prev_row = row
CASE 'Child_1', 'Grandchild','Child_2'
ls_find = "row_type = 'Parent'" // next Parent
ll_find_row = this.find( ls_find, row, this.rowcount() + 1 )
IF ll_find_row = 0 THEN
ll_prev_row = this.rowcount() + 1 // put at end of list
ELSE
ll_prev_row = ll_find_row // move to before the next Parent after the row dropped on
END IF
END CHOOSE
// move rows to new position in list
rowsmove( ll_row, ll_dragrows, Primary!, THIS, ll_prev_row , Primary!)
CASE 'Child_1'
// Child_1s can be associated with any Parent - if you move one, however, you must also move any bound Grandchilds
// get all the Grandchilds under this Child_1
IF ll_row = this.rowcount() THEN // Child_1 at end of list
ll_dragrows = ll_row
IF ll_dragrows = 2 THEN RETURN
ELSE
// get all the Grandchilds under this Child_1
// search for next Child_1 or a Child_2 on the Parent
// starting Child_1 is the selected row
// get last row of Grandchilds for this Child_1
FOR ll_i = ll_row TO this.rowcount()
IF ll_i = ll_row THEN
ll_end_child1 = ll_i
ELSE
IF this.object.row_type[ll_i] = 'Grandchild' THEN
ll_end_child1 = ll_i
ELSE
// Grandchilds are sequential so any other row_type indicates the end
EXIT
END IF
END IF
NEXT
IF ll_end_child1 > ll_row OR ll_end_child1 = ll_row THEN
ll_dragrows = ll_end_child1
END IF
// now the rows to move are from ll_row to ll_dragrows
// need to check if we are staying within the same Parent or not
// now get row of Child_1
ll_original_parent = getitemnumber(ll_row,'row_group_id')
IF row > 0 THEN
ll_new_parent = getitemnumber(row,'row_group_id')
CHOOSE CASE ls_dest_rowtype
CASE 'Child_1'
//
// ok to move row since 'target' is a Child_1
IF ll_new_parent <> ll_original_parent THEN
FOR ll_i = ll_row to ll_dragrows
this.object.row_group_id[ll_i] = ll_new_parent
NEXT
END IF
ll_prev_row = row
CASE 'Grandchild'
// Grandchilds always bound to an Child_1
ls_find = "row_type = 'Child_1'"
ll_prev_row = this.find( ls_find, row, 1) // search up list to Child_1 this Grandchild is bound to
// Grandchild row has same Parent id as it's Child_1
IF ll_new_parent <> ll_original_parent THEN
FOR ll_i = ll_row to ll_dragrows
this.object.row_group_id[ll_i] = ll_new_parent
NEXT
END IF
CASE 'Child_2'
// Child_2s are after the last Child_1/Grandchild grouping
// search up list to find last Child_2 for the target's Parent
FOR ll_i = row TO 1 STEP -1
IF this.object.row_type[ll_i] <> 'Child_2' THEN
ll_prev_row = ll_i + 1 // row number of the last Child_2 bound to the Parent of the target row
EXIT
END IF
NEXT
// Child_2 row has same Parent id as the Child_1
IF ll_new_parent <> ll_original_parent THEN
FOR ll_i = ll_row to ll_dragrows
this.object.row_group_id[ll_i] = ll_new_parent
NEXT
END IF
CASE 'Parent'
// Parent is at start of a group. put dragged Child_1/Grandchild group right after the Parent (seq_no = 1)
IF row = 1 THEN
ll_prev_row = 2
IF ll_new_parent <> ll_original_parent THEN
FOR ll_i = ll_row to ll_dragrows
this.object.row_group_id[ll_i] = ll_new_parent
NEXT
END IF
ELSE
//put as first Child_1
ls_find = "row_type = 'Child_1'"
ll_child1_row = this.find( ls_find, row + 1, this.rowcount() + 1) // search down list to Child_1
ls_find = "row_type = 'Child_2'"
ll_child2_row = this.find( ls_find, row + 1, this.rowcount() + 1)
ls_find = "row_type = 'Parent'"
ll_parent_row = this.find( ls_find, row + 1, this.rowcount() + 1)
IF row + 1 = ll_parent_row THEN ll_prev_row = ll_parent_row // next item was another Parent
IF row + 1 = ll_child2_row THEN ll_prev_row = ll_child2_row // next item was an Child_2
IF row + 1 = ll_child1_row THEN ll_prev_row = ll_child1_row // next item was an Child_1
IF ll_prev_row = 0 THEN ll_prev_row = row + 1 // no next item so adding to end of list
// Grandchild row has same Parent id as it's Child_1
IF ll_new_parent <> ll_original_parent THEN
FOR ll_i = ll_row to ll_dragrows
this.object.row_group_id[ll_i] = ll_new_parent
NEXT
END IF
END IF
END CHOOSE
ELSE
//put as first Child_1
ls_find = "row_type = 'Child_1'"
ll_child1_row = this.find( ls_find, this.rowcount(), 1 ) // search up list to Child_1
ls_find = "row_type = 'Child_2'"
ll_child2_row = this.find( ls_find, this.rowcount(), 1 )
ls_find = "row_type = 'Parent'"
ll_parent_row = this.find( ls_find, this.rowcount(), 1)
IF this.rowcount() = ll_parent_row THEN ll_prev_row = ll_parent_row + 1 // next item was another Parent
IF this.rowcount() = ll_child2_row THEN
// search upwards to find first Child_2
DO UNTIL ll_child2_row = 0
IF this.object.row_type[ll_child2_row - 1] = 'Child_2' THEN
ll_child2_row --
ELSE
EXIT // next row up is not a Child_2 which means we have found the first one for the Parent
END IF
LOOP
ll_prev_row = ll_child2_row
END IF
IF this.rowcount() = ll_child1_row THEN
ll_prev_row = ll_child1_row + 1 // next item was an Child_1
ELSEIF ll_prev_row = 0 THEN
ls_find = "row_type = 'Grandchild'" // last Child_1 has associated Grandchilds
ll_find_row = this.find( ls_find, this.rowcount(), ll_child1_row ) // search up list to Child_1
IF this.rowcount() = ll_find_row THEN ll_prev_row = ll_find_row + 1
END IF
IF ll_prev_row = 0 THEN ll_prev_row = row + 1 // no next item so adding to end of list
END IF
END IF
// move the rows
rowsmove( ll_row, ll_dragrows, Primary!, THIS, ll_prev_row , Primary!)
CASE 'Grandchild'
// Grandchilds are bound to a Child_1 - they cannot be moved to other Child_1s from this window
// can only drag within the same Child_1
ll_dragrows = ll_row
// need to check if we are staying within the same Parent or not
// now get row of Child_1
ls_find = "row_type = 'Child_1'"
ll_start_child1 = this.find(ls_find, ll_row, 1) // search up to get the Child_1 the Grandchild is bound to
// get last row of Grandchilds for this Child_1
FOR ll_i = ll_row TO this.rowcount()
IF this.object.row_type[ll_i] = 'Grandchild' THEN // change in row_type indicates an end to the group
ll_end_child1 = ll_i
ELSE
EXIT
END IF
NEXT
IF ll_start_child1 + 1 = ll_end_child1 THEN RETURN
// can only drag within same Child_1
IF row > ll_end_child1 OR row < ll_start_child1 THEN RETURN
IF row = ll_start_child1 THEN
ll_prev_row = ll_start_child1 + 1
ELSE
ll_prev_row = row
END IF
rowsmove( ll_row, ll_dragrows, Primary!, THIS, ll_prev_row , Primary!)
CASE 'Child_2'
// Child_2 rows are always at the end under their corresponding Parent (after any Child_1s and Grandchilds)
ll_dragrows = ll_row // only one row is moved
ll_original_parent = getitemnumber(ll_row,'row_group_id')
IF row > 0 THEN
ll_new_parent = getitemnumber(row,'row_group_id')
CHOOSE CASE ls_dest_rowtype // what kind of row item is dropped on
CASE 'Child_1','Grandchild','Parent'
// Child_2s are last after all Child_1/Grandchild groups
ll_child1_row = row
DO UNTIL ll_child2_row > 0
ll_child1_row ++ // looking at next line
IF ll_child1_row >= this.rowcount() THEN ll_child2_row = this.rowcount() + 1 // nothing after this row so add to end
IF this.object.row_group_id[ll_child1_row] <> ll_new_parent THEN ll_child2_row = ll_child1_row// last item on list for this Parent
IF this.object.row_type[ll_child1_row] = 'Child_2' THEN ll_child2_row = ll_child1_row // item is an Child_2, put this one in front
LOOP
ll_prev_row = ll_child2_row
CASE 'Child_2'
ll_prev_row = row
END CHOOSE
ELSE
ll_prev_row = this.rowcount() + 1 // end of entire list
ll_new_parent = this.object.row_group_id[this.rowcount()]
END IF
IF ll_original_parent <> ll_new_parent THEN
// set new Parent id on row
this.object.row_group_id[ll_row] = ll_new_parent
END IF
rowsmove( ll_row, ll_dragrows, Primary!, THIS, ll_prev_row , Primary!)
END CHOOSE // rowtype being dragged
// renumber
FOR ll_i = 1 to Rowcount()
THIS.object.row_new[ll_i] = ll_i
NEXT
ll_row = getselectedrow(0)
this.scrolltorow(ll_row)
END CHOOSE // drag source
Datawindow Setup
The example has an external datasource. The columns are:
table(column=(type=number updatewhereclause=yes name=row_original dbname="row_original" ) column=(type=number updatewhereclause=yes name=row_new dbname="row_new" ) column=(type=char(16) updatewhereclause=yes name=row_type dbname="row_type" ) column=(type=number updatewhereclause=yes name=row_id dbname="row_id" ) column=(type=number updatewhereclause=yes name=row_parent_id dbname="row_parent_id" ) column=(type=char(1) updatewhereclause=yes name=row_drag dbname="row_drag" ) column=(type=number updatewhereclause=yes name=row_indent dbname="row_indent" ) column=(type=number updatewhereclause=yes name=row_group_id dbname="row_group_id" ) sort="row_original A " )
The data in the datawindow is:
data( 1, 0,"Parent", 100, 0,null 0, 100, 2, 0,"Child_1", 200, 100,null 1, 100, 3, 0,"Child_1", 201, 100,null 1, 100, 4, 0,"Grandchild", 300, 201,null 2, 100, 5, 0,"Grandchild", 301, 201,null 2, 100, 6, 0,"Child_2", 400, 100,null 1, 100, 7, 0,"Parent", 101, 101,null 0, 101, 8, 0,"Parent", 102, 0,null 0, 102, 9, 0,"Child_1", 202, 102,null 1, 102, 10, 0,"Grandchild", 302, 202,null 2, 102, 11, 0,"Grandchild", 303, 202,null 2, 102, 12, 0,"Child_1", 203, 102,null 1, 102, 13, 0,"Grandchild", 304, 203,null 2, 102, 14, 0,"Child_2", 401, 102,null 1, 102, 15, 0,"Child_2", 402, 102,null 1, 102,)
The detail band has an expression on the Color property:
color="536870912~tcase( row_type when 'Parent' then 1095774 when 'Child_2' then 4991183 when 'Child_1' then 47834 when 'Grandchild' then 2519531 else 536870912)"
There are two Line objects in the detail band. These form the indicator for drag/drop:
line(band=detail x1="9" y1="88" x2="2395" y2="88" name=l_bottom visible="0~tIF (row_drag = 'B',1,0)" pen.style="0" pen.width="14"... line(band=detail x1="9" y1="4" x2="2395" y2="4" name=l_top visible="0~tIF ( row_drag = 'T',1,0)" pen.style="0" pen.width="14"...
The attached PB11.5 DragDrop example contains the Powerbuilder 11.5 objects and exports.
You might also be interested in