Friday, October 26, 2012

Implementing Drag and Drop from your application to Windows Explorer

Getting PowerBuilder to accept file drops from Windows Explorer is fairly straightforward.  Getting it to support drag and drop of files from your application to Windows Explorer is actually fairly straightforward as well, but not entirely obvious.  We're going to start looking at how to do with starting with the .Net target types (WPF and .Windows Form) and then look to see how we might use the same technique for Classic Win32 applications.




WPF

What makes it fairly straightforward in WPF is the Systems.Windows.DragDrop class, a stand alone class that takes care of all the tricky details for us.  All we need to do is create an instance of System.Windows.DataObject class, load it up with the file names that we want to send Windows Explorer, and then pass it along to the DoDragDrop method of the DragDrop class along with the drag source and an enumerated value from System.Windows.DragDropEffects to indicate what we're doing with the files (e.g., copy).

Create a WPF target in PowerBuilder.Net and a WPF window that it open.  Then drop a native WPF control (not a PowerBuilder control) onto the WPF window, perhaps a ListView control.  We're using a native rather than a PowerBuilder control because the DoDragDrop method doesn't recognize the PowerBuilder control as a potential drag source.  I've attempted to try to get it to recognize the InnerControl property of the PowerBuilder control, and the script compiles, but it doesn't seem to work.  If I figure that part out, I'll update this blog.

If you've got a clean install of PB.Net, you probably don't have native WPF controls on your toolbox yet.  Simply right click in the toolbox and select "Add Tab".  You might name the new tab something like "Native WPF Controls".  Then within that new tab right click and select "Add Items..."  A dialog will appear that shows you all the WPF controls that the operating system knows are installed.  Select a few appropriate ones, hit OK, and they'll be in your toolbar for future use.

You also need to add System.Windows.Forms to the referenced assemblies because we're going to use some classes from it.  To make things a bit simpler when we code, go to the Usings for the Window an add the following references:

     System.Windows
     System.Windows.Input
     System
     System.Collections.Specialized

That means we won't need keep adding that namespace info when we use classes from those namespaces.

Let's create an instance variable in the window now to keep track of when the user initially left clicked on the control as follows:

     Point start

Now let's find the PreviewMouseLeftButtonDown event for the control we added and add this to that script:

     IInputElement elm = sender
     start = e.GetPosition(elm)

Now we're tracking the location of the mouse when the user left clicked.  The next thing we need to do is check to see whether the user has moved the mouse the minimum distance required to initiate a drag and drop operation.  We'll do that in the MouseMove event, where you can add the following script:

     IInputElement elm = sender
     Point mpos = e.GetPosition(elm)

      if ( e.LeftButton = MouseButtonState.Pressed! and &
                    System.Math.Abs(start.X - mpos.X) > SystemParameters.MinimumHorizontalDragDistance and &
                    System.Math.Abs(start.Y - mpos.Y) > SystemParameters.MinimumVerticalDragDistance) then
                    StringCollection files
                    files = create StringCollection()
                    files.Add("C:\Users\bruce\Documents\PB12\DragDrop\wpfapp.out\simple_img_1.jpg")
            DataObject dataObject
            dataObject = create DataObject()
            dataObject.SetFileDropList(files)
            System.Windows.DragDrop.DoDragDrop( this, dataObject, DragDropEffects.Copy!)
     end if    

What we're doing here is getting the current location of the mouse as it's being moved.  We're checking to make sure that the user is still holding down the left mouse button (otherwise it's no longer a drag operation).  And we're looking to see if the user has move the mouse in both the X and Y axis more than the amount that is defined in the System Parameters as the minimum to initiate the drag and drop operation.  By default that is 4 pixels, but the user can change that.

