1. Jeff Kandt
  2. PowerBuilder
  3. Friday, 4 October 2024 18:28 PM UTC

We've been struggling, like a lot of people apparently, with providing the ability to drag messages and attachments from Outlook into our PowerBuilder app.

We had been using  Dddll.dll from Catsoft, but that stopped working when Outlook went to 64-bit, since we haven't yet migrated our app to 64-bit.

We looked at the clever method of using OLE described here: (https://community.appeon.com/index.php/qna/q-a/drag-and-drop-emails-from-64-bit-outlook) but found it has a number of functional disadvantages. Specifically, Dddll.dll is still used to notify PB when something got dropped from Outlook, but it doesn't communicate exactly what was dropped. So it then makes an OLE connection to Outlook and calls methods to determine what is currently selected within Outlook and then tells Outlook to export the selected message(s).

The problem with this is that what's selected isn't always what was dragged. In the example code, when the user drags attachments out of a message window, the entire message gets exported instead. And if after opening Message 1 the user selects Message 2 in their Inbox before dragging out of the Message 1 window, the Message 2 was exported instead of the one being dragged out of. In trying to fix these issues we found that there are some situations in which it appears to be impossible via OLE to determine what was actually dragged out of Outlook. As one worst-case example: When the user is dragging out of a preview pane in their Inbox, the user may have multiple types of entities selected at once: attachments selected in the preview pane, plus messages selected in the list. Did the user drag the message(s) or attachment(s)?

In the end, we found what I believe to be a better solution that I wanted to share here: A webbrowser object used as a drag target. The page loaded in the webbrowser contains Javascript to access the HTML5 Drag-and-Drop and File APIs, and passes the name and binary data from the dropped files into PowerBuilder by calling a PB event. In order to be called from Javascript, the PB event can only accept a single string argument, so the file data is encoded to base 64.

One thing I ran into was an apparent limit on the size of the string that can be passed into the PB event. To get around that, the javascript "chunks" the file data into 32k pieces and calls the event for each chunk. The PB event then has to re-assemble the chunks.

The new webbrowser control's Chromium-based engine has excellent support for drag-and-drop, with no apparent 32/64 bit issues. It is able to pull messages and attachments out of Outlook, as well as files from File Explorer. As a special bonus in our environment where some users run our app via Citrix, it even supports dragging across the local/remote boundary: You can drag messages and attachments out of a locally running instance of Outlook and into our remotely hosted PB app.

As noted in the post on the OLE method, the "New Outlook" doesn't support drag and drop yet, so until Microsoft fixes this we'll be encouraging our users to switch back to the "old" Outlook if they want any of this to work.

The main disadvantage of this method is that it doesn't enable dropping directly into an existing PB control, such as a datawindow, like we could before. Instead. the browser control must be a separate visible "Drop files here" area on the window, specifically used only as a drag target.  I tried various ways to make the browser control transparent and position it over the PB control so that it looks like you can drop onto an existing control, but couldn't make it work -- please let me know if anyone figures that out.

I've uploaded a zip file containing a sample application on CodeExchange: https://community.appeon.com/index.php/codeexchange/powerbuilder/364-drag-and-drop-from-outlook-using-webbrowser-control-drop-target

 

 

Steps to implement:

1) Create a webbrowser control on your window

2) In the window's open event, navigate the browsercontrol to the static page containing the special javascript. In my example that page is called "dd.html":

browsercontrol_1.Navigate("file://C:\[path]\dd.html")

3) In the browsercontrol's "navigationstart" event, register the PB event to be called from the page's Javascript. In my example the event is called "ue_filedropchunk":

This.RegisterEvent("ue_filedropchunk")

4) Create instance variables on the browsercontrol to allow the event to keep track of file chunks across multiple event calls:

string is_fileName, is_fileData

integer ii_totalChunks

integer ii_receivedChunks

5) Create a "ue_filedropchunk" event on the webbrowser that accepts a single string argument called "as_data":

string ls_fileName, ls_chunkData, ls_decodedBlob

integer li_chunkIndex, li_totalChunksInMessage

long ll_pos1, ll_pos2, ll_pos3, ll_file, ll_row, ll_byteswritten//, ll_stringsize, ll_chunksize, ll_blobsize

CoderObject lco_coder

blob lbl_chunkBlob, lbl_decodedBlob

 

// Parse the incoming string (fileName|chunkIndex|totalChunks|chunkData)

ll_pos1 = Pos(as_data, "|")

ll_pos2 = Pos(as_data, "|", ll_pos1 + 1)

ll_pos3 = Pos(as_data, "|", ll_pos2 + 1)

 

ls_fileName = Left(as_data, ll_pos1 - 1)

li_chunkIndex = Integer(Mid(as_data, ll_pos1 + 1, ll_pos2 - ll_pos1 - 1))

li_totalChunksInMessage = Integer(Mid(as_data, ll_pos2 + 1, ll_pos3 - ll_pos2 - 1))

ls_chunkData = Mid(as_data, ll_pos3 + 1)

 

 // Check if this is the first chunk (initialize global variables)

if li_chunkIndex = 0 then

is_fileName = ls_fileName

is_filedata = ""

ii_totalChunks = li_totalChunksInMessage

ii_receivedChunks = 0

end if

 

// Append the current chunk to the global string

 

is_fileData = is_fileData + ls_chunkData

 

ii_receivedChunks++

 

 // Check if all chunks have been received

if ii_receivedChunks = ii_totalChunks then

// All chunks received, save the file

 

lco_coder = Create CoderObject

lbl_decodedBlob = lco_coder.Base64Decode(is_fileData)

Destroy lco_coder

 

ll_file = FileOpen("C:\temp\" + is_fileName, StreamMode!, Write!)

if ll_file <> -1 then

ll_byteswritten = FileWriteEx(ll_file,  lbl_decodedBlob)

FileClose(ll_file)

//ll_row = dw_1.InsertRow(0)

//dw_1.Object.Files[ll_row] = ls_FileName

else

MessageBox("Error", "Failed to save the file.")

end if

 

// Reset instance variables

is_fileName = ""

is_filedata = ""

ii_totalChunks = 0

ii_receivedChunks = 0

end if

6) Finally, create the html page and save it in the location referenced by the navigation in the window's open event. In my example, the page implements the necessary javascript, and also visually displays a dashed rounded border around the entire page, with an upload icon and "Drop files here" text, to clearly identify the drag target to the user. I'll let you find your own icon file, and adjust to CSS to match your own needs, such as the page background color:

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Centered Image with Caption</title>

    <style>

        html {

            height: 100%;

            margin: 0;

            padding: 8px;

            width: 100%;

            box-sizing: border-box;

}

body {

   background-color: LightGrey;

            height: 100%;

            margin: 0;

            padding: ;

            width: 100%;

            display: flex;

            justify-content: center;

            align-items: center;

            border: 3px dashed DarkSlateGrey;

border-radius: 15px;

        }

 

        .container {

            text-align: center;

            width: 100%;

        }

 

        .centered-image {

            max-height: 80px;

            height: 100vh;

            width: auto;

            max-width: 80%;

   opacity: .5;

        }

 

        .caption {

            margin-top: 5px;

            margin-bottom: 5px;

            font-size: 24px;

            font-family: 'Arial', sans-serif;

            font-weight: bold;

            color: #333;

   opacity: .8;

        }

    </style>

</head>

<body>

    <div id="drop-zone" class="container">

        <img src="/cloud_arrow_down_icon.png" alt="Cloud Image" class="centered-image">

        <p class="caption">Drop files here</p>

    </div>

<script>

        // Function to handle file drop event

        function handleDrop(event) {

            event.preventDefault();  // Prevent default behavior

            const files = event.dataTransfer.files;  // Get dropped files

            if (files.length > 0) {

                for (let i = 0; i < files.length; i++) {

                    let file = files[i];

                    readAndSendFile(file);  // Read and send each file

                }

            }

        }

 

const CHUNK_SIZE = 32000;  // Set chunk size to 32 KB (or smaller, depending on the limit)

 

// Function to read the file and send it to PowerBuilder

function readAndSendFile(file) {

const reader = new FileReader();

 

reader.onload = function(e) {

const base64Content = e.target.result.split(",")[1];  // Extract base64 content

const fileName = file.name;

const totalChunks = Math.ceil(base64Content.length / CHUNK_SIZE);

 

// Send the file in chunks

for (let i = 0; i < totalChunks; i++) {

const chunk = base64Content.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);

const chunkData = `${fileName}|${i}|${totalChunks}|${chunk}`;  // Include chunk index and total chunks

 

// Send each chunk to PowerBuilder

window.webBrowser.ue_filedropchunk(chunkData);  // Send chunk data to PowerBuilder

}

};

 

// Read the file as a Base64 string

reader.readAsDataURL(file);

}

 

 

        // Prevent default behavior for dragover event

        function handleDragOver(event) {

            event.preventDefault();

        }

 

        // Initialize drag-and-drop zone

        window.onload = function() {

            const dropZone = document.getElementById('drop-zone');

            dropZone.addEventListener('dragover', handleDragOver);  // Set dragover event

            dropZone.addEventListener('drop', handleDrop);  // Set drop event

        };

 

   </script>

</body>

</html>

…That's it! I think I've included all the necessary pieces, but let me know if you have any issues. And I'm sure this can be improved upon, so please share anything you discover.

Attachments (1)
Who is viewing this page
Jeff Kandt Accepted Answer Pending Moderation
  1. Saturday, 5 October 2024 22:23 PM UTC
  2. PowerBuilder
  3. # 1
Comment
There are no comments made yet.
John Fauss Accepted Answer Pending Moderation
  1. Friday, 4 October 2024 18:55 PM UTC
  2. PowerBuilder
  3. # 2

VERY nice, Jeff! A very creative solution. Well done!

Would you please consider creating a small, example app that implements this and post it in The CodeXchange portion of the Community?

    https://community.appeon.com/index.php/codexchange/share-code

This would be a great addition to the PowerBuilder section of CodeXchange.

Sometimes when posting in CodeXchange, I include in the zip file that contains the .pbl, .pbt, and .pbw a Word document with some explanatory text , such as what you've posted here in this Q&A thread. Doing this would ensure the helpful explanation you've written would be kept with the example app in one place.

Best regards, John

 

Comment
  1. Jeff Kandt
  2. Friday, 4 October 2024 20:27 PM UTC
Thanks for the suggestion, John. I've uploaded an example app to CodeExchange and will post a link here after it gets approved by the moderators.
  1. Helpful 1
  1. Miguel Leeuwe
  2. Saturday, 5 October 2024 12:38 PM UTC
Nice one!

Se do something similar running our 32 bit pb apps, when users have 64 bit office installed. The dllddlll.dll gives us enough info that a file has been dropped and then we use OLE to get the emails.

A customer told us that "Classic" Outlook is supposed to retire in 2029. IF that is true, we are not happy. Our apps are very dependent on OLE, so we really are going to have to re-write a lot of code. I guess, using ms graph libraries and smtp.

regards.
  1. Helpful
There are no comments made yet.
  • Page :
  • 1


There are no replies made for this question yet.
However, you are not allowed to reply to this question.