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