PowerBuilder – Datawindow Drag/Drop Rows with Business Rules

Posted on Wednesday, December 21st, 2011 at 12:40 pm in

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

Top