Skip navigation

Category Archives: visualforce

One of the most hyped, and in my opinion least documented features of the Summer 13 release of Salesforce is custom Chatter Publisher Actions. Various Salesforce bloggers have written about how easy it is to create and use regular Chatter Publisher Actions, which, basically, just let you quickly create records related to a primary record from your Chatter Feed (e.g. from an Account’s Chatter feed, creating a Contact or Opportunity, without having to go down to the corresponding related lists. For one excellent discussion of regular Publisher Actions, see Daniel Hoechst’s post. The release notes also give examples of regular Publisher Actions, so I won’t go into any more depth on them here.

However, neither the release notes, nor any other bloggers, nor Salesforce themselves in their recent release webinars, have yet shown any working examples of custom Publisher Actions. What are they? Here’s a brief overview:

  • They come in two flavors: Global and Object-Specific.
    • Global custom Actions: can be added to any Chatter Publisher Layout, anywhere.
    • Object-specific custom Actions: can only be added to one specific object’s Publisher Layouts.
  • You create them using Visualforce Pages.
    • For Global custom actions, your Visualforce page must either have no controller, or a custom controller that is NOT an extension.
    • For Object-specific custom Actions, your Visualforce page must use the standard controller of the object you’d like to use the Action with.
  • Chatter implements custom Actions using iFrames.
    • When creating a new custom Action, you must specify a height for your Action — this is setting the height of the iFrame in which your page will be included.
    • This iFrame is always run from within the Visualforce Page’s domain. So, if it’s a local VF Page, the domain will be something like “c.na14.visual.force.com”, whereas if the page is included in a managed package, the domain will be something like “skuid.cs15.visual.force.com”.

So custom Publisher Actions are, by virtue of having to be created through Visualforce Pages, and being embedded in iFrames, already more complicated to develop. This begs the question: what sort of “Action” would be compelling enough to warrant going through this hassle? What’s a good use case?

A use case: create a Contact and linked Opportunity Contact Role all at once, from an Opportunity

Many of the examples I thought of all fall into one category: creating records of multiple objects all at once, in particular, creating a junction object and creating the joined record if it doesn’t exist yet. For example, from an Opportunity record, you often want to create a new Opportunity Contact Role, but can’t because the related Contact doesn’t exist yet. Or, in a hypothetical iTunes-on-Force.com app, from an Album’s page, create a “Genre Association” junction object as well as a new Genre if it doesn’t exist yet.

I finally settled on the “Create a Contact and Opportunity Contact Role all at once” example.

Making it happen

The first step in creating a Custom Publisher Action is to create the Action’s associated Visualforce Page. For this example, my Custom Action is specific to the Opportunity object, so our Page must use the Opportunity Standard Controller. As for the body of the page, since I’m using Skuid, I just need to specify the name of a Skuid Page I want to include, which we’ll build later.

VisualforcePage_ForFeedAction

Now that we have a Visualforce Page associated to the Opportunity standard controller, we can create our Action!

The first step is to go to the newly-reorganized Opportunity “Buttons, Links, and Actions” section. In Summer 13, Salesforce has consolidated Custom Buttons, Custom Links, Standard Actions and Overrides (e.g. the place where you can override what happens for the “View”, “Edit”, “New”, and “Tab” action for each object), and the new Chatter Publisher Actions, into this one list for each Object.

Click on “New Action”, and then enter the following:

ButtonsLinksAndActions NewAction

Notice two things:

  1. The “Height” attribute corresponds directly to the height of the iFrame which will embed your Visualforce page
  2. The “Label” is the actual label that users will see for your new Publisher Action, e.g. they’ll see “Link”, “File”, “Post”, “Poll”, and “Add Contact”.

Now that we’ve created the action, we need to actually add it to one of our Opportunity page layout’s Publisher Actions area. By default, all objects will inherit from the default “Global” Publisher Action area, so we’ll need to explicitly “break” this inheritance to allow the set of Publisher Actions that are displayed on our Opportunity layout(s) to be different than the Publisher Actions for all other objects. TO do this, click “override the global layout” in the “Publisher Actions” area of one of your Opportunity page layouts.

OverrideTheGlobalLayout

Next, drag in the “Add Contact” Publisher Action we just created, and position it as you’d like relative to the other Publisher Actions:

DragInNewAction

Once we save the Page Layout, we’re ready to roll! This Publisher Action should show up in our Opportunity page’s Chatter feed. Because we positioned it last in the list, and there’s more than 4 Publisher Actions, our Action shows up under the “More” area:

SelectingThePublisherAction

Now, we’ll show how we built this with Skuid later, but here’s what our custom Publisher Action actually looks like:

PublisherAction_InAction

We’re presented with two columns of data. In the first column, we enter some bare bones info for a new Contact record we want to create that’s associated with this Opportunity’s Account. In the second column, we define the Opportunity Contact Role. Once we click Save, what Skuid will do is the following:

  1. Create a new Contact record using the fields we filled out, with the AccountId of the Opportunity in context (“Acme”)
  2. Create a new Opportunity Contact Role record, using the fields we entered, and associated to the Opportunity in context (“Acme”) and our newly created Contact (“Bilbo Baggins”)
  3. Create a new post in this Opportunity’s Chatter Feed describing what just happened.
  4. Refreshing the page — we’ll describe why this is necessary in a second.

In the Skuid page I built in place of the standard Opportunity detail page, I display Opportunity Contact Roles in a “Key People” tab. As you can see, a new “Bilbo Baggins” contact record was created, and an Opportunity Contact Role record linking him to the “Acme – 500 Widgets” Opportunity was created as well.

ContactRole_CreatedByFeedAction

Finally, a new FeedItem was created that records the addition of the new Contact and his Role:

FeedItem_CreatedByAction

The Skuid page that made the real magic happen

Getting the actual Publisher Action configured is pretty quick once you’ve got a Visualforce Page on hand to use. But it’s this Visualforce Page, of course, which has to do all the hard work to make the custom Publisher Action useful.

Using Skuid as the content of the Publisher Action made the process very fast and fairly painless. In a nutshell, here’s what I did:

(1) Create a new Skuid Page using the “New Page” template on the Contact object.

The “New Page” template, here, scaffolds a basic Skuid Page for use in creating brand new Contact records. It sets up a Model on the Contact page with “Create Default Row if None Exists” pre-checked, and adds a Field Editor component with the FirstName and LastName fields already thrown in:

NewSkuidPage

ContactModelProperties

(2) Add in 3 additional Models: on the Opportunity, OpportunityContactRole, and FeedItem objects:

AddlModels

The Opportunity Model grabs the Opportunity record in context from the page, using the “id” URL Parameter. We then grab the AccountId lookup field from the Opportunity, so that we can use this to automatically prepopulate the AccountId of our newly-created Contact:

OppConditions OppFields

For the Opportunity Contact Role model, we pull in the Role and IsPrimary fields so that we can display them in the page, and we add 2 “Model Merge” Conditions to associate the new record to the context Opportunity and the newly-created Contact:

OppCRConditions

Finally, for the FeedItem model, we add a single condition to automatically associate the new FeedItem to the Opportunity in context, and we add in the Body field, which we will edit in our Save action:

FeedItemConditions FeedItemFields

(3) Place 2 Field Editors, one each for the Contact and ContactRole Models, in separate Panels:

PanelSetLayout

(4) Embed this Panelset within the first step of a new Wizard Component

-We could have used a PageTitle component instead, but we do this because it allows for easy transition into multi-step wizards. We could easily have put the Opportunity Contact Role fields within a separate step of the wizard, and had a “Next Step”/”Previous Step” sequential navigation here.

Wizard

(5) Create a custom Inline (Snippet) to use for our “Save All” Wizard Step action

Skuid’s standard wizard action types almost suffice here for achieving everything we want to do, but we want to automatically populate the Body field of our new FeedItem, which takes a little code, and,  because we’re using this Page as a Chatter Publisher Action, which is run in an iFrame, we run in to a Gotcha involving using the URL Redirect action, as we need to reload the location of our frame’s parent, not the frame itself (window).

First, we customize our Action to execute a Snippet called Save All:

SaveAllStepAction

Finally, we create the Inline (Snippet) itself:

InlineSnippet

//
// Get our Models
var contactModel = skuid.model.getModel('ContactData'),
    contactRoleModel = skuid.model.getModel('ContactRole'),
    feedItemModel = skuid.model.getModel('FeedItem');

// Get the first rows in each of our Models
var contact = contactModel.getFirstRow(),
    contactRole = contactRoleModel.getFirstRow(),
    feedItem = feedItemModel.getFirstRow();

var bodyText = 'Added new ' + contactRole.Role
    + ' to this Opportunity: '
    + contact.FirstName + ' ' + contact.LastName
    + ' (' + contact.Title + ').';

// Update the Body of our new Feed Item
feedItemModel.updateRow(feedItem,'Body',bodyText);

// Save our Models,
// and when we're done,
// refresh the iframe's parent

skuid.model.save(
    [contactModel,contactRoleModel,feedItemModel]
,{ callback: function() {
    parent.location.reload();
}});

In the Snippet, we do the following:

  1. Get references to 3 of our Models
  2. Get references to their first rows
  3. Populate the “Body” field of the FeedItem model’s first row with the Role field from our OpportunityContactRole record and the First and Last Name fields from our Contact record
  4. Save all 3 of our Models in sequence, starting with the Contact model (VERY important) so that the later models can use the newly-created Contact’s Id to populate their new records.
  5. Once the save is done, we reload our parent window. This is necessary because this Skuid page will be stuck into an iFrame by Chatter.

Thorny Gotchas, and the undocumented Chatter Publisher API

The basic buildout of this took less than 30 minutes — seriously! However, we spent some time trying to work out how best to “talk” to the Chatter Publisher using the undocumented Chatter Publisher API — an effort which ended in nothing but frustration.

The thorny gotchas to avoid are mostly related to the fact that custom Publisher Actions are implemented using iFrames:

  1. Any  JavaScript you write in your child Visualforce Page will be unable to talk to the parent page UNLESS both your page and the page containing the Chatter component are in the same domain, due to cross-domain script issues — which is impossible unless BOTH pages are Visualforce. Consider these scenarios:
    1. Chatter Feed in “na15.salesforce.com”, Custom Action VF Page in “c.na15.salesforce.com” — BLOCKED.
    2. Chatter Feed in “c.na15.visual.force.com”, Custom Action VF Page in “skuid.na15.visual.force.com” — BLOCKED.
    3. Chatter Feed in “c.na15.visual.force.com”, Custom Action VF Page in “c.na15.visual.force.com” — SUCCEEDS.
    4. Chatter Feed in “skuid.na15.visual.force.com”, Custom Action VF Page in “skuid.na15.visual.force.com” — SUCCEEDS.
  2. We couldn’t find a way, using the undocumented Chatter API, to “refresh” the Chatter Feed to show our newly-created Feed Item, so we had to do a full page refresh to get the Chatter Feed to show this. We tried the following methods, none of which worked:
    1. chatter.getPublisher().resetPublisher()
    2. chatter.getPublisher().submit()
    3. chatter.getFeed().refresh()
    4. chatter.getFeed().showNewUpdates()

We would have LOVED to use one of these methods in our Skuid “saveAll” Snippet, but we’ll have to wait for Chatter to document its API.

Screen shot 2013-05-15 at 1.04.16 PM

To help foster an ongoing conversation among Salesforce ISV and OEM partners — aka developers of Salesforce AppExchange apps — I started this discussion on the Salesforce ISV Partners LinkedIn group, which I encourage fellow ISV’s/OEM’s to join:

Let’s pool our thoughts – best practices for Salesforce ISV/OEM app development

One of the best practices I brought up was the need to properly “protect” or “sandbox” your application’s references to external JavaScript libraries within complex “mash-up” pages that include JavaScript code written by various other ISV’s / OEM’s / consultants / developers, as well as underlying Salesforce.com JavaScript code.

These days, more and more apps being developed on the Salesforce Platform rely heavily on external JavaScript libraries such as jQuery, jQuery UI, jQuery Mobile, ExtJS, Sencha Touch, KnockoutJS, AngularJS, Backbone, MustacheJS, Underscore, JSONSelect, etc. Leveraging these libraries is definitely a best practice — don’t reinvent the wheel! As jQuery quips, “write less — do more!” As a Salesforce consultant, I think, this is generally the goal 🙂

Problems emerge, though when multiple ISV’s include different versions of these JavaScript libraries as Global Variables within their Visualforce Pages or Visualforce Components — because whichever version is loaded last will, by default, overwrite the earlier version. This makes it very difficult to mash-up / integrate Visualforce Components or Pages from multiple ISV’s into a single page. When faced with this, a common developer response is to stop using the latest version of the external library and trying to make their code work against the earlier version of the library forcibly included in the page (perhaps by a managed Visualforce Component or embedded Visualforce Page).

Fortunately, there IS a better way to avoid this.

In a nutshell, the solution is: “protect” or “localize” your references to any external libraries, preferably in a JavaScript namespace corresponding to your managed package.

For instance, if your Salesforce application has the namespace “skuid”, you’re already going to probably have various JS Remoting functions available within the “skuid” JavaScript object that Salesforce automatically creates in pages with controllers with JS Remoting methods — and as an ISV, you are ensured of your managed app’s namespace being Salesforce-unique. So this namespace is just about the safest global variable you can use in the Salesforce world (anyone else who messes with it is being very, very naughty)

As a brief side-note, here’s how to ensure that your app’s “namespace global” has been defined before using it:

// Check that our Namespace Global has been defined as already,
// and if not, create it.
skuid || (window.skuid = {});

To protect your external library references, store a unique reference to these libraries within your namespace’s object, e.g. IMMEDIATELY after loading in an external library, such as MustacheJS,

// Load in Mustache -- will be stored as a global,
// thus accessible from window.Mustache
(function(){ /* MustacheJS 0.7.2 */ })()

// Store a protected reference to the MustacheJS library that WE loaded,
// so that we can safely refer to it later.
skuid.Mustache = window.Mustache;

Then, even if some other VF Component or Page loads in a different version of this library later on, you will still have access to the version you loaded (0.7.2)

// (other ISV's code)
window.Mustache = (function(){ /* MustacheJS 0.3.1 */ })()

// THIS code will run safely against 0.7.2!
skuid.Mustache.render('{{FirstName}} {{LastName}}',contactRecord);

// THIS code, however, would run against the latest version loaded in, e.g. 0.3.1,
// and thus FAILS, (since Mustache 0.3.1 has no render() method)
Mustache.render('{{Account.Name}}',contactRecord);

How to avoid having to use global references all the time

Some of you are probably thinking, “Great, now I have to prepend this global namespace all over the place!” Fortunately, with JavaScript, that’s not necessary. By wrapping your code in closures, you can safely keep using your familiar shorthand references to external libraries without worrying about version conflicts.

For instance, say that your application uses jQuery 1.8.2, but other Visualforce Components on the page are using jQuery as old as 1.3.2! (SO ancient… 🙂 What’s a developer to do?

Well, jQuery provides the helpful jQuery.noConflict() method, which allows you to easily obtain a safe reference to a version of jQuery immediately after you load it into your page. So, as an example, in YOUR code, you need to pull in jQuery 1.8.2:

<!-- load jQuery 1.8.3 -->
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script><script type="text/javascript">// <![CDATA[
// Get a safe reference to jQuery 1.8.3 var jQuery_1_8_3 = $.noConflict(true);
// ]]></script>

Then, to your horror, another Visualforce Component, that the customer you’re developing for has “got to have” in this same page (and which you don’t want to iframe…), has loaded in jQuery 1.3.2, but not bothered to namespace it!!! Therefore, both of the commonly-used jQuery globals (jQuery and $), now refer to jQuery 1.3.2!

Fortunately, FOR YOU, you’re safe! You got a protected reference to jQuery 1.8.3, and your code can carry on using $ without any issues, as long as you wrap it in a closure:

(function($){

   $('.nx-table').on('click','tr',function(){
       // Add "edit-mode" styles to this table row
       $(this).toggleClass('edit-mode');
   });

// Identify jQuery 1.8.3 as what we are referring to within this closure,
// so that we can carry on with $ as a shorthand reference to jQuery
// and be merry and happy like usual!
})(jQuery_1_8_3);

We at Skoodat are considering exposing the platform for we use for rapidly building and deploying killer custom UI’s on Force.com — aka Skoodat Skuid. But we’re looking for your feedback: are we crazy, or does this totally excite you? Let us know!

Skoodat Skuid — Wicked cool UI platform for Force.com, or not?

When I hear the words “Reports” and “Managed Packages” in the same sentence, I involuntarily let out a grunt of displeasure. Ask any seasoned ISV, and I guarantee you that the same sour taste fills their mouths. Why? Well, here’s the classic problem: An ISV includes some Reports in their managed package. Now, a common trick for making Reports “dynamic” is to leave one of the Filter Criteria blank and then have its value passed in through query string parameters using the “pv<n>” syntax, where n is the parameter you’d like to populate. For example, in this report of Enrollments at a given School, parameter 2 (0-indexed) is left BLANK:

Then, if we load up this page with query string parameter “pv2” set to the name of a School, like so:

the value that we passed in will be dynamically inserted into the 2nd Filter Criterion, and we’ll have a report on Enrollments at Lincoln High School:

This is awesome, right? Quick, let’s throw a Custom Link on the Account Detail Page called “Enrollments” that links to this report, but passing in the Id of the Report! Yeah, yeah, yeah! I love this!

Hold your horses, partner.

This is where the ISV’s hang their heads in sadness… sorry son, it just ain’t that easy.

What’s the matter, grandpa? Come on, this is child’s play!

Not quite.

Notice where we said that we’d be passing in the ID of the Report. Hard-coded. For an ISV, ID’s are the ultimate  taboo. Why? Well, sure, you can package up the Report and Custom Link. But as soon as you install the package into a customer’s org, the Id of the Report has CHANGED — and the link is BROKEN. It’s a situation that the typical one-org Admin will never have to face, but, well, welcome to the world of the ISV.

Isn’t there one of those handy Global Variables which lets you grab the Name or DeveloperName of a Report?

Nope, sorry partner.

So, what DO you do?

Well, you write a ‘ViewReport’ Visualforce page that takes in the API name of a Report — which does NOT change across all the orgs that a package is installed into — and uses this API name to find the ID of the Report and send you to it. What does this look like?

The Visualforce is simple — one line, to be exact:


<apex:page controller="ViewReportController" action="{!redirect}"/>

The Apex Controller is a little more interesting. Here’s the meat, with test code included that achieves 100% coverage (so you can start using it right away!!!):


public class ViewReportController {

    // Controller for ViewReport.page,
    // which redirects the user to a Salesforce Report
    // whose name is passed in as a Query String parameter

    // We expect to be handed 1-2 parameters:
    // r: the DEVELOPER name of the Report you want to view
    // ns: a salesforce namespace prefix (optional)
    public PageReference redirect() {
        // Get all page parameters
        Map<String,String> params = ApexPages.currentPage().getParameters();

        String ns = params.get('ns'); // NamespacePrefix
        String dn = params.get('dn'); // DeveloperName

        List<Report> reports;

        // If a Namespace is provided,
        // then find the report with the specified DeveloperName
        // in the provided Namespace
        // (otherwise, we might find a report in the wrong namespace)
        if (ns != null) {
            reports = [select Id from Report
                  where NamespacePrefix = :ns
                  and DeveloperName = :dn limit 1];
        } else {
            reports = [select Id from Report where DeveloperName = :dn limit 1];
        }

        PageReference pgRef;

        // If we found a Report, go view it
        if (reports != null && !reports.isEmpty()) {
            pgRef = new PageReference('/' + reports[0].Id);
            // Add back in all of the parameters we were passed in,
            // MINUS the ones we already used: ns, dn
            params.remove('ns');
            params.remove('dn');
            pgRef.getParameters().putAll(params);
        } else {
            // We couldn't find the Report,
            // so send the User to the Reports tab
            pgRef = new PageReference('/'
                + Report.SObjectType.getDescribe().getKeyPrefix()
                + '/o'
            );
        }

        // Navigate to the page we've decided on
        pgRef.setRedirect(true);
        return pgRef;

    }

    ////////////////////
    // UNIT TESTS
    ////////////////////

    // We MUST be able to see real Reports for this to work,
    // because we can't insert test Reports.
    // Therefore, in Spring 12, we must use the SeeAllData annotation
    @isTest(SeeAllData=true)
    private static void Test_Controller() {
        // For this example, we assume that there is
        // at least one Report in our org WITH a namespace

        // Get a report to work with
        List<Report> reports = [
            select Id, DeveloperName, NamespacePrefix
            from Report
            where NamespacePrefix != null
            limit 1
        ];

        // Assuming that we have reports...
        if (!reports.isEmpty()) {
            // Get the first one in our list
            Report r = reports[0];

            //
            // CASE 1: Passing in both namespace, developername,
            // and a parameter value
            //

            // Load up our Visualforce Page
            PageReference p = System.Page.ViewReport;
            p.getParameters().put('ns',r.NamespacePrefix);
            p.getParameters().put('dn',r.DeveloperName);
            p.getParameters().put('pv0','llamas');
            p.getParameters().put('pv2','alpacas');
            Test.setCurrentPage(p);

            // Load up our Controller
            ViewReportController ctl = new ViewReportController();

            // Manually call the redirect() action,
            // and store the page that we are returned
            PageReference ret = ctl.redirect();

            // We should be sent to the View page for our Report
            System.assert(ret.getURL().contains('/'+r.Id));
            // Also, make sure that our Filter Criterion values
            // got passed along
            System.assert(ret.getURL().contains('pv0=llamas'));
            System.assert(ret.getURL().contains('pv2=alpacas'));

            //
            // CASE 2: Passing in both just developername
            //

            // Load up our Visualforce Page
            p = System.Page.ViewReport;
            p.getParameters().put('dn',r.DeveloperName);
            Test.setCurrentPage(p);

            // Load up our Controller
            ctl = new ViewReportController();

            // Manually call the redirect() action,
            // and store the page that we are returned
            ret = ctl.redirect();

            // We should be sent to the View page for our Report
            System.assert(ret.getURL().contains('/'+r.Id));

            //
            // CASE 3: Passing in a nonexistent Report name
            //

            // Load up our Visualforce Page
            p = System.Page.ViewReport;
            p.getParameters().put('dn','BlahBLahBlahBlahBlahBlah');
            Test.setCurrentPage(p);

            // Load up our Controller
            ctl = new ViewReportController();

            // Manually call the redirect() action,
            // and store the page that we are returned
            ret = ctl.redirect();

            // We should be sent to the Reports tab
            System.assert(ret.getURL().contains(
                '/'+Report.SObjectType.getDescribe().getKeyPrefix()+'/o'
            ));

        }

    }

}

And here’s an example of using this code in a Custom Link:

Voila! A ISV-caliber way to link to any Report in any Managed Package, and it won’t break in an org where the package is installed!

The basic flow of the Apex is pretty simple: the redirect method gets called immediately upon page load, and it returns a page reference to redirect the user to. So all that Apex needs to do for us is find the Report with the provided API name / DeveloperName (and optionally in a specific namespace), and send us to /<Id>, where <Id> is the Id of that Report. Pretty straightforward. Just a few interesting points:

  1. We ‘tack-on’ to the resultant page reference any OTHER page parameters that the user passed along, so we can pass in any number of dynamic Filter Criteria parameters using the pv<n> syntax.
  2. You may be wondering — wait, you can QUERY on the Report object? Yep! Reports are technically SObjects, so you can query for them, but they fall under the mysterious category called “Setup” objects which, among other peculiar quirks (Google “MIXED_DML_OPERATION” for one of the more annoying ones), only selectively obey CRUD and only expose some of their fields to SOQL. Fortunately, for our purposes, the Id, Name, DeveloperName, and NamespacePrefix fields are all included in this short list. Actually, fire up SOQLXplorer — you might be inspired by some of the other fields that are exposed.
  3. Namespacing of Reports — Reports included in Managed Packages don’t have to have a globally unique name — they only have to be unique within their Namespace. Therefore, when querying for Reports, it’s best to query for a report within a particular namespace.
  4. If the Report is not found — in our example, we send the User to the Reports tab. You might want to do something different.

There’s nothing like a fresh Salesforce release to reassure SF consultants who, gainfully distracted by quotidian customer requests, have had their impetus to innovate temporarily quenched. Translation — I love working on a platform that innovates faster than I can.

Before Richard Vanhook jumps out of his skin (check out his excellent idea posted over a year ago), let me assure you that Salesforce has NOT added a much needed Apex API for Custom Labels / Translations. However, with Spring 12, it HAS finally enabled us to use Visualforce to dynamically render and use Custom Labels — and associated Translations — when the Label name is NOT known beforehand.

How does this make Custom Labels / Translation Workbench more useful? There are several thoughts that come to mind, but I’ll just describe one use case:

Rebranding the UI — At Skoodat, our trademark is design-oriented products, so we’ve done some heavy-duty overhauling of the standard SFDC UI, and have built a custom markup language to dynamically drive the content of our pages. Having the ability to dynamically insert text that automatically gets translated into other languages is a huge win, and the triumvirate of Salesforce’s Translation Workbench, built-in multi-language architecture, and Custom Labels, really makes the implementation of this fairly painless.

HOWEVER, for an ISV Partner interested in rendering pages dynamically based on custom metadata, this whole architecture is inaccessible, because there’s no way to access the value of a Label or Translation through Apex by name, i.e. there is no equivalent of loosely-typed System.Label.get('MyLabelName') … only the strongly-typed System.Label.MyLabelName .

So, how is this magic achieved? Well, with Spring 12, there will actually be 2 ways to achieve it:

  1. Dynamic Visualforce Bindings (which have now been extended to Global Variables)
  2. Dynamic Visualforce Components (which are finally Generally Available [i.e. not Pilot, not Beta] — and thus Packageable!!!

For this example, I first enabled Translation Workbench, and added one language: Spanish. I then created 2 Custom Labels: ‘Release’ and ‘WelcomeMessage’

Custom Labels

and then created an Apex controller called CustomLabelsController:


public class CustomLabelsController {

    public Component.Apex.OutputText output { public get; private set; }
    public String labelName { public get; private set; }

    public CustomLabelsController() {

        // Get the name of the Custom Label to display
        // from a Query String parameter
        Map params = ApexPages.currentPage().getParameters();
        labelName = params.get('label');

        // Instantiate a new Dynamic Visualforce Component,
        // and set its value attribute to a dynamic expression
        output = new Component.Apex.OutputText();
        output.expressions.value = '{!$Label.' + labelName + '}';
    }
}

This controller reads in a query string parameter called ‘label’ and assigns it to a page property (which we will retrieve from Visualforce using a Dynamic Binding), and then instantiates a new Dynamic OutputText component, setting the value property to the VF expression needed to retrieve a Custom Label.

I then created the following Page (called ‘CustomLabels’) which uses this controller:


<apex:page controller="CustomLabelsController">

    <apex:pageBlock title="Using Dynamic VF BINDINGS">
        <apex:outputText value="{!$Label[labelName]}"/>
    </apex:pageBlock>

    <apex:pageBlock title="Using Dynamic VF COMPONENTS">
        <apex:dynamicComponent componentValue="{!output}"/>
    </apex:pageBlock>

</apex:page>

In this page, I first use Dynamic VF Bindings to access the $Label Global Variable, which, as of Spring 12, can now be used with Dynamic Bindings to retrieve an arbitrary label name. I then display the Dynamic Component.

When I navigate to ‘/apex/CustomLabels?label=WelcomeMessage’, I am greeted in our language:

Where this becomes incredibly useful is when we start translating our Labels. I translated my labels into Spanish:

Spanish translation of Welcome Message

and then changed my user language to Spanish. Returning to my page, I was greeted by the following:

For  those of you who joined the Dynamic VF Components Pilot, this functionality technically has existed since Summer 11. In my mind, however, this is really a feature that would most appeal to ISV’s, so the inability to use it in Managed Packages sidelined its appeal until Spring 12.