Implementing Drag and Drop

by Mike Hanson

Published 1999-01-01    Printer-friendly version

Download the code here

There are all sorts of cool Windows features that Clarion can achieve, but many of these seem more difficult than they need to be, compared with Clarion's regular, impressive ease-of-use. One of these is "Drag-and-Drop" or "D&D". That's where you drag something from one place, and drop it onto a different control (or sometimes even the same control if it's a List).

The most common example is dragging from one list of items to another. If you are hand coding all of your list boxes, then adding support for D&D is no big deal. However, most of us are using the BrowseBox template to produce our list boxes, and adding D&D can be a little tricky. Before we get too far into such a complex implementation, let's look at the basics of D&D. There are two properties and three events that are used to monitor this activity:

PROP:DragID – This control attribute indicates that you will be dragging something from this control.

PROP:DropID – This control attribute indicates that you will be dropping something on this control. The D&D operation is only valid if you are dragging something from another control where the other control's PROP:DragID matches this controls PROP:DropID.

Notice that I've used the generic term "control", and not "list box". This is because you can drag and drop from almost any control type to almost any other control type. The only exception that I can recall is that you can't drag from a REGION, but you can drop on something on one. This does make sense, because a region represents a "place" rather than a "thing", whereas most other controls are both "things" and "places". Additionally, list boxes are containers for things.

Something else you should recognize is that a control can have multiple PROP:DragIDs and PROP:DropIDs. This means that you could have multiple types of D&D operations. If you drag from one control to another it means one thing, and these two controls would have a matching DragID (from) and DropID (to). Meanwhile, dragging something from the first control to a different control means something else, and the first control would have an additional DragID matching the third control's DropID. For example:

     ?List1:  DragID = A,B
     ?List2:    DropID = A
     ?List3:    DropID = B

You can also have multiple controls with the same DragID and DropID. This would indicate that the items could be dragged from and to multiple locations around the window. Also, your "From" and "To" controls don't have to be on the same window. This can get pretty complex, and most implementations are of the simpler form; i.e., one-way or bidirectional.

By setting these properties, Clarion will automatically do the following for you:

  • When the user begins dragging from a control with a DragID, the cursor is changed the "dragging" cursor, and EVENT:Dragging occurs for the control.
  • While the mouse cursor is being dragged around the screen, the cursor automatically changes as it moves on and off controls whose DropID matches the original control's DragID.
  • If the user releases the mouse button while not over a valid drop site, then nothing else happens. The mouse cursor is returned to its normal state.
  • If the user releases the mouse button over a valid drop site, then two events occur: EVENT:Drag is posted to the "from" control, then EVENT:Drop is posted to the "to" control.
  • If the user releases the mouse button over an invalid drop site, then only the EVENT:Drag is posted, with DRAGID() returning a blank string.

It is up to us to handle these events. EVENT:Dragging is used to change the mouse cursor to something other than the generic "Can't Drop Here" cursor. In most cases, you won't change this. We're more concerned with the other two events: EVENT:Drag and EVENT:Drop.

Think of EVENT:Drag as "picking something up from here", and EVENT:Drop as "dropping the thing there". With EVENT:Drag you must remember what you've picked up, and maybe remove it from the original location. With EVENT:Drop, you must add it to the destination. Remember, though, that this is done in two stages.

However, if you have only one Drag control for a given Drop, then you can cheat a little. The Drag and Drop events always come together. Therefore, you sometimes have the option of using one or the other. In our example we'll use both events when dragging in one direction, and only the Drag event for the other direction.

You must use a local variable to remember the "something". If you are dragging from one window to another within the same program, you must use a global variable (non-threaded). If you are dragging from one program to another you need to use something like a field in a data file or an INI file. For the purpose of this article, we're going to concentrate on dragging from one list box to another on the same window. Remember that the techniques can be utilized in all of these other situations too.

The Sample Application

Let's define our test case further: We're creating a system to track employee skills. These skills come from a predefined pool of skills that are of interest to the company. On the employee update form we want to have a browse of all possible skills, and another with the skills of the employee currently being edited. In addition, if the employee has a particular skill, then we don't want to see that skill included in the list of all possible skills.

To achieve this we require three files: Employee, Skill, and EmployeeSkill. When an Employee has a Skill, the EmployeeSkill file will contain a record with a pointer to the Employee file, and another to the Skill file. (This is a standard many-to-many relationship.) I suggest that you download the sample application and look through it as you read this article. The files will look something like this:

Employee          FILE,PRE(Emp),RECLAIM,CREATE
SysIDKey            KEY(Emp:SysID),UNIQUE,PRIMARY
NameKey             KEY(Emp:LastName,Emp:FirstName),NOCASE,OPT
Record              RECORD
SysID                 LONG
FirstName             STRING(20)
LastName              STRING(30)
                    END
                  END
Skill             FILE,PRE(Ski),RECLAIM,CREATE
SysIDKey            KEY(Ski:SysID),UNIQUE,PRIMARY
NameKey             KEY(Ski:Name),UNIQUE,NOCASE,OPT
Record              RECORD
SysID                 LONG
Name                  STRING(80)
                    END
                  END
EmployeeSkill     FILE,PRE(EmpSki),RECLAIM,CREATE
EmployeeSkillKey    KEY(EmpSki:EmployeeSysID,EmpSki:SkillSysID),UNIQUE,PRIMARY
EmployeeKey         KEY(EmpSki:EmployeeSysID)
SkillKey            KEY(EmpSki:SkillSysID)
Record              RECORD
EmployeeSysID         LONG
SkillSysID            LONG
                    END
                  END

Notice that the EmployeeSkill file seems to have a redundant key. We've got both EmployeeKey and EmployeeSkillKey. The first is used in browses to display an employee's skills. There are no field components beyond the EmployeeSysID, which makes it possible to use the "Additional Sort Fields" to display the entries by Ski:Name. The second is used to perform a "quick hit" on the file for filter purposes, as well as ensuring that there are no duplicates. I'll be expanding on these issues (i.e. overriding browse orders) in a future article. For now, we need both keys.

On the Employee update form you'll have two browses. For the Skill browse to include only those skills that the employee does not have, you must add code to the ValidateRecord method to lookup an EmployeeSkill record for the current skill and employee. Don't forget to make the Ski:SysID into a Hot Field in the Browse settings. The code would look like this:

EmpSki:EmployeeSysID = Emp:SysID
EmpSki:SkillSysID = Ski:SysID
IF ~Access:EmployeeSkill.TryFetch(EmpSki:EmployeeSkillKey)
  RETURN Record:Filtered
END

Regarding our earlier discussion of the "redundant" EmpSki:EmployeeSkillKey, if we did not have it, we would have to build a VIEW structure to access the file to determine if there was a matching record. (We could manually walk through the file with a SET+NEXT, but that's old-fashioned <g>.)

The EmployeeSkill browse is easier to implement, as it uses a regular Range to display records with a File Relationship involving the Employee file.

Hand Coded Drag & Drop

Here's what we need to do to add D&D to the window the "hard" way:

  1. Add a local LONG variable called "Loc:SkillSysID".
  2. Go into the control properties for the Skill browse list box, and specify that the DragID is "InsertSkill", and the DropID is "DeleteSkill".
  3. Go into the control properties for the EmployeeSkill browse list box, and specify that the DragID is "DeleteSkill" and the DropID is "InsertSkill". (This means that you can assign skills to the employee by dragging from the Skill browse to the EmployeeSkill browse, and remove the skill using the opposite action.
  4. Place the following code into the Skill browse's Drag event embed. In this case "SkillBrowse" is the name of the Browse class object, as specified on the Classes tab for the browse extension.
IF DRAGID() = 'InsertSkill'
  SkillBrowse.UpdateViewRecord
  Loc:SkillSysID = Ski:SysID
END!IF
  1. In the Embeds window for the EmployeeSkill browse, place the following code into the embed for the Drop event :
IF DROPID() = 'InsertSkill'
  Access:EmployeeSkill.PrimeRecord
  EmpSki:EmployeeSysID = Emp:SysID
  EmpSki:SkillSysID = Loc:SkillSysID
  IF Access:EmployeeSkill.Insert()
    Access:EmployeeSkill.CancelAutoInc
  ELSE
    ThisWindow.Reset(1)
  END
END
  1. Then, in the EmployeeSkill browse's Drag event embed, place the following code. As I mentioned earlier, we are handling the complete operation in the Drag event, and the Drop event will be ignored. "EmployeeSkillBrowse" is the name of the Browse class object.
IF DRAGID() = 'DeleteSkill'
  EmployeeSkillBrowse.UpdateViewRecord
  IF ~Relate:EmployeeSkill.Delete(0)
    ThisWindow.Reset(1)
  END
END
  1. In addition to these bits of code I also turned on the "Hide" attribute for both list boxes, so that the selector bar is only visible when the list control has focus.

Template Driven Drag & Drop

The template must do the same things that you did yourself, but it should look at the whole concept of D&D in a generic fashion. It may be that you need multiple D&D templates to handle different situations. If possible, though, we're going to make each template handle as many special cases as possible, without making them too weird to use.

Let's look at our test case. We can define the template as "implementing drag and drop between two browses on a window. One browse represents one side of the MANY:MANY relationship, while the opposite file in the relationship is 'fixed'. The second browse represents records in a 'join' file between the two other files. This template will add and delete records to that file". It needs to know the following:

  1. From which browse control? -- %FromControl
  2. To which to browse control (i.e. "Join" file)? -- %ToControl
  3. Other "Constant" file (i.e. third file in the many-to-many relationship)? -- %FixedFile
  4. Dragging in both directions supported? -- %Bidirectional

The rest of the information can be derived from this. The %FromControl browse indirectly specifies the Anchor file (e.g. Skill), while the %ToControl specifies the Join file (e.g. EmployeeSkill). In our example we could have determined the %FixedFile automatically, because we're doing this in a Form procedure. However, we can make this much more flexible by asking the question. Once we know the two controls, we can determine all of the file and field names as necessary. This is what the template prompts will look like:

#EXTENSION(mhDragDropM2M,'Drag & Drop|
(MANY:MANY)'),DESCRIPTION('[MikeH] Drag & Drop between ' &|
 %FromControl & ' and ' & %ToControl),MULTI,PROCEDURE
#BOXED('')
  #PROMPT('From Anchor Browse:', FROM(%Control, |
%ControlTemplate='BrowseBox(ABC)')), %FromControl, REQ
  #PROMPT('To Join Browse:', FROM(%Control, |
