- Jeff Kandt
- PowerBuilder
- Friday, 4 October 2024 06: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.
Find Questions by Tag
Helpful?
If a reply or comment is helpful for you, please don’t hesitate to click the Helpful button. This action is further confirmation of their invaluable contribution to the Appeon Community.