If all that is true, we create a System.Collections.Specialized.StringCollection object and add one or more file names to it (one just for this example, and you'll want to point it to an actual file on your system).  We then create the System.Windows.DragObject and give it the list of files.  Finally, we call the DoDragDrop method on the DragDrop class, referencing our native WPF control as the drag source, pass in the DataObject and tell it we're doing a copy.  We're including the full namespace reference for the DragDrop class to ensure that PowerBuilder doesn't confuse it with it's own DragDrop event.

That's all there is to it.  Run your app and then attempt to drag from the native WPF control to the desktop or a directory in Windows Explorer and you should see the file copied to that location.

Windows Forms

Windows Forms is only a slightly different variation on the same approach.  Windows Forms doesn't have the DragDrop class, but the Windows Forms Control base class does have a DoDragDrop method that accomplishes the same thing.  To make this demo a bit simpler, we're going to implement the code directly on the clicked method of the control.  I'll leave adding in the logic to check for the correct amount of mouse movement as an exercise for the student.

So create a Windows Form target in PowerBuilder Classic, create a window for it to open, and drop some PowerBuilder control on the window (perhaps a listview again).  You're also going to want to add references to the System.Windows.Forms.dll and PresentationCore.dll in the target.  (Right click on the target, select .Net Assemblies

The code is going to look subtly different in the Windows Form application than it did in WPF.

#if defined pbdotnet then
          System.Collections.Specialized.StringCollection files
          files = create System.Collections.Specialized.StringCollection
          files.add ( "C:\Users\bruce\Documents\PB12\DragDrop\simple_img_1.jpg" )
          System.Windows.Forms.DataObject dataObject
          dataObject = create System.Windows.Forms.DataObject()
          dataObject.SetFileDropList ( files )
          System.Windows.Forms.Control control
          control = create System.Windows.Forms.Control
          control.DoDragDrop(dataObject, System.Windows.Forms.DragDropEffects.Copy)
#end if

The first big difference of course is that the code is wrapped in an "#if defined pbdotnet then" and "#end if" block.  That's what tells PowerBuilder Classic that the code in question isn't standard PowerScript, it's a variation of PowerScript that the syntax checker ignores and is handled directly by the PowerScript to C# compiler to handle references to .Net classes.

We're also using a slightly different class (System.Windows.Forms.DataObject rather than System.Windows.DataObject) to store the data.  We also need a System.Windows.Forms.Control to call the DoDragDrop method on.  Unfortunately, like with WPF, the compiler doesn't recognize the PowerBuilder control as a valid control for that.  However, it turns out we can simply declare an instance of that class within the script and use that.

Once again, that's all there is too it.  You should be able to drag the reference file to the desktop or a directory in Windows Explorer by dragging from the PowerBuilder control to either of those.

Win32 Classic

Well, if we can declare a control within the script to invoke the drag and drop in a Windows Forms target, we should be able to do the same thing from a .Net assembly and then just use that from a PowerBuilder Classic Win32 application.  And it turns out we can.  That's good, because doing it using the Windows API methods is a bit involved.

First, let's got back to PowerBuilder.Net, because we're going to use that to create the .Net assembly.  Create a .Net assembly target there, with custom nonvisualobject as the object type we're going to create.  Add System.Windows.Forms to the referenced assemblies, because we'll be using classes from that.

Create a method on the nonvisualobject (I called mine dodragdrop ) which take an argument of type string called filename and then add the following code:

System.Collections.Specialized.StringCollection files
files = create System.Collections.Specialized.StringCollection
files.add ( filename )
string dataFormat = System.Windows.Forms.DataFormats.FileDrop
System.Windows.Forms.DataObject dataObject
dataObject = create System.Windows.Forms.DataObject()
dataObject.SetFileDropList ( files )
System.Windows.Forms.Control control
control = create System.Windows.Forms.Control
control.DoDragDrop(dataObject, System.Windows.Forms.DragDropEffects.Copy!)

You can pass more than one file at a time through the StringCollection, I'm only passing one here for demo purposes.  You might expand on this example to support multiple files on your own.  The code is essentially what we used within the Windows Forms application.

Go back to the project object for the target now, because you have to tell it on the Objects tabs that you want to expose the method you just created as public in the assembly.  While you're there, you might want to modify the Class name and Object name to give them more non-PowerBuilder type names.  Those will just be aliases for the real methods when they are exposed in the assembly.

While you're in the project, go to the Sign tab as well.  If we're going to load the assembly to the GAC and let PowerBuilder classic access it there it must be signed.  You can't add unsigned assemblies to the GAC.  If we decide to leave the file in a windows directory and reference it at that location, it is still highly recommended to sign the assembly.  In fact the REGASM utility that creates the registry entries PowerBuilder Classic will use to access the assembly will warn you if the assembly is not signed and ask you to do it.

Signing the assembly is as simply as clicking the "Sign the Assembly" checkbox, and then hitting "New" so that PowerBuilder will generate a strong name key file for you.  Now compile the assembly.

Hopefully you're using PowerBuilder 12.5, because it by default makes the classes within it's assemblies COM visible.  We need that in order for PowerBuilder Classic to use them in an Win32 target.  If you're using an earlier version of PowerBuilder.Net there are ways to mark their assemblies as COM Visible, but it's beyond the scope of this blog entry.

What we need to do now is run regasm.exe on the assembly to generate the registry entries that PowerBuilder Classic needs.  For purposes of this demo, run it as follows:

     regasm.exe <name of your assembly>.dll /codebase /regfile:<name of your assembly>.reg

Codebase tells regasm to embed the physical location of the file in the registry entries, so we don't need to worry about adding it to the GAC.  The /regfile option tells regasm to write the entries out to a file rather than load them directly to the registry.  We may need to do that because we might need to modify the file.

The issue occurs when you are on a 64 bit operation system.  If you're still on a 32 bit operatin system, you can skip this paragraph.  What happens is that, depending on what particular regasm file you run (there are several on your machine) the utility may have created 64 bit registry entries rather than 32 bit entries.  The easy way to tell the differences is that 32 bit entries on a 64 bit system have Wow6432Node as part of the key name.  That is, most of the entries will be under HKEY_CLASSES_ROOT\Wow6432Node\CLSID instead of just HKEY_CLASSES_ROOT\CLSID.  If you are on a 64 bit system and regasm left Wow6432Node out of the key names, add them before adding the entries to your registry.

To add the entries to your registry, we just need to double click on the file.  You'll also want to examine the file to determine what ProgID was given to the class.  In my case, the ProgID was "pbdragdrop.PBDragDrop".  We'll need that information in just a bit.

Let's open up PowerBuilder classic, create a standard application target, a window that it opens and a control that we want to initiation the drag and drop from.  Once again, I've chosen a listview and for simplicity we'll just code the clicked event.  The code you want to add there looks something like this:

integer  li_rc
oleobject loo
loo = create oleobject
li_rc = loo.ConnectToNewObject ( "pbdragdrop.PBDragDrop" )
loo.DoDragDrop ( "C:\Users\bruce\Documents\PB12\DragDrop\simple_img_1.jpg" )
loo.DisconnectObject()
Destroy loo

It's fairly simple.  We just create an oleobject and tell it to establish a connection to our assembly using the ProgID we obtained earlier.  Then we just call the method on the class (your method name will probably differ) to implement the drag and drop.  At that point we can disconnect from the oleobject and destroy it.  The .Net Framework handles everything for us under the covers, allowing us to treat the assembly like a COM object.  If you want more information on it, you might check out the documentation on the Microsoft site, or some of the numerous articles I've written on the technique for PBDJ.

Summary

That's it.  Drag and drop from PowerBuilder applications to Windows Explorer for 3 different target types.  And particularly in the case of Win32 targets, using what I would consider to be a much simpler method than what is required to implement it using the native Windows API.  Hope you find it useful.

No comments: