Using Browser Technologies in Visualforce - Part 4
Part 1 | Part 2 | Part 3 | Part 4
Contents |
Building a Flex Application for Visualforce
In this series of articles I've explained how to interact with a Visualforce page and it's controller using Javascript. This final article will reveal the code and techniques behind the Flex part of the application described in article three.
Although I am focusing on Flex, everything leading to this point is equally applicable to whatever AJAX library you may favor for creating your UI. Flex is interesting because it's an easier technology to work with and has a lot of features that just aren't available in AJAX libraries.
The Flex Tree
I'll start off by describing the structure of the FlexTree application. The application consists of a single component based on the standard Tree component. Because I wanted to represent folders and documents in the Tree control, I needed to modify the behavior of the Tree component. The standard component wants to insert a dragged item between two other items. In the case of documents and folders, I wanted to drop a document onto a folder. This slight difference means that I needed to extend the Tree component to get the behavior that I want.
The FlexTree Application
So let's look at the mxml file for the FlexTree application.
<?xml version="1.0" encoding="utf-8"?> <mx:Application creationComplete="setup()" xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:text="flash.text.*" xmlns:local="*" width="282" height="421" paddingLeft="0"> <!-- This is the custom Tree component that simply extends the standard Tree. The key reason for a custom tree is to override a couple functions that provide feedback during the drag operation. --> <local:MyTree allowDragSelection="true" id="btree" labelField="@label" useRollOver="true" width="282" height="421" dragEnabled="true" dropEnabled="true" dragMoveEnabled="true" dropIndicatorSkin="DropSkin" dataReady="handleDataReady(event)" x="0" y="0"/> </mx:Application>
The first tag is the root or Application tag for the application. The only thing of note here is that I am hooking the creationComplete event. This is a pretty common event to hook as it allows you to be sure that the application has fully initialized and is ready to go. I've created a function called setup(event) that is called when this event fires.
The next tag is the MyTree which is the name of the extended Tree component that I am using. I'm using the standard attributes for making the Tree drag and drop capable - dragEnabled, dragMoveEnabled, dropEnabled. I actually don't set a dataProvider for the tree until I have data. In the previous article we waited until the Flex application told us it was fully initialized and ready to run, at which point we called the initApp function and passed in the data. It is in that function that we set the dataProvider property.
The MyTree tag also specifies that the labelField is the xml node attribute label. As we will see when we get to the script on this mxml file, we are creating xml nodes and mapping the name field of a document or folder to the label attribute of the xml node.
I have created a new event on my version of the Tree component called dataReady. You can see in the MyTree xml tag that we are assigning a handler to this function called handleDataReady. This event is fired when an item has been successfully dropped on a folder in the tree. I create a custom event for this to keep a clear separation from standard Tree events and any custom ones that I needed to use.
One other thing to note is that I have defined a simple programmatic skin for the drop indicator to be used by this tree. The standard drop indicator is a horizontal line that is placed between two tree items. Again, I want to indicate that you can drop a document onto a folder item, so the line didn't make much sense. Instead, I wanted to highlight the folder that the drag operation was over. To do this my programmatic skin draws a rectangle over the appropriate node rather than a line below the node.
The Scipt
Let's take a look at the script that is on my mxml file.
<mx:Script>
<![CDATA[
import mx.events.TreeEvent;
/**
* Setup and external callback to accept the folder and document data
* from the Visualforce page.
*
* Also, let the page know that the flex app is ready to rock.
*/
private function setup():void {
ExternalInterface.addCallback("initApp", initApp);
ExternalInterface.call("flexIsReady");
}
/**
* This function is called by the Visualforce page to load the data that
* will be displayed by the tree. The function reformats the data into
* and XML structure that the tree handles easily.
*
* Once the XML has been crated, then we set the XML as the dataprovider for the tree
*/
public function initApp(treeData:Object):void {
var jsFolders:XMLList = new XMLList();
var doc:String = "";
for (var i:int = 0;i<treeData.length;i++) {
var nodeString:String = '<node label="' + treeData[i].name + '" id="' +
treeData[i].id + '" type="' + treeData[i].type + '">\n';
var docs:Array = treeData[i].docs as Array;
for (var j:int = 0;j<docs.length;j++) {
nodeString += '<node label="' + docs[j].name + '" id="' +
docs[j].id + '" type="' + docs[j].type + '"/>\n';
}
nodeString += '</node>\n';
doc += nodeString;
}
jsFolders = new XMLList(doc);
btree.dataProvider = jsFolders;
}
/**
* Handler for the dataReady custom event that is dispatched by
* the tree in response to a document being dropped on an new
* parent folder.
*/
private function handleDataReady(event:TreeEvent):void {
var moveData:Object = new Object();
moveData.docId = event.item.docId;
moveData.sfolderId = event.item.sfolderId;
moveData.dfolderId = event.item.dfolderId;
ExternalInterface.call("moveDocument", moveData);
}
]]>
</mx:Script>
The setup Function
private function setup():void {
ExternalInterface.addCallback("initApp", initApp);
ExternalInterface.call("flexIsReady");
}
The first bit of code that is executed is the setup function. This is called in response to the application's creationComplete event. In this function we want to do two things. First is to register a callback function that is available to Javascript. We are basically creating an interface between the Flex application and the Javascript on the page that is hosting the swf.
To review the flow once more, when the page loads and the Flex application is fully functional, we want to alert the Javascript on the host that the Flex application is ready to accept data. When that alert is captured we want the Javascript to give us the data that has been collected on the page. The setup function begins that process.
So, the second thing that the setup function does is make a call to the flexIsReady Javascript function.
The Javascript flexIsReady function on the Visualforce page:
function flexIsReady() {
flexApp = getMyApp("FlexTree");
flexApp.initApp(jsFolders);
}
The initApp Function
This is the function that allows the Javascript on the page to pass the data to our Flex application. The argument treeData is an array of folders and each folder contains, in addition to name and id values, an array of documents.
We simply use a nested iteration to reformat the data into a simple Flex XMLList and then assign that list as the dataProvider for our tree.
public function initApp(treeData:Object):void {
var jsFolders:XMLList = new XMLList();
var doc:String = "";
for (var i:int = 0;i<treeData.length;i++) {
var nodeString:String = '<node label="' + treeData[i].name +
'" id="' + treeData[i].id + '" type="' + treeData[i].type + '">\n';
var docs:Array = treeData[i].docs as Array;
for (var j:int = 0;j<docs.length;j++) {
nodeString += '<node label="' + docs[j].name + '" id="' +
docs[j].id + '" type="' + docs[j].type + '"/>\n';
}
nodeString += '</node>\n';
doc += nodeString;
}
jsFolders = new XMLList(doc);
btree.dataProvider = jsFolders;
}
Communicating with the Contoller
The last function in the Flex application mxml file is the handleDataReady function. This is the function that we specified in the MyTree tag that should handle the dataReady event. The dataReady event is fired when a drag and drop operation has completed successfully. When that operation is completed, we want to have the controller for our Visualforce page re-parent the dropped document.
Because I've created a custom event, I can easily pull the required data out of my event for passing to the Javascript function moveDocument. I've included the Javascript moveDocument function below for convenience .
function moveDocument(moveData) {
docId.value = moveData.docId;
sfolderId.value = moveData.sfolderId;
dfolderId.value = moveData.dfolderId;
btnMove.click();
}
The Custom Tree Component
As mentioned earlier, to gain the behaviors appropriate for the use case I've created and extended version of the standard Flex Tree component. The ability to extend Flex components in this way is one of the more compelling reasons to consider Flex over other browser technologies.
For my purposes, I just needed to override three functions on the Tree component, the dragOverHandler, the dragDropHandler and the showDropFeedback function.
The dragOverHandler
The whole purpose of overriding this handler was to customize the criteria for indicating a valid drop target. The criteria that I have implemented is that the item be a folder, based on the "type" attribute of the items underlying xml and that the folder not be the current parent folder of the document. No sense in dropping a document into the folder that it is already in.
You can see this logic in the second if statement near the bottom of the handler.
override protected function dragOverHandler(event:DragEvent):void
{
if (event.isDefaultPrevented())
return;
//Get the index of the event target in the list
var ind:int = calculateDropIndex(event);
//Get the actual xml of the item being dragged
var draggedItem:XML = event.dragSource.dataForFormat("treeItems")[0] as XML;
//Get the actual xml of the dragged items parent.
var sourceFolder:XML = draggedItem.parent() as XML;
//Get the item event target's item (renderer) using the list index
var item:IListItemRenderer = indexToItemRenderer(ind);
//Get the actual xml of the event target
var xmlItem:XML = new XML(item.data);
//Check to see that the event target is a folder and is not the dragged items
//current folder (parent)
if (xmlItem.@type == "folder" && xmlItem.@id != sourceFolder.@id) {
DragManager.showFeedback(DragManager.MOVE);
showDropFeedback(event);
return;
}
hideDropFeedback(event);
DragManager.showFeedback(DragManager.NONE);
}
The dragDropHandler
Overriding the dragDropHandler is required because the standard version doesn't calculate the target node in a useful way for this use case. My handler determines the folder item that the document item was dropped on and then appends a copy of the document item to the folder item's children. After that is done successfully, the handler removes the dropped document item from the original folder item's children.
Once the house keeping has been done the custom event, dataReady, is dispatched to be handled by any code listening for that event.
override protected function dragDropHandler(event:DragEvent):void {
event.preventDefault();
//Get the target folder xml node
var targetFolder:XML = indexToItemRenderer(calculateDropIndex(event)).data as XML;
//Get the node that is being dropped
var draggedItem:XML = event.dragSource.dataForFormat("treeItems")[0] as XML;
//Get the parent folder of the dropped node
var sourceFolder:XML = draggedItem.parent() as XML;
//append a copy of the dropped node into the target folder
targetFolder.appendChild(draggedItem.copy());
//Save off the id so we can send it to the data ready event
var draggedItemId:String = draggedItem.@id;
//Delete the dropped node from the parent folder
delete sourceFolder.node.(@id == draggedItemId)[0];
hideDropFeedback(event);
//Raise an event that our host application can use
//to notify the Visualforce page with
var evt:TreeEvent = new TreeEvent("dataReady", false, false,
{ "docId":draggedItemId, "sfolderId":sourceFolder.@id.toString(),
"dfolderId":targetFolder.@id.toString() });
dispatchEvent(evt);
}
The showDropFeedback Function
As was mentioned earlier, the feedback provided for the standard Tree control is not sufficient for the use case. This function simply calculates the size of the drop indicator based on the item that has already been determined is a valid drop target. The drop indicator is part of the standard Tree control implementation and it's appearance is determined by the dropIndicatorSkin attribute that we talked about earlier. In this case the dropIndicatorSkin is a custom programmatic skin called DropSkin.
override public function showDropFeedback(event:DragEvent):void
{
super.showDropFeedback(event);
var rowNum:int = 0;
var yy:int = rowInfo[rowNum].height;
var pt:Point = globalToLocal(new Point(event.stageX, event.stageY));
while (rowInfo[rowNum] && pt.y > yy)
{
if (rowNum != rowInfo.length-1)
rowNum++;
yy += rowInfo[rowNum].height;
}
var yOffset:Number = pt.y - rowInfo[rowNum].y;
var rowHeight:Number = rowInfo[rowNum].height;
//position drop indicator
dropIndicator.width = listContent.width;
dropIndicator.height = rowHeight;
dropIndicator.x = 0;
if (_dropData.emptyFolder)
{
dropIndicator.y += _dropData.rowHeight / 2;
}
}
The DropSkin
I won't go into much detail about this class as it is pretty straight forward. Basically, when this class is assigned as a skin to a component and the component is drawn, the updateDisplayList function for the skin is called to acutally display the object.
This skin just draws a simple rectangle with a black fill. The black is turned to a gray by using the alpha property to adjust transparency. That's it.
package
{
import flash.display.Graphics;
import mx.skins.ProgrammaticSkin;
public class DropSkin extends ProgrammaticSkin
{
public function DropSkin() {
super();
}
override protected function updateDisplayList(w:Number, h:Number):void
{
super.updateDisplayList(w, h);
var g:Graphics = graphics;
graphics.clear();
g.clear();
g.beginFill(0x000000, 0.5);
g.lineStyle(0, 0xffffff);
g.drawRect(0, 0, w, h);
g.endFill();
}
}
}
Summary
The keys to using Flex as a UI component in Visualforce pages are:
- Use ExternalInterface to facilitate communication between the Flex application and the Visualforce controller. This static class makes it very easy to pass data to and from Flex and Javascript.
- Allow the Visualforce controller to handle data interactions. Leverage the components and Flex environment to it's full extent and leverage the force.com platform to it's full extent by keeping separation between the model the view and the controller.
Note on Debugging
When you are dealing with two discreet systems, the Visualforce page and the Flex application, you will often need to debug the interaction between the two. There is a facility in Flex Builder to enable you to debug the Flex part of your Visualforce page in the context of the actual page. This is extremely helpful for viewing the insies and outsies of the Flex application.
Below is the url for a Visualforce page:
If your are using Flex Builder you should know how to launch your application locally with the debugger. If you don't it's as easy as right-clicking the mxml file and selecting Debug-As/Flex Application. You can also control many aspects of the debugging session by clicking Run/Debug/Other from the menu.
This will result in a dialog that allows you to customize the debug session.
The section that I want to point out is the "URL or path to launch" on the right of the dialog. This will allow you to set a path or URL to the where you have deployed your swf so that you can debug it in context. For doing this is a Visualforce page you would uncheck the "Use defaults" checkbox. When that box is unchecked you can edit the three text values.
Copy the address from your browser when you are at your Visualforce page and paste it into the "Debug" text box.
Now, when you launch the debugger, it will load your Flex application in the context of your Visualforce page.
A couple of things to note.
- You must already be logged into your salesforce.com account otherwise you will be prompted for login and the debug session will have to be restarted.
- Don't forget to replace the static resource that contains your swf every time you make a change and want to debug it in this context.