%ControlTemplate='BrowseBox(ABC)')), %ToControl, REQ
  #PROMPT('Other Anchor File:', FILE), %FixedFile, REQ
  #PROMPT('Drag in both directions', CHECK), %Bidirectional, |
DEFAULT(%True)
#ENDBOXED

Now let's determine the rest of the stuff that we'll need in the #ATSTART and #AT(%GatherSymbols) sections. I split this up, because the Browse extensions need to initialize their symbols before I peek at them. %GatherSymbols occurs after the Browse has initialized.

#ATSTART
  #DECLARE(%FromFile)
  #DECLARE(%FromObject)
  #DECLARE(%ToFile)
  #DECLARE(%ToObject)
  #EQUATE(%InstancePrefix, 'MHDD'& %ActiveTemplateInstance &':')
#ENDAT
#!-----
#AT(%GatherSymbols)
  #CALL(%DDGetBrowseSettings, %FromControl, %FromFile, %FromObject)
  #CALL(%DDGetBrowseSettings, %ToControl, %ToFile, %ToObject)
  #FIX(%File, %FixedFile)
  #FIX(%Relation, %ToFile)
  #IF(%FileRelationType <> '1:MANY')
    #ERROR(%Procedure &' (Drag&Drop): '& %FixedFile &' and '& |
