Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to implement drag & drop with CefSharp WinForms #1593

Closed
robinrodricks opened this issue Feb 15, 2016 · 15 comments
Closed

How to implement drag & drop with CefSharp WinForms #1593

robinrodricks opened this issue Feb 15, 2016 · 15 comments

Comments

@robinrodricks
Copy link
Contributor

I'm currently intercepting the OnDragEnter event that CefSharp provides, but I want to handle the OnDragDrop event which does not exist. I am already handling the form's drop drop event and I've set form.AllowDrop = true, but since CEF intercepts the drag, I cannot handle it in the form!

In IDragHandler.OnDragEnter:

  • If I return true, then the user cannot drag anything on the browser (and I don't get the event);
  • If I return false, then the user can drag a file, but CefSharp handles it internally! (and I don't get the event)

So either ways I'm jammed. Is there any way to properly implement drag & drop in CefSharp, such that CefSharp/CEF does NOT do its default behaviour, and such that I get the DROP event handler. (or similar)

Alternatively, if CefSharp can use RevokeDragDrop (also here) in the OnDragEnter event, (in response to AllowDrop = false) then I can handle the drop event on the form itself.

Alternatively, If I can get access to these CEF flags then I can disable the internal drag/drop entirely and so fallback to the form's handling. Edit: Not possible in CEF3.

@amaitland
Copy link
Member

Try searching ceforum e.g. http://magpcss.org/ceforum/search.php?keywords=OnDragEnter

We're moving towards using Gitter Chat. Please ask followup questions there.

@robinrodricks

This comment has been minimized.

@amaitland

This comment has been minimized.

@robinrodricks

This comment has been minimized.

@amaitland

This comment has been minimized.

@robinrodricks

This comment has been minimized.

@robinrodricks
Copy link
Contributor Author

I'm posting my current workaround for whoever may need similar functionality. This works, as in, we are able to override the CefSharp drag event and do whatever we want, while preventing CEF from loading the dragged content as an HTML file. However, since we are responding to the DragOver event, even if the user cancels the drag (ie. hovers over the browser without releasing the mouse button) the event will fire.

public class BrowserWrapperClass, IDragHandler {

    public ChromiumWebBrowser Browser;

    // call this code anywhere
    public void Init(){
        Browser = new ChromiumWebBrowser(startURL);
        Browser.DragHandler = this;
    }

    // drag enter handler .. DONT CHANGE THE NAME!
    public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask)  {

        // if any filenames dragged into browser
        if (dragData.FileNames.Exists()) {
            string path = dragData.FileNames[0];

            // 1000 ms means the user has 1 sec to release the mouse;
            // if he does NOT, the callback will be executed anyways!
            // this is a crude workaround; the `OnDragRelease` event
            // cannot be accessed in CefSharp!
            ExecuteDelegate(1000, delegate{

                // do whatever you want here
                // file name is in the `path` var

                // if you want to access UI, you'll have to use Form.BeginInvoke()

            });
        }

        // false to prevent CefSharp from handling the event
        return false;
    }

    private static void ExecuteDelegate(int delay, Action handler) {
        System.Timers.Timer timer = new System.Timers.Timer(delay);
        timer.AutoReset = false;
        timer.Elapsed += delegate {
            timer.Enabled = false;
            timer.Stop();
            timer.Dispose();
            handler();
        };
        timer.Start();
    }

}

@robinrodricks
Copy link
Contributor Author

@annevu - Can you check this?

@annevu
Copy link

annevu commented Mar 25, 2016

@hgupta9 I went with using the WPF implementation. If I have time after my project, I will explore your workaround and let you know. Thanks for posting it!

@ghost
Copy link

ghost commented Sep 7, 2016

I ended up going with an implementation similar to @hgupta9's, since there isn't an OnDragDrop inside IDragHandler. I'm using the OnDragEnter to set a bool dragDrop = true and a timer for 1s (after 1s it sets dragDrop = false), and then I've implemented IRequestHandler.OnBeforeBrowse (which is called with the file path as the URL after the drop) like this:

        public bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool isRedirect)
        {
            if (navigator.dragDrop)
            {
                navigator.dragDrop = false;
                // drag/drop detected
                return true;
            }
            return false;
        }

@krptodr
Copy link

krptodr commented Feb 23, 2017

I was wondering if anyone would be willing to help me figure out how I can allow CefSharp to upload Outlook Items (Attachments, Mail Items) as files into a Drop area on a website? It doesn't seem the IDragHandler responds when dragging a file onto a web page, or even the BrowserTab. I'm using the WPF Example for my testing.

