cefsharp / CefSharp

.NET (WPF and Windows Forms) bindings for the Chromium Embedded Framework
http://cefsharp.github.io/
Other
9.83k stars 2.92k forks source link

How to implement drag & drop with CefSharp WinForms #1593

Closed robinrodricks closed 8 years ago

robinrodricks commented 8 years ago

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:

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 commented 8 years ago

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 commented 8 years ago

So you closed my issue in favor of chat? At least leave it open so if someone has a fix they can respond. Closed issues don't even show up on the project's issues tab anymore. No one will even know such issues exist.

amaitland commented 8 years ago

So you closed my issue in favor of chat?

Yes. Gitter is quite active. Only a handful of people respond here.

robinrodricks commented 8 years ago

Okay thanks I'll check there.

amaitland commented 8 years ago

So you just go around closing issues in favor of chat? I don't understand....

Be aware that regardless of your edits, your original comments are emailed.....

I'm not particularly pleased with your comments, whilst you may not agree with my stance it is currently my choice, I would ideally like to keep issues limited to bugs, preferably ones that have been discussed and verified to limit the noise. In this case if you'd politely asked if the issue could be reopened I would have considered it. Previously I would have freely given my time in an attempt to help you, and all I get is rude comments? Good luck finding a solution, remember to show some gratitude towards anyone who gives freely of their time in an attempt to help you.....

robinrodricks commented 8 years ago

I was not particularly pleased with my comments either, which is why I attempted to be polite and edit them. Sorry for having caused trouble. I had no idea comments were emailed. Sorry dude.

robinrodricks commented 8 years ago

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 commented 8 years ago

@annevu - Can you check this?

annevu commented 8 years ago

@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 commented 8 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 5 years ago

@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 commented 1 year ago

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 commented 1 year ago

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..