4281 Views // 0 Comments // Not Rated

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

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):

Code Listing 1

  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:

Code Listing 2

  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.

Code Listing 3

  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.

Code Listing 4

  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.

Code Listing 5

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

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

Code Listing 6

  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.

Code Listing 7

  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:

Code Listing 8

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

The idea is to get the HTML 5 drop event metadata, process each file, and build up some XML that acts as a data contract between JavaScript and Silverlight. Lines #13 and #14 initialize the _xml global variable to the root node of our XML markup, and _fileCounter to the number of dropped files.

Most of the magic happens in ProcessFile, which follows.

Code Listing 9

  1. function ProcessFile(file)
  2. {
  3. //create reader
  4. var reader = new FileReader();
  5. //client side error handling
  6. reader.onerror = function (error)
  7. {
  8. //determine error
  9. switch (error.target.error.code)
  10. {
  11. //show appropriate error
  12. case 1:
  13. alert(file.name + " was not found.");
  14. break;
  15. case 2:
  16. alert(file.name + " has been modified since it was dropped.");
  17. break;
  18. case 3:
  19. alert(file.name + " has been cancelled.");
  20. break;
  21. case 4:
  22. alert(file.name + " could not be read.");
  23. break;
  24. case 5:
  25. alert(file.name + " is too large.");
  26. break;
  27. }
  28. }
  29. //read data
  30. reader.readAsDataURL(file);
  31. reader.onloadend = function (dropped)
  32. {
  33. //get raw data from file
  34. var data = dropped.target.result;
  35. if (data.length > 128)
  36. {
  37. //build xml representation of each file
  38. _xml = _xml + "<file><name>" + file.name + "</name><size>" + file.size + "</size><data>" + data + "</data></file>";
  39. //decrement the counter...
  40. _fileCounter--;
  41. //...and when there are no more files to upload...
  42. if (_fileCounter == 0)
  43. {
  44. //...close the xml...
  45. _xml = _xml + "</files>";
  46. //...and send it to silverlight
  47. $("#hidFileDropInfo").val(_xml).click();
  48. }
  49. }
  50. else
  51. {
  52. //error
  53. alert(file.name + " has invalid data. It may be corrupted.");
  54. }
  55. }
  56. }

In Line #4, we new up an HTML 5 FileReader, which when fed the metadata of a drop event, gives us a nice OO representation of a file. The error event is hooked in Line #6, and processing is done according W3C protocol. Line #'s 30 and 31 load the file; the fact that this is an asynchronous process is what forces us to use global variables. In Line #38, we build the XML representation of the file array we're going to be passing to Silverlight. The _fileCounter variable is decremented in Line #40 each time a file is successfully collected, so that the check in Line #42 can determine if all asynchronous operations have completed, the XML can be capped off, and finally be sent to Silverlight via the aforementioned bridge-and-change method in Line #47.

So we have both our drag leave and our drop methods setting the value of a hidden HTML button and then clicking it. The click event is handled by Silverlight, and deals with both cases.

Code Listing 10

  1. private void HandleFileReceived(object sender, HtmlEventArgs args)
  2. {
  3. //initialization
  4. FileDragDropHelper.IsHovered = false;
  5. string value = HtmlPage.Document.GetElementById("hidFileDropInfo").GetProperty("value").ToString();
  6. //get value
  7. if (string.IsNullOrEmpty(value) || value.Equals("CLEAR"))
  8. {
  9. //process a reset
  10. HtmlElement element = HtmlPage.Document.GetElementById("divDropSurface");
  11. if (element != null)
  12. {
  13. //reset drop surface
  14. element.SetStyleAttribute("top", "0px");
  15. element.SetStyleAttribute("left", "0px");
  16. element.SetStyleAttribute("width", "0px");
  17. element.SetStyleAttribute("height", "0px");
  18. }
  19. }
  20. else
  21. {
  22. try
  23. {
  24. //build xml doc to hold D&D raw data
  25. XDocument doc = XDocument.Load(new StringReader(value));
  26. List<FileDragDropHelper.DroppedFileWrapper> files = new List<FileDragDropHelper.DroppedFileWrapper>();
  27. //iterate all dropped files...
  28. foreach (XElement node in doc.Root.Nodes())
  29. {
  30. //...and poor-man deserialize them
  31. List<XElement> nodes = node.Nodes().Cast<XElement>().ToList();
  32. string name = nodes[0].Value;
  33. long size = Convert.ToInt64(nodes[1].Value);
  34. //strip heading off of base64 file header data
  35. string data = nodes[2].Value;
  36. data = data.Substring(data.IndexOfThatDoesntBreakMacs(";base64,") + 8);
  37. //collect files
  38. files.Add(new FileDragDropHelper.DroppedFileWrapper(name, size, Convert.FromBase64String(data)));
  39. }
  40. //pass file along to behavior
  41. var b = this.GetCurrentDNDBehavior();
  42. if (b != null)
  43. b.OnDrop(files.ToArray());
  44. }
  45. catch (Exception ex)
  46. {
  47. //error
  48. MessageBox.Show(ex.ToString());
  49. }
  50. finally
  51. {
  52. //pass drag leave to the rest of the D&D infrastructure
  53. this.DoLeave();
  54. FileDragDropHelper.DoHangledDragLeave();
  55. }
  56. }
  57. }

Once again, Line #41 gets the D&D behavior, and fires the drop event (with the files) on the target UIElement. Line #7 determines if this is a "CLEAR" event (in which case the drop surface is hidden) or if this is an actual drop. Drag leave still works normally in FireFox 5 on a Mac, so the behavior itself can update the VisualStateManager and reset any "dragging" flags in either case. The rest is not too exciting: Line #31 builds an XDocument from the raw XML and iterates the child "file" nodes. It then pulls out the file name and size properties, as well as converts the data from a base64 string to a byte array, and feeds it to our DroppedFileWrapper DTO (which is a wrapper around a FileInfo object).

If time allowed, this would have all been baked into our behavior and done proper: implementing XML deserialization for the file bits, not using buttons to pass data around, and generally less piece-wising these technologies together. But with the timeline I had, the complexity of the solution, and the existing infrastructure it had to fit into, (which itself was already quite complex due to cross-browser, cross-platform Silverlight D&D support) this is how it was born.

To summarize, if you're not using a behavior or any other existing D&D implementation, here's what you need to do to get Silverlight D&D working in FireFox 5 on a Mac:

  1. Create a "hidden" (no length or width and positioned at 0,0) absolutely-positioned div to act as a drop surface.
  2. Wire up the HTML 5 D&D events on the drop surface.
  3. Position it over the "drop zone" (the Silverlight UIElement to be dropped onto) when it's dragged over.
  4. Use HTML 5 JavaScript to process the drop event and read each file.
  5. Pass the raw data to Silverlight.
  6. "Hide" the drop surface (remove its length and width and reposition to 0,0) after the drop or upon the drop surface's drag leave event.

And there you have it: Silverlight drag and drop in FireFox 5 on a Mac. Have fun!

No Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.