Using Browser Technologies in Visualforce - Part 3

Part 1 | Part 2 | Part 3 | Part 4

Contents

Overview

In the previous article in this mini-series we examined the mechanisms by which we can interact in a meaningful way with the controller of a Visualforce page using standard browser technologies, in particular Javascript.

In this part, we will be relying on all the techniques previously presented, but also leveraging Flex as the UI portion of the page. In this simple example, flex is the only UI portion, but that is not due to any limitations of Visualforce pages.

What are we building?

In an attempt to clearly describe how these pieces work together, I've create a simple Flex application that is a tree component. The tree should display all the folders in my salesforce.com account and the documents that belong to those folders as child nodes of the tree. The tree is being used as my UI component because I'd like a nice interface that includes drag and drop capabilities so that I can move documents around from folder to folder. I'd like to have some good feedback as to where a document can and can't be dropped.

This is a fairly trivial application, but the application itself is not the point. Hopefully, when are finished with this article you will not only understand how these standard browser technologies mesh with Visualforce pages, but maybe pick up a thing or two about Flex applications as well.

Here is the target we are aiming for.

Image:Finished.jpg


The Controller

We'll start the discussion by talking about the controller piece. For this page we need to use a custom controller. Since standard controller pertain to a single object, this is the only choice for dealing with multiple folders.

The controller contains an embedded class called myFolder which is very similar to the embedded class in the previous article. The reason for the class is wrap a Folder object and it's documents in a single object. Although documents are children of folders, the relationship is such that we can't pull the folders and the children in a single SOQL statement. The myFolder class contains three fields - one for the name of the folder, one for the id and one to hold the documents that are "contained" in that folder. Each field has it's own getter.

The controller contains four data fields. We are keeping a List field, folders, that we populate once the first time the field is referenced from the page. The folders field has a getter. The three other fields are used for binding to hidden input elements that will be used to communicate with the Flex application via Javascript. Each of these three fields have getters and setters to facilitate that binding to and from the page.

Finally, the controller has an action method called moveDocs that does that re-parenting of the dropped document.

public class docController {

  public class myFolder {

    private String name;
    private String id;
    private Map<String, Document> docs = new Map<String, Document>();
 
    public List<Document> getDocs() { return new List<Document>(docs.values()); }
    public String getName() { return name; }
    public String getId() { return id; }
  }

  private Map<String, myFolder> folders = new Map<String, myFolder>();
  private String my_DocId;
  private String my_SfolderId;
  private String my_DfolderId;

  public String getMyDocId() { return my_DocId; }
  public String getMySfolderId() { return my_SfolderId; }
  public String getMyDfolderId() { return my_DfolderId; }

  public void setMyDocId(String id) { my_DocId = id; }
  public void setMySfolderId(String id) { my_SfolderId = id; }
  public void setMyDfolderId(String id) { my_DfolderId = id; }

  public String moveDoc() {
    Document doc = [Select Id, FolderId From Document Where Id = :my_DocId];
    doc.FolderId = my_DfolderId;
    update doc;
    return null;
  }

  public List<myFolder> getFolders() {
    List<Folder> thefolders = [Select Folder.id, Folder.name, Folder.type From Folder Where Folder.Type = :'Document'];
    for (Folder fldr : thefolders) {
      myFolder mfldr = new myFolder();
      mfldr.name = fldr.name;
      mfldr.id = fldr.id;
      folders.put(fldr.name, mfldr);
      List<Document> docs = [Select Id, Name From Document Where FolderId = :mfldr.id];
      for (Document doc : docs) {
        mfldr.docs.put(doc.id, doc);
      }
    }
    return new List<myFolder>(folders.values());
  }
}

moveDoc Function

The moveDoc function is pretty simple. Since we are just "moving" the documents from folder to folder all we have to do is update the document using a different FolderId value than the original. Of cource, the document itself is not actually moved since it doesn't actually physically reside in a folder. The method is returning null because we don't really have any useful information to return.

getFolders Function

The getter function getFolders just needs to pull all the folders and documents that are visible to us and wrap that data in a List of myFolder objects to return to the page. This is done with two loops, the first to get the folders and the second to get the documents for each folder. Although I tried creating a single SOQL statement that used Document as the primary From clause, this ended up not returning empty folders, so the double query is the way to go.

NOTE: You should be very careful when using nested queries as you can easily run into governer limits.

For each folder I create a new myFolder object setting the appropriate name and id fields. Then we use the id for the folder as the criteria for selecting the documents in the nested query. when looping through the documents I can just put them into the myFolder's docs field.

The Page

The Visualforce page is organized into three distinct parts, Javascript, Flex object, Visualforce component tags.

The Flex Application

Embedding the Flex app is done very simply by taking the standard boilerplate for embedding a Flex application into an SControl and pasting it into the page. Now a Flex application, physically, is a SWF file. This file needs to be stored in a place that can be referenced from the VF page.

One of the great new features available on the platform is something called Static Resources. This provides the developer with the ability to place resource on the platform and refer to it from various places, not the least of which is a VF page. Let's take a look at the object tag that embeds the Flex application into our page.

<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
  id="FlexTree" width="282" height="421"
  codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
  <param name="movie" value="{!$Resource.FlexTree}" />
  <param name="quality" value="high" />
  <param name="bgcolor" value="#869ca7" />
  <param name="allowScriptAccess" value="sameDomain" />
  <embed src="{!$Resource.FlexTree}" quality="high" bgcolor="#869ca7"
    width="282" height="421" name="FlexTree" align="middle"
    play="true"
    loop="false"
    quality="high"
    allowScriptAccess="sameDomain"
    type="application/x-shockwave-flash"
    pluginspage="http://www.adobe.com/go/getflashplayer">
  </embed>
</object>

This is very standard boilerplate for embedding a Flex application in an html page. There are some slight differences. Because the platform doesn't provide and addressable file system like a typical website, we need to specify where the SWF file is using a page formula. The syntax for specifying a Static Resource location in a page formula is {!$Resource.<resourceName>}. There are two places in the code above where you can see this in action. First, the movie param of the object tag, and secondly the src attribute of the embed tag. This is done is two places to support the two main browsers IE and FF. Don't worry about the details, as I mentioned this is boilerplate.

A couple of details to pay attention to though are the width and height settings. This will determine, at least at load time, the width and height of the Flex application container.

I'll be talking more about the inner workings and outer workings of the Flex application later on in this article.

The JavaScript

The Javascript required for this is actually quite straight forward. Aside from some script interleaved inside our repeat tags, there are two global variables and three functions.

<script>
  var jsFolders = [];
  var flexApp;
  var docId, sfolderId, dfolderId, btnMove;

  function getMyApp(appName) {
    if (navigator.appName.indexOf("Microsoft") != -1) {
      return window[appName];
    } else {
      return document[appName];
    }
  }

  function flexIsReady() {
    flexApp = getMyApp("FlexTree");
    flexApp.initApp(jsFolders);
  }

  function moveDocument(moveData) {
    docId.value = moveData.docId;
    sfolderId.value = moveData.sfolderId;
    dfolderId.value = moveData.dfolderId;
    btnMove.click();
  }
</script>

The first variable is to hold the array of myFolder objects that will be retrieved from the controller. This is similar to the Javascript array that was created in the previous article and filled in a similar way (more on that later).

The second variable is to hold a reference to the Flex application. At some point we will need to tell the Flex application that we have data that we want it to display. We will do that using the flexApp variable. This variable is set by the getMyApp function. I pulled this function straight out of the Adobe Flex help and it is pretty obvious that we are just trying to get a reference to the thing we embedded using a different method depending on the browser.

The flexIsReady function is never called by other Javascript on this page. This can be thought of more as an event handler that is fired when the Flex application is fully created and initialized. After the page loads and the Flash Player loads the Flex application, the application will do it's initialization, screen painting event setup and whatever else it needs and will then make a call to this Javascript function to indicate that it's locked and loaded.

Once the Flex application has indicated that it's ready to roll, we create our reference to it, and then immediately pass the array of myFolder objects that were generated for us when the platform delivered the page. We'll see how that works in a bit. The Flex application has defined an externally available method called initApp. This method accepts a well defined object that it understands as displayable in it's Tree component. When the Flex application executes that method it will crack open the array and populate the tree.

The last function is the moveDocument function. Again, this function is not called by Javascript on the page, but by the Flex application when a drag and drop operation was successfully completed. In this function we are using $Component.<id> and the getElementById method of the browser to set the information that is required to effect the re-parenting operation on the controller. This is the exact same technique used in the previous article. As is the last line of this function which causes the hidden button "btnClick" to be "clicked".

In the previous article we waited until just prior to submit to set the form variables. In this case we are doing this prior to submit. Not sure if one is advantageous over the other. I did it this way to illustrate the flexibility that you have.

There are more bits if script scattered amongst the VF component tags and I'll explain those as we get to them. Otherwise, that's it for the script.

Once you understand and become familiar with the patterns for using standard browser technologies on pages it becomes pretty easy to implement.

The Components

There really aren't that many components involved in this page as you might expect. There really shouldn't be too many as we are leveraging Flex for the entire UI. As you can see in the code below, there are two "sections" of <apex:*> tags. I'll explain each section.

  <apex:repeat id="folderData" value="{!folders}" var="fldr">
    <script> jsFolder = { name:"{!fldr.name}", id:"{!fldr.id}", docs:[], type:"folder" }; </script>
    <apex:repeat value="{!fldr.docs}" var="doc">
      <script> jsFolder.docs.push( { name:"{!doc.name}", id:"{!doc.id}", type:"document" } ); </script>
    </apex:repeat>
    <script> jsFolders.push(jsFolder); </script>
  </apex:repeat>


<apex:form id="moveDocForm">
  <apex:inputHidden id="docId" value="{!myDocId}"/>
  <apex:inputHidden id="sfolderId" value="{!mySfolderId}"/>
  <apex:inputHidden id="dfolderId" value="{!myDfolderId}"/>
  <apex:commandButton action="{!moveDoc}" 
        value="Move" id="btnMove" style="display: none" />
  <script>
    docId = document.getElementById("{!$Component.docId}");
    sfolderId = document.getElementById("{!$Component.sfolderId}");
    dfolderId = document.getElementById("{!$Component.dfolderId}");
    btnMove = document.getElementById("{!$Component.btnMove}");
  </script>
</apex:form>

Starting with the repeat component, if we harken back to the previous article again, we can see what's going on pretty easily. The value attribute of our folderData repeat component causes the getFolder method on the controller to execute and return our array of myFolders. This repeat component is the beginning of our outer iteration. Like the controller's nested query, the page uses a nested repeat component.

The script tag directly after the folderData repeat is creating a Javascript object on the fly. When this object is created, we set the id, name and initialize a docs array. We are essentially, duplicating the myFolder object in Javascript. There is one extra field on the Javascript object, the type field. This is used later by the Flex application.

The nested repeat component allows us to iterate over the documents for the folder we are representing in Javascript. For this article we just care about the document's id and name. Like the folder we are creating a Javascript representation of the document and adding the type field, again for Flex to use in it's display and drag and drop processing. Each Javascript document object is then pushed onto the docs array of the current folder object.

At the end of the outer iteration we push the Javascript folder object onto the array of folders that get handed to the Flex application in the initApp function that we talked about earlier. That's it. We now have every folder and every document, sans actual document content, represented in Javascript.

The second set of components handle the posting of changes to the controller. We have our form defined as well as the three values we use to re-parent a document and a submit button. You should notice that in this case we have defined a display style of none for the button since we don't want someone randomly clicking our submit button (you know they would click it no matter how it was labeled).

The combination of the Javascript, HTML object tag and apex tags work together to create the simple application that we saw at the beginning of this article.

Try It Yourself

If you want to try this yourself, please be my guest.

Things you will need:

  • Create some folders and populate them with documents.
  • Download the Flex swf from here and create a new static resource named FlexTree using the swf file.
  • Create a new Visualforce page called, well, anything you like actually. This sample is not dependent on the name of the page.
  • Cut and paste the full page listing below into the new page.
  • Cut and paste the full controller listing into the controller editor.

Download the Flex source code files: source.

Full Listing of Page

<apex:page controller="docController" id="myPage">

<script>
var jsFolders = [];
var flexApp;
var docId, sfolderId, dfolderId, btnMove;

function getMyApp(appName) {
  if (navigator.appName.indexOf("Microsoft") != -1) {
    return window[appName];
  } else {
    return document[appName];
  }
}

function flexIsReady() {
  flexApp = getMyApp("FlexTree");
  flexApp.initApp(jsFolders);
}

function moveDocument(moveData) {
  docId.value = moveData.docId;
  sfolderId.value = moveData.sfolderId;
  dfolderId.value = moveData.dfolderId;
  btnMove.click();
}

</script>

<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
id="FlexTree" width="282" height="421"
codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
  <param name="movie" value="{!$Resource.FlexTree}" />
  <param name="quality" value="high" />
  <param name="bgcolor" value="#869ca7" />
  <param name="allowScriptAccess" value="sameDomain" />
  <embed src="{!$Resource.FlexTree}" quality="high" bgcolor="#869ca7"
    width="282" height="421" name="FlexTree" align="middle"
    play="true"
    loop="false"
    quality="high"
    allowScriptAccess="sameDomain"
    type="application/x-shockwave-flash"
    pluginspage="http://www.adobe.com/go/getflashplayer">
  </embed>
</object>


<apex:repeat id="folderData" value="{!folders}" var="fldr">
  <script> jsFolder = { name:"{!fldr.name}", id:"{!fldr.id}", docs:[], type:"folder" }; </script>
  <apex:repeat value="{!fldr.docs}" var="doc">
    <script> jsFolder.docs.push( { name:"{!doc.name}", id:"{!doc.id}", type:"document" } ); </script>
  </apex:repeat>
  <script> jsFolders.push(jsFolder); </script>
</apex:repeat>


<apex:form id="moveDocForm">
  <apex:inputHidden id="docId" value="{!myDocId}"/>
  <apex:inputHidden id="sfolderId" value="{!mySfolderId}"/>
  <apex:inputHidden id="dfolderId" value="{!myDfolderId}"/>
  <apex:commandButton action="{!moveDoc}" value="Move" id="btnMove" style="display: none" />
  <script>
    docId = document.getElementById("{!$Component.docId}");
    sfolderId = document.getElementById("{!$Component.sfolderId}");
    dfolderId = document.getElementById("{!$Component.dfolderId}");
    btnMove = document.getElementById("{!$Component.btnMove}");
</apex:form>
</apex:page>


The next article will focus on the Flex part of this application.

Part 1 | Part 2 | Part 3 | Part 4