%ToFile &' are not related 1:MANY')
  #ENDIF
  #FIX(%File, %FromFile)
  #FIX(%Relation, %ToFile)
  #IF(%FileRelationType <> '1:MANY')
    #ERROR(%Procedure &' (Drag&Drop): '& %FromFile &' and '& |
%ToFile &' are not related 1:MANY')
  #ENDIF
#ENDAT

Our %DDGetBrowseSettings group is responsible for checking the settings of a Browse template to determine its primary file and class object. These values are part of an "unrelated" template. Therefore, we must use #ALIAS to grab the value of %ManagerName, and we have to change the #ActiveTemplate and %ActiveTemplateInstance to get the %Primary file. Note that the PRESERVE attribute on the #GROUP will restore the %ActiveTemplate settings after the group is finished. (We used to do this manually.)

#GROUP(%DDGetBrowseSettings,%Ctl,*%F,*%O),PRESERVE
#FIX(%Control, %Ctl)
#ALIAS(%M, %ManagerName, %ControlInstance)
#SET(%O, %M)
#FIX(%ActiveTemplate, %ControlTemplate)
#FIX(%ActiveTemplateInstance, %ControlInstance)
#SET(%F, %Primary)

We need to create the local variables to remember the current entry as it is being dragged across the window.

#AT(%DataSection)
  #FIX(%File,%FromFile)
  #FIX(%Relation,%ToFile)
  #FOR(%FileKeyField)
%[20](%InstancePrefix&':'&%FileKeyField) LIKE(%FileKeyField)
  #ENDFOR
#ENDAT

Now we must initialize the necessary control attributes after the window is opened:

#AT(%WindowManagerMethodCodeSection, 'Init', '(),BYTE'), PRIORITY(8030)
%FromControl{PROP:DragID} = '%(%InstancePrefix &'F')'
%ToControl{PROP:DropID} = '%(%InstancePrefix &'F')'
  #IF(%Bidirectional)
%FromControl{PROP:DropID} = '%(%InstancePrefix &'B')'
%ToControl{PROP:DragID} = '%(%InstancePrefix &'B')'
  #ENDIF
%FromControl{PROP:NoBar} = True
%ToControl{PROP:NoBar} = True
#ENDAT

Finally, we must insert the control event handling code:

#AT(%ControlEventHandling, %FromControl, 'Drag'), PRIORITY(7500)
IF DRAGID() = '%(%InstancePrefix &'F')'
  %FromObject.UpdateViewRecord
    #FIX(%File,%FromFile)
    #FIX(%Relation,%ToFile)
    #FOR(%FileKeyField)
  %InstancePrefix:%FileKeyField = %FileKeyField
    #ENDFOR
END
#ENDAT
#!-----
#AT(%ControlEventHandling, %ToControl, 'Drop'), PRIORITY(7500)
IF DROPID() = '%(%InstancePrefix &'F')'
  Access:%ToFile.PrimeRecord
    #FIX(%File,%FixedFile)
    #FIX(%Relation, %ToFile)
    #FOR(%FileKeyField)
  %FileKeyFieldLink = %FileKeyField
    #ENDFOR
    #FIX(%File,%FromFile)
    #FIX(%Relation, %ToFile)
    #FOR(%FileKeyField)
  %FileKeyFieldLink = %InstancePrefix:%FileKeyField
    #ENDFOR
    IF Access:%ToFile.Insert()
      Access:%ToFile.CancelAutoInc
    ELSE
    %WindowManager.Reset(1)
  END
END
#ENDAT
#!-----
#AT(%ControlEventHandling, %ToControl, 'Drag'), PRIORITY(7500), WHERE(%Bidirectional)
IF DRAGID() = '%(%InstancePrefix &'B')'
  %ToObject.UpdateViewRecord
  IF ~Relate:%ToFile.Delete(0)
    %WindowManager.Reset(1)
  END
END
#ENDAT

Conclusion

Here we are again, hopefully with a better understanding of Drag and Drop, as well as a template to save us from needing to understand it <grin>. Of course, this template handles only one D&D scenario, albeit one of the most common. If you encounter others, you should be able to apply the techniques presented here. As usual, you can download the template (along with the rest of my public domain templates) from www.BoxsoftDevelopment.com. Look for MHAB*.EXE.

Printer-friendly version