I have looked into https://www.codeproject.com/Articles/28209/Outlook-Drag-and-Drop-in-C
and
(https://outlook2web.com/)

I asked on Gitter, but haven't received any responses.

I understand everyone's time is valuable, but I would forever be thankful for anyones help on this issue.

@itshaadi
Copy link

itshaadi commented May 27, 2017

here is my workaround using globalmousekeyhook to simulate OnDragDrop

Demo:
Demo

using System.Collections.Generic;
using Gma.System.MouseKeyHook;
using System.Windows.Forms;
using CefSharp;

public partial class MainForm : Form
{
    private IList<string> dropFiles;
    private ChromiumWebBrowser Browser;
    public class DragDropHandler : IDragHandler
    {
        public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask)
        {
            this.dropFiles = dragData.FileNames;

            /*
            Gotcha !!
            */
            return false;
        }

        public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IList<DraggableRegion> regions)
        {

        }
    }
    public MainForm()
    {
        InitializeComponent();
        Subscribe();

        Browser = new ChromiumWebBrowser("URL");
        Browser.Dock = DockStyle.Fill;

        Browser.DragHandler = new DragDropHandler();

        this.Controls.Add(Browser);

    }
    /* global mouse Drag hook  */
    public void Subscribe()
    {
        m_GlobalHook = Hook.GlobalEvents();

        m_GlobalHook.MouseDragFinished += GlobalHookDragFinished;
    }
    private void GlobalHookDragFinished(object sender, MouseEventArgs args)
    {
        /*
        
        Detect if cursor is within the bounds of a control,
        so we are actually listening for any Drag Finished event
        and if cursor is inside our window then this event belongs to our program

        this.DesktopBounds.Contains(Cursor.Position)

        */
        if (this.DesktopBounds.Contains(Cursor.Position) && this.dropFiles != null)
        {
            this.OnDragDrop();
            /* You have to clear the results to prevent endless loop */
            this.dropFiles = null;
        }
        else if(!this.DesktopBounds.Contains(Cursor.Position) && this.dropFiles != null )
        {
            /* also avoid using MessageBox becasue it will interrupt this hook and cause endless loop */
            this.Text = "drop in canceld";
            this.dropFiles = null;
        }
    }
    private void Unsubscribe()
    {
        m_GlobalHook.MouseUpExt -= GlobalHookDragFinished;
        m_GlobalHook.Dispose();
    }
    private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        Unsubscribe();
    }
    /* now you can do whaterver you want */
    private void OnDragDrop()
    {
      /* Your code here */
    }
}

Gotcha

it only catch your files wherever a draggable region is defined in your html. otherwise default drag handling behavior fiers watch this demo

but you have two choice:
either return True from OnDragEnter to prevent default handling behavior. (it will set the DragOperationsMask to none but still you can do whatever you want.)

OR

define your <body> or .wrapper as a draggable region. this way the entire window is draggable and you can prevent the default handling behavior from javascript. something like

<body>
<div class="wrapper" ondrop="preventDrop()" ondragover="preventDrop()">
</div>
<script>
(function() {
   function preventDrop(){
      this.preventDefault()
    }
})();
</script>
</body>

i suggest using Vue.js simply because it's easier to handle everything.

<template>
  <div id="app" @dragover.prevent @drop="preventDrop">
  </div>
</template>

<script>
export default {
  methods: {
    preventDrop: function(e){
      e.preventDefault();
    }
  }
}
</script>

another gotcha is that you have to clean The obtained result in GlobalHookDragFinished function to prevent endless loops. also you are not allowed to use MessageBox in this function for same reason.

@ekalchev
Copy link

ekalchev commented May 13, 2019

@itshaadi

Instead of using GlobalHooks, i am using javascript drop event where I am looking if the user is dropping a file list. If the user is dropping file list, I am invoking javascript -> CEF method to notify CEF host that it should do something with the file list captured in OnDragEnter event.

class HostCommands {
    constructor(logger) {
        this._logger = logger;
        cefSharp.bindObjectAsync("hostCommands");
    }

    execute(command, commandParameter) {
        if (window.hostCommands) {
            window.hostCommands.execute(JSON.stringify({
                name: command,
                parameter: commandParameter
            }));
        }
        else {
            this._logger.log("error: hostCommands not bound");
        }
    }
}

$('body').on('drop', function (e) {
            let ev = e.originalEvent;
            if (_draggableContainsFile(ev)) {
                ev.preventDefault();
                app._hostCommands.execute("fileListDropped");
            }
        });

function _draggableContainsFile(ev) {
        if (ev.dataTransfer.items) {
            // Use DataTransferItemList interface to access the file(s)
            for (var i = 0; i < ev.dataTransfer.items.length; i++) {

                // if we are dropping a file, notify host and 
                if (ev.dataTransfer.items[i].kind === 'file') {
                    return true;
                }
            }
        }

        return false;
    }

@emcodem
Copy link

emcodem commented Jan 8, 2023

Instead of using GlobalHooks, i am using javascript drop event where I am looking if the user is dropping a file list. If the user is dropping file list, I am invoking javascript -> CEF method to notify CEF host that it should do something with the file list captured in OnDragEnter event.

@ekalchev
This sounds like a nice solution. Taking your example and adding this

$('body').on(
		'dragover',
		function(e) {
			e.preventDefault();
			e.stopPropagation();
		}
	)
	$('body').on(
		'dragenter',
		function(e) {
			e.preventDefault();
			e.stopPropagation();
		}
	)

...i was able to actually get the body drop event working and can pass it to the csharp part.
But i can only access the file "names" instead of the full path, just as in any normal browser/webpage.
Were you able to get full paths using this js method?

Edit: forget it, i see you store the original filelist in the OnDragEnter event... That is also why you don't pass the file list in app._hostCommands.execute("fileListDropped");
But is there a safe way to connect the ondragenter with the actual drop event or do we have to guess/hope that they belong together?

@ekalchev
Copy link

ekalchev commented Jan 8, 2023

But is there a safe way to connect the ondragenter with the actual drop event or do we have to guess/hope that they belong together?

I am not aware of any.

Edit: I implemented that before Chromium allowed to see the file names of the dropped file list in javascript. I know that this is now supported in latest versions. You can send the dropped filename from javascript to C# host and compare the list with what you captured in OnDragEnter. if they match, do your stuff..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants