Wednesday, August 10, 2011

Getting Silverlight Drag And Drop Support For FireFox 5 On A Mac

Chris Domino, Director, Enterprise Architect

I've been working on a massive cross browser, cross platform Silverlight application for the last year or so. One of the major components is file drag and drop (heretofore D&D, not to of course be confused with Dungeons and Dragons). Getting this to work on Windows is trivial, and there are myriad resources out there to get you started. However, over on the Mac, things were much more difficult.

FireFox wasn't actually too bad, but Safari required us to wire up some JavaScript to help convince the browser to pass the dropped file's bits along to Silverlight. But all-in-all, when we were done, I felt that it was still pretty amazing that we had D&D working in Silverlight cross-platform with what turned out to be a lot less effort than I expected.

Our app went into beta, and the Mac users immediately started logging bugs about D&D not working on FireFox. So we dusted off our test Mac, and everything seemed fine. The disconnect turned out to be, after some quick investigation, a version conflict. We were running one of the last builds of FireFox 3; the users were all on 5 (if you recall, version 5 came out rather quickly after 4 shipped).

So we upgraded, and quickly saw the problem: although drag still worked, drop was completely dead. FireFox version 4 and up had broken support for Silverlight D&D (or, technically, just the second D). And I use the term "support" loosely because official backing from Microsoft wasn't really there. (It was kind of like Silverlight 4 "not supporting" Chrome, although it works just fine.)

What I want to discuss here is how to get Silverlight D&D working on Macs running FireFox 5.

We went the full nine yards when we built our Silverlight D&D infrastructure: using a Silverlight Behavior for the infrastructure that allowed us to attach the functionality to any UIElement. It used a VisualStateManager to provide cues to the user that a dragged file could be dropped at that particular area (by "lighting" up the text and animating a glowing DropShadow around the boarder). Additionally, we had a cursor control that hid the mouse pointer and showed different images in its place that provided additional visual indications where D&D was enabled. Finally, a static helper class provided common functionality, such as flags to track if we were dragging and if a drop action, based on the current cursor location, was possible.

Check out my colleague Jonathan Rupp's post on how he got us this far. I'm going to take the next step here and get our Silverlight D&D logic working for FireFox 5 on a Mac. The basic approach is to leverage the fact that drag still works, and use a combination of HTML 5, the Silverlight HTML bridge, and jQuery to basically "fake" a drop. Do read Jonathan's post, as I'll be referring to the code he presents.

The idea is to use Silverlight's ability to still subscribe to the drag events raised by FireFox 5 on a Mac to position a transparent div over the drop zone, wire up HTML 5 drop events on it, handle the file processing in JavaScript, and then pass the raw data back to Silverlight. I know that I just presented like half a dozen different technologies in that one sentence, so let's break the procedure down step by step.

The first thing to do is create a div in the ASPX page that hosts our Silverlight control. This is what we'll be using as our drop surface; there's nothing too special about it (yet):

  1. <div id="divDropSurface" style="background: transparent; z-index: 50; position: absolute; top: 0px; left: 0px; width: 0px; height: 0px;">

As you can tell, the styling implies that this div will be absolutely positioned and dimensioned to cover a certain portion of the screen. One thing to note: this technique will only work on a Mac, where Silverlight automatically sets the windowless mode to true, allowing the Silverlight region to participate in HTML Z-indexing. If you set windowless to true on a PC, you'll break D&DN altogether, as well as introduce some performance hits. I wrote more about this here.

Next we need to hook up the HTML 5 D&D events on our drop surface. Call the following method in jQuery's document ready event:

  1. function HookFFDropEvents()
  2. {
  3. //ff on mac
  4. if (window.navigator.userAgent.indexOf("Mac OS") >= 0 && $.browser.mozilla)
  5. {
  6. //get drop surface
  7. var ds = document.getElementById("divDropSurface");
  8. //hook D&D events
  9. ds.addEventListener("drop", FFDrop, false);
  10. ds.addEventListener("dragenter", FFDragEnter, false);
  11. ds.addEventListener("dragleave", FFDragLeave, false);
  12. ds.addEventListener("dragover", FFClearEvent, false);
  13. }
  14. }

We'll talk about the FFDragLeave and FFDrop methods later. FFDragEnter is not used (but will be, I'm sure, if FireFox 6 breaks drag as well). FFClearEvent is the standard JavaScript that blocks the browser's default handling of a file drop and allows the code on the page to handle it.

  1. function FFClearEvent(e)
  2. {
  3. //override the browser's default behavior for file drops
  4. e.stopPropagation();
  5. e.preventDefault();
  6. }

The next step is to position this div directly on top of a particular area of your Silverlight control when a drag is detected. (Recall that in Silverlight, you hook drag events on a particular UIElement, not the entire application.) Our Silverlight D&D behavior keeps a reference to the UIElement that it's supporting. So when a file is being dragged over a valid drop zone, an event is fired that basically updates the VisualStateManager for that UIElement. What I added to this was logic to get the HTML coordinates of this UIElement, and position our drop surface div over it.

  1. //determine if we are on mac on FF5
  2. if (FileDragDropHelper.IsMacFF5)
  3. {
  4. //get the drop target and the drop surfce
  5. Control target = this.GetCurrentDNDBehavior().Target;
  6. HtmlElement element = HtmlPage.Document.GetElementById("divDropSurface");
  7. if (element != null)
  8. {
  9. //set flag to indicate that we're currently dragging
  10. FileDragDropHelper.IsHovered = true;
  11. //position drop surface over drop target
  12. Point position = target.TransformToVisual(null).Transform(new Point());
  13. element.SetStyleAttribute("top", string.Concat(position.Y, "px"));
  14. element.SetStyleAttribute("left", string.Concat(position.X, "px"));
  15. element.SetStyleAttribute("width", string.Concat(target.ActualWidth, "px"));
  16. element.SetStyleAttribute("height", string.Concat(target.ActualHeight, "px"));
  17. }
  18. }

The flag in Line #2 is maintained in the aforementioned D&D helper utility. Line #5 is another helper method that takes in the current mouse position (gotten from the drag event), gets the UIElement at that location, and grabs the associated behavior (made possible by the gluey nature of attached properties). Everything else is pretty straight forward, utilizing the beauty of the Silverlight HTML bridge.

Now if the user were to release the mouse button, the drop would happen. But before we drop, we need to handle the case were the user drags off the control without dropping. This interaction is important, as it not only mimics what our D&D behavior is doing for us automatically in other environments, but can be reused upon a drop (since the UI needs to be reset properly).

First, we create a hidden HTML button that will be used to facilitate communication from JavaScript to Silverlight.

  1. <input type="button" id="hidFileDropInfo" style="display: none;" />

Then we hook its click event in Silverlight (as part of the behavior).

  1. HtmlPage.Document.GetElementById("hidFileDropInfo").AttachEvent("click", HandleFileReceived);

We'll see what HandleFileReceived looks like in a bit. First, let's revisit the FFDragLeave JavaScript method.

  1. function FFDragLeave(e)
  2. {
  3. //reset our button, telling silverlight to update the UI that we're no longer over a drop surface
  4. $("#hidFileDropInfo").val("CLEAR").click();
  5. }

We're using the Silverlight HTML bridge and jQuery chaining to set the value of the hidden HTML button to "CLEAR" and then click it. That click event will be handled in Silverlight by the aforementioned HandleFileReceived method, which we're still not quite ready to discuss. First, let's talk about the drop workflow.

While the drop surface is positioned over the drop zone, Silverlight won't be receiving any mouse input. But that's okay, since the visual state won't need to change until the drop surface receives either a drag leave or a drop event. We've covered what happens in the former case, so without further ado, let's talk about drop.

Here's the FFDrop method in all its glory:

  1. //globals
  2. var _xml;
  3. var _fileCounter;
  4. function FFDrop(e)
  5. {
  6. //override the browser's default behavior for file drops
  7. FFClearEvent(e);
  8. //get files
  9. var files = e.dataTransfer.files;
  10. if (typeof files == "undefined" || files.length == 0)
  11. return;
  12. //initialize global variables
  13. _xml = "<files>";
  14. _fileCounter = files.length;
  15. //process each file
  16. for (var n = 0; n < files.length; n++)
  17. ProcessFile(files[n]);
  18. }