Wednesday, 15 June 2011

Grouping Records and Children in Visualforce

I've been working this week on producing a Visualforce page custom hierarchy style view of records and their children with additional grouping.  In this scenario I firstly want to display a record and its children in indented form, e.g

Account 1
   Contact 1
   Contact 2
Account 2
   Contact 1
   Contact 2

However, I also wanted to allow the user to group the parent records by particular attributes and display count information, e.g.

Attribute Value 1 - 2 records
   Account 1
      Contact 1
      Contact 2
   Account 2
      Contact 1
      Contact 2
Attribute Value 2 - 1 record
   Account 1
      Contact 1
      Contact 2

And finally, the user could regroup on demand by choosing a different attribute from a drop down list.

As I was looking to combine records and some additional information, I headed for my old friend the wrapper class.  In this case I wanted to wrap the group value, the list of records and the size of the records list.  As an aside, if I didn't need the record count I could probably have achieved this using a Map and Visualforce Dynamic Bindings using the techniques described in this post.

The wrapper class is shown below:

public class GroupWrapper
{
 public List<Account> accs {get; set;}
 public String groupedVal {get; set;}
 public Integer count {get {return accs.size(); } set;}
}

I've created this as an inner class of my controller. My page will be backed by a list of these classes, which I initially build from the constructor, but also rebuild if the user chose to regroup by a different attribute.

The first thing my controller constructor does is to retrieve the master list of records:

allAccs=[select id, Name, BillingStreet, BillingCity, BillingCountry, Type,
       (select id, Name, Email, Phone from Contacts limit 5)
    from Account
    where Type != null
    limit 10];

Note that I'm retrieving the child records through a relationship query. This means that I don't need to worry about managing these in my wrapper class, I can simply navigate to them via the parent record when traversing the list.

Next I define the initial grouping attribute as the Type field and invoke the method that groups the data. This initially traverses the list of accounts and stores them into a Map keyed by the group attribute value, whose value is a list of the accounts whose group attribute matches that value. Once the data is organized, the keyset of the map is traversed and an instance of the wrapper class created for each key/value pair:

private void setupGrouping()
{
 Map<String, List<Account>> groupedMap=new Map<String, List<Account>>();
 for (Account acc : allAccs)
 {
  String key=String.valueof(acc.get(groupFieldName));
  if ( (null==key) || (0==key.length()) )
  {
   key='Undefined';
  }
  List<Account> groupedAccs=groupedMap.get(key);
  if (null==groupedAccs)
  {
   groupedAccs=new List<Account>();
   groupedMap.put(key, groupedAccs);
  }
  
  groupedAccs.add(acc);
 }
 
 groups=new List<GroupWrapper>();
 for (String key : groupedMap.keySet())
 {
  GroupWrapper gr=new GroupWrapper();
  groups.add(gr);
  gr.accs=groupedMap.get(key);
  gr.groupedVal=key;
 }
}


The final job of the constructor is to create the select options that allow the user to choose a different grouping:


groupOptions=new List<SelectOption>();
groupOptions.add(new SelectOption('Name', 'Name'));
groupOptions.add(new SelectOption('BillingCity', 'BillingCity'));
groupOptions.add(new SelectOption('BillingCountry', 'BillingCountry'));
groupOptions.add(new SelectOption('Type', 'Type'));

The full controller is shown below:

public class GroupingExampleController 
{
 private List<Account> allAccs {get; set;}
 public List<GroupWrapper> groups {get; set;}
 public String groupFieldName {get; set;}
 public List<SelectOption> groupOptions {get; set;}
 
 public GroupingExampleController()
 {
  allAccs=[select id, Name, BillingStreet, BillingCity, BillingCountry, Type,
         (select id, Name, Email, Phone from Contacts limit 5)
     from Account
     where Type != null
     limit 10];
  groupFieldName='Type';
  
  setupGrouping();
  groupOptions=new List<SelectOption>();
  groupOptions.add(new SelectOption('Name', 'Name'));
  groupOptions.add(new SelectOption('BillingCity', 'BillingCity'));
  groupOptions.add(new SelectOption('BillingCountry', 'BillingCountry'));
  groupOptions.add(new SelectOption('Type', 'Type'));
 }
 
 public PageReference regroup()
 {
  setupGrouping();
  return null;
 }
 
 
 private void setupGrouping()
 {
  Map<String, List<Account>> groupedMap=new Map<String, List<Account>>();
  for (Account acc : allAccs)
  {
   String key=String.valueof(acc.get(groupFieldName));
   if ( (null==key) || (0==key.length()) )
   {
    key='Undefined';
   }
   List<Account> groupedAccs=groupedMap.get(key);
   if (null==groupedAccs)
   {
    groupedAccs=new List<Account>();
    groupedMap.put(key, groupedAccs);
   }
   
   groupedAccs.add(acc);
  }
  
  groups=new List<GroupWrapper>();
  for (String key : groupedMap.keySet())
  {
   GroupWrapper gr=new GroupWrapper();
   groups.add(gr);
   gr.accs=groupedMap.get(key);
   gr.groupedVal=key;
  }
 }
 
 public class GroupWrapper
 {
  public List<Account> accs {get; set;}
  public String groupedVal {get; set;}
  public Integer count {get {return accs.size(); } set;}
 }
}

The Visualforce markup to display the data is quite straightforward - its a simple matter of generating a table by iterating the list of wrapper classes, iterating the list of accounts contained by each wrapper class, and finally iterating the associated contacts. That, plus a select list that allows the user to choose a different grouping and a command button to action the user's choice, is all there is to it:


<apex:page controller="GroupingExampleController" tabstyle="Account">
 <apex:form >
  <apex:pageBlock >
  Group By: <apex:selectList value="{!groupFieldName}" size="1">
     <apex:selectOptions value="{!groupOptions}" />
  </apex:selectList>&nbsp; <apex:commandButton value="Go" action="{!regroup}"/>
  <table border="0">
   <apex:repeat value="{!Groups}" var="group">
      <tr>
       <td colspan="3"><b>{!groupFieldName}:{!group.GroupedVal}</b> - {!group.count} records</td>
     </tr>
      <apex:repeat value="{!group.accs}" var="acc">
      <tr>
          <td width="30px"></td>
        <td colspan="2"><b>Account:</b>{!acc.Name}</td>
       </tr>
         <apex:repeat value="{!acc.Contacts}" var="cont">
           <tr>
               <td width="30px"></td>
               <td width="30px"></td>
              <td><b>Contact:</b>{!cont.Name}</td>
           </tr> 
         </apex:repeat>
      </apex:repeat>
   </apex:repeat>
  </table>
  </apex:pageBlock>
 </apex:form>
</apex:page>

And here's a screen capture of the page in action:

Wednesday, 8 June 2011

Automatic Dashboard Refresh

This weeks post concerns a piece of functionality that I've been struggling with for ages, and I'm quite pleased with myself to have it working.  It's a feature that is requested time and time again by clients - a dashboard that refreshes itself.

The sticking point for this has always been that a Visualforce component is required to add logic to a dashboard, but as Visualforce pages are contained in an iframe served from a different host to the dashboard page, it isn't possible to navigate to the Refresh button and click it programmatically - the browser blocks this as a cross-site scripting attack.  There is another technique to embed Javascript into a Salesforce page via a sidebar component, but dashboard pages don't have a sidebar so that's out too.

Thus I needed to take an alternative approach to this and started to investigate effecting the refresh server side from Apex code.

The first thing that I decided I needed to get at was the ID of the dashboard itself.  This is encoded in the dashboard URL, an example from my dev org is : https://na6.salesforce.com/01Z80000000lf7nEAA, where 01Z80000000lf7nEAA is the ID.  Once again though, it isn't possible to interrogate the URL of the main window from a Visualforce component due to Cross Site Scripting.  Thinking back to my web development days at Olive Systems Limited, it struck me that the HTTP request headers for the Visualforce component might provide some useful information.  I therefore created a Visualforce page with a controller that traversed the HTTP headers and output the details to the System log:

Map headers=ApexPages.currentPage().getHeaders();
for (String key : headers.keySet())
{
   System.debug('### ' + key + '=' + headers.get(key));
}

I then added this page to my dashboard which gave the following output in the system log:

14:50:34.055 (55491000)|USER_DEBUG|[20]|DEBUG|### Accept=application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
14:50:34.055 (55645000)|USER_DEBUG|[20]|DEBUG|### Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.3
14:50:34.055 (55790000)|USER_DEBUG|[20]|DEBUG|### Accept-Encoding=gzip,deflate,sdch
14:50:34.055 (55936000)|USER_DEBUG|[20]|DEBUG|### Accept-Language=en-GB,en-US;q=0.8,en;q=0.6
14:50:34.056 (56092000)|USER_DEBUG|[20]|DEBUG|### CipherSuite=RC4-MD5 TLSv1 128-bits
14:50:34.056 (56239000)|USER_DEBUG|[20]|DEBUG|### Connection=keep-alive
14:50:34.056 (56388000)|USER_DEBUG|[20]|DEBUG|### Host=kab-tutorial.na6.visual.force.com
14:50:34.056 (56532000)|USER_DEBUG|[20]|DEBUG|### Referer=https://na6.salesforce.com/01Z80000000lf7nEAA
14:50:34.056 (56677000)|USER_DEBUG|[20]|DEBUG|### User-Agent=Mozilla/5.0 (X11; Jolicloud Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Joli OS/1.2 Chromium/11.0.696.14 Chrome/11.0.696.14 Safari/534.24
14:50:34.056 (56823000)|USER_DEBUG|[20]|DEBUG|### X-Salesforce-Forwarded-To=na6.salesforce.com
14:50:34.056 (56967000)|USER_DEBUG|[20]|DEBUG|### X-Salesforce-SIP=80.176.152.54

and I have a result - the Referer header shows the full URL of the dashboard that my Visualforce component has been added to.

Referer=https://na6.salesforce.com/01Z80000000lf7nEAA

The next hurdle was how to programmatically trigger a dashboard refresh.  Inspecting the Refresh button didn't help much - it was tied to a bunch of Javascript that wouldn't help me server side.   Using the Fiddler web debugger, I clicked the Refresh button and monitored the requests that were submitted to the Salesforce server with fingers crossed that it would be a GET rather than a POST request.  Once again I had a result and could see that one of the requests was an HTTP GET on:


https://na6.salesforce.com/dash/dashboardRefresh.apexp?id=01Z80000000lf7nEAA.

I copied this URL and submitted it directly via the browser. The first attempt returned a minimal page body that the refresh had been submitted - not quite what I had in mind. Upon requesting the page again, I received another small page, but this time telling me that the dashboard had been refreshed. Opening the dashboard page in another tab showed that the refresh had taken place successfully.

At this point I started to think that this might be possible after all!

Next up I added code to my controller to retrieve the referrer and extract the id.  Upon adding this to my dashboard it threw an error.  Not quite what I had in mind.  The reason for this turned out to be simple - the URL for a dashboard edit page is in a different format to the view.  A similar issue was waiting when the dashboard was embedded in a Home page - in this case the Referer header wasn't present.  Luckily these were straightforward to detect and put out a message that the dashboard wouldn't refresh.

I then created an action method that would carry out the HTTP GET, by creating a PageReference to the  refresh page identified above, adding the id to the parameters and executing the getContent() method.  This code is inside a loop, which will make a maximum of 10 attempts to refresh before giving up.  An actionFunction on the page in association with a Javascript setTimeout() invoked the refresh action method 60 seconds after the page was loaded.   Finally, using a technique outlined in an earlier post from this blog, I was able to automatically reload the dashboard after the refresh.

Refreshing after 60 seconds seemed far too arduous for the Salesforce servers, so I upped the timeout to one hour.  This introduced the next issue - session timeout.  The next version reduced the timeout to 60 seconds, but for the first 59 times the timeout expired, the page simply refreshed.  Only on the 60th did the dashboard actually refresh.  This nicely circumvented the session timeout, as the page was submitted every minute.

Finally, I wanted to give the users a visual indication that the dashboard would automatically refresh, resulting in a recurring timeout that fired every second and counted down the seconds until refresh.

While this is probably starting to sound like quite a lot of code, there isn't actually that much to it.

Controller:

////////////////////////////////////////////////////////////
//
// Custom controller for dashboard refresh Visualforce page
//
// Author: Keir Bowden
//
////////////////////////////////////////////////////////////
public with sharing class DashboardRefreshController
{
 public Boolean needsRefresh {get; set;}
 public Boolean canRefresh {get; set;}
 public Id dbIdent {get; set;}
 public Integer minutes {get; set;}
 
 public DashboardRefreshController()
 {
  needsRefresh=true;
  setup();
  minutes=59;
 }
 
 public void setup()
 {
  Map<String, String> headers=ApexPages.currentPage().getHeaders();
  String referrer=headers.get('Referer');
  
  if (null==referrer)
  {
   canRefresh=false;
  }
  else
  {
   Integer lastSlashPos=referrer.lastIndexOf('/');
   lastSlashPos++;
   Integer paramPos=referrer.indexOf('?', lastSlashPos);
  
  
   String result='';
   if (-1!=paramPos)
   {
    result=referrer.substring(lastSlashPos, paramPos);
   }
   else
   {
    result=referrer.substring(lastSlashPos);
   }
   
   try
   {
    dbIdent=result;
    canRefresh=true;
   }
   catch (Exception e)
   {
    canRefresh=false; 
   }
  }
 }
 
 public PageReference refreshDashboard()
 {
  minutes--;
  if (-1==minutes)
  {
   needsRefresh=false;
   String refUrlStr='/dash/dashboardRefresh.apexp?id='+dbIdent;
   Boolean refreshed=false;
   Integer idx=0;
   while ( (!refreshed) && (idx<10) )
   {
    PageReference pr=new PageReference(refUrlStr);
    Blob body=pr.getContent();
    String bodyStr=body.toString();
    refreshed=(-1!=bodyStr.indexOf('Last refreshed'));
    idx++;
   }
  }
   
  return null;
 }
}

Page:


<apex:page sidebar="false" showheader="false" standardstylesheets="false" controller="DashboardRefreshController">

<apex:outputPanel rendered="{!canRefresh}">
 <apex:form >
     <apex:actionFunction name="doRefresh" action="{!refreshDashboard}" />
     <apex:outputPanel id="detail">
   <div id="countDown"></div>
     </apex:outputPanel>
 </apex:form>

 <apex:outputPanel id="scriptPanel">
     <apex:outputPanel rendered="{!needsRefresh}">
   <script>
    window.onload = function() 
    {
     startCountDown(59, 1000, doRefresh);
    } 

    function startCountDown(i, p, f) 
    {
     var pause = p;
     var fn = f;
    
     var countDownObj = document.getElementById("countDown");
     if (countDownObj == null) 
     {
      alert("div not found, check your id");
      return;
     }
    
     countDownObj.count = function(i) 
     {
      countDownObj.innerHTML = 'Refreshing in {!minutes} minutes ' + i + ' seconds';
      if (i == 0) 
      {
       fn();
       return;
      }
      setTimeout(function() 
       {
        countDownObj.count(i - 1);
       },
       pause
      );
     } 
  
     countDownObj.count(i);
    }
   </script>
     </apex:outputPanel>
     
  <apex:outputPanel rendered="{!NOT(needsRefresh)}">
   <script>
       window.top.location='/{!dbIdent}';
   </script>
     </apex:outputPanel>
 </apex:outputPanel>

</apex:outputPanel>
<apex:outputPanel rendered="{!NOT(canRefresh)}">
   Edit mode/home page - refresh disabled
</apex:outputPanel>


</apex:page>

Below is a screenshot of the page embedded in a dashboard:


Once the countdown reaches 0 minutes and zero seconds, the page is automatically refreshed and reloaded to show the updated details.

A couple of words of caution:


  • The page that I'm hitting to refresh isn't documented by Salesforce, which most likely means it is unsupported.  Thus a future release could easily break this functionality.
  • I haven't tried this on a hugely complex dashboard, so I don't know what would happen if the dashboard took significant time to refresh.
  • This is using the browser to trigger the refresh, so if you close the browser session, the refresh won't happen.
  • Refreshing dashboards pulls in information from a number of sources, so refreshing with a short time interval will put additional strain on the Salesforce servers.  
  • I've only tested this with google chrome, so it may not work with other browsers.

Saturday, 4 June 2011

Execute Custom Search when Opening Page

This week's post is another generated from posts on the Salesforce discussion boards.

The scenario that was troubling the poster was as follows:

  • They had written a custom search page
  • When the results of the search were displayed, the user could click on a result and be taken to another page to edit the record
  • Once the change was made, the user should be returned to the search page
  • The search page should display the previous list of results - i.e. retain the query that was executed
It was the final bullet point that was causing the problem - how to retain the query through the call to the edit page and back again.

In this example, I'm using the standard Salesforce edit page.

The controller code is shown below:

public class RetUrlSearchController 
{
 public String nameQuery {get; set;}
 public List<Account> accounts {get; set;}
 
 public PageReference executeSearch()
 {
  String queryStr='%' + nameQuery + '%';
  accounts=[select id, Name, BillingStreet 
            from Account 
            where name like :queryStr];
           
  return null;
 }
 
 public RetUrlSearchController()
 {
  // if query appears in URL, execute it
  String urlQuery=ApexPages.currentPage().getParameters().get('query');
  
  if ( (null!=urlQuery) && (0!=urlQuery.length()) )
  {
   nameQuery=urlQuery;
   executeSearch();
  }
 }
}

The important part of the controller is the constructor - this looks for a query string supplied in the URL and if one is found, executes the search and populates the results.

Next up the page:

<apex:page controller="RetUrlSearchController">
  <apex:form >
    <apex:pageBlock >
      <apex:pageBlockSection title="Criteria">
      <apex:outputLabel value="Enter Name Snippet"/>
      <apex:inputText value="{!nameQuery}"/>
      <apex:commandButton action="{!executeSearch}" value="Search"/>
   </apex:pageBlockSection>
   
   <apex:pageBlockTable value="{!accounts}" var="acc">
      <apex:column headerValue="Name">
         <apex:outputLink value="/{!acc.id}/e?retURL={!URLENCODE('/apex/RetUrlSearchPage?query='+nameQuery)}">{!acc.Name}</apex:outputLink>
      </apex:column>
      <apex:column value="{!acc.BillingStreet}"/>
   </apex:pageBlockTable>
 </apex:pageBlock>
  </apex:form>
</apex:page>

Notice how the link to the edit page is constructed:

<apex:outputLink value="/{!acc.id}/e?retURL={!URLENCODE('/apex/RetUrlSearchPage?query='+nameQuery)}">{!acc.Name}</apex:outputLink>

This sets the retURL parameter which will be used by Salesforce to determine which page to return the user to after their editing is complete.    As well as the page, I've also added the query that the user last entered.  Thus when the browser is returned to the RetUrlSearchPage, the constructor shown earlier will pull the query from the URL, execute the search and the page displayed to the user will be that of their last search.

Here are screenshots showing the page flow.  First up is the search page with a query executed - note there are no parameters in the URL:


I then click on the Name link and am taken to the standard edit page but with the retURL parameter set in the URL:



Note that I've edited the Billing Street value.  I then click Save, which takes me back to the search page and executes the search:


Looking at the URL shows that the query parameter is set, the search is therefore automatically executed and the edited version of the BrightGen account appears.

Sunday, 29 May 2011

Chatter Autofollow Users

The subject of today's blog post is auto-following users.  This can be useful if you have people who are important with your organization (or think they are and you have no choice but to agree with them) and you want to ensure that all new users follow them in chatter. Regular readers of this blog will no doubt be surprised to see that there is no Visualforce aspect to this!

The first part of the solution is to create a public group containing the Users to be followed - I've chosen to call mine "Auto Follow Users" and the only important user is me!

Next up is an after insert trigger on the user object, as shown below:

trigger ai_user on User (after insert) 
{
   ChatterAutofollow.AutofollowUsers(trigger.newMap.keySet());
}

Not much to the trigger as I'm handing off to a utility class method to actually carry out the work. Usually this is a matter of personal preference, but in this case it has to be done that way for reasons which will be explained later.

The utility class is shown below:

public class ChatterAutofollow
{
   // constant group name
   public static final String AUTO_FOLLOW_GROUP='Auto Follow Users';
 
 
   // method has to be @future as cannot mix setup/non-setup DML in the same transaction      
   @future
   public static void AutofollowUsers(Set<Id> userIds)
   {
      // set up the users that should automatically be followed
      List<User> usersToFollow=new List<User>();
 
      Group autoFollowGroup=[Select Id From Group Where Name = :AUTO_FOLLOW_GROUP];
    
      List<GroupMember> members=[Select Id, UserOrGroupId From GroupMember Where GroupId = :autoFollowGroup.id];
      List<EntitySubscription> entSubs=new List<EntitySubscription>();
    
      // loop the users that have been created
      for (Id userId : userIds)
      {
         // Loop through all members in a group
      for (GroupMember member : members)
      {
     EntitySubscription entSub = new EntitySubscription (parentId = member.UserOrGroupId,
                                     subscriberid = userId);
       entSubs.add(entSub);
       }
      }
      insert entSubs;
   }
}

The method has the @future annotation, and this is the reason why the work has to be done by a utility class. Apex doesn't allow setup (i.e. User) DML to be mixed with non-etup (i.e. chatter EntitySubscription) in the same transaction. While I'm not carrying out any setup DML in my code, as this trigger is invoked from the insertion of a User, another part of the platform certainly has done this. Thus the insertion of the EntitySubscriptions takes place asynchronously in a separate transaction.

The method itself is pretty straightforward. First the group details are retrieved:

Group autoFollowGroup=[Select Id From Group Where Name = :AUTO_FOLLOW_GROUP];
    
List<GroupMember> members=[Select Id, UserOrGroupId From GroupMember Where GroupId = :autoFollowGroup.id];

Next the subscription objects are created, by iterating the inserted user ids and for each of these, iterating the auto follow group members and creating an EntitySubscription to their feed. In accordance with best practice and being able to handle bulk mode triggers, the subscriptions are added to a list and inserted en-masse at the end of the processing.



for (Id userId : userIds)
// loop the users that have been created
{
   // Loop through all members in a group
   for (GroupMember member : members)
   {
     EntitySubscription entSub = new EntitySubscription (parentId = member.UserOrGroupId,
                                     subscriberid = userId);
     entSubs.add(entSub);
   }
}
insert entSubs;

To check everything works as expected,  I create a new user called Fred Blogger and receive a confirmation email that he is now following me:

Saturday, 21 May 2011

Onload Handling

There's been a few posts around page onload handling on the Visualforce discussion board in the last couple of weeks, specifically around breaking existing Salesforce behaviour when adding a custom onload handler.

An example of this is adding a custom onload handler to a Visualforce page that contains a Date input field.

Here's my original page with no custom onload handler:

<apex:page standardController="Opportunity">
   <apex:form >
      <apex:pageBlock mode="detail">
         <apex:pageBlockSection columns="2">
            <apex:inputField value="{!Opportunity.Name}" />
            <apex:inputField value="{!Opportunity.AccountId}" />
            <apex:inputField value="{!Opportunity.CloseDate}" />
            <apex:inputField value="{!Opportunity.Amount}" />
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

As expected, when I click into the Close Date field, the date picker appears and I can select the appropriate date:




However, if I add the following javascript to the bottom of the page, specifying my custom onload handler, things go awry:


<script>
      window.onload=function()
      {
         alert('My onload handler');
      };
</script>



The alert displays as expected:


However, once I've okayed the alert, I find that clicking into the Close Date field no longer displays a date picker.  It looks from this that the date picker is added through a javascript onload handler, which I have  overwritten with my onload function.

The solution is to check for an existing onload function before specifying my own, and if there is one already defined, append my function to that.

Here's the javascript to do exactly that:

function addLoadEvent(func) 
{ 
  var oldonload = window.onload; 
  if (typeof window.onload != 'function') 
  { 
     window.onload = func; 
  } 
  else 
  { 
      window.onload = function()  
      { 
        if (oldonload) 
        { 
           oldonload(); 
        } 
        func(); 
      } 
   } 
} 
  
addLoadEvent(function()  
{ 
   alert('My onload handler');
});

The addLoadEvent function checks if there is an onload handler specified, and if there isn't simply sets it to my function.  If there is one, a new function is created that invokes the existing onload handler followed my function and this is added as the onload handler.

Thus I end up with a chain of functions being executed when the page is loaded, starting with that defined by Salesforce.

Saturday, 14 May 2011

Updating Attributes in Component Controller

A while ago I was working on a  component that would allow a user to select/input a number of data values, and return these as a semi-colon separated String to the controller of the parent page.

So I created my component, taking the String as an attribute and assigning it to a variable in the component's controller.

Component:

<apex:component controller="StringEntryController">
   <apex:attribute type="String" name="theString" description="The string to update" assignTo="{!stringVal}"/>
   <apex:outputLabel value="Select the string components"/>
   <apex:selectList value="{!chosenVals}" multiSelect="true" size="4">
     <apex:selectOption itemValue="UK" itemLabel="UK"/>
     <apex:selectOption itemValue="USA" itemLabel="USA"/>
     <apex:selectOption itemValue="Canada" itemLabel="Canada"/>
     <apex:selectOption itemValue="France" itemLabel="France"/>
     <apex:selectOption itemValue="Japan" itemLabel="Japan"/>
   </apex:selectList> 
</apex:component>

controller:


public class StringEntryController {
 public String stringVal {get; set;}
 public String[] chosenVals {get; 
                         set {
                                stringVal='';
                                for (String val : value)
                                {
                                 stringVal+=':' + val;
                                }
                           
                             stringVal=stringVal.substring(1);
                             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, ' Set string val to ' + stringVal));
                             }
                         }
 
 
}

I've added a message to the page to confirm that the String has been updated with the user's selections.

Here's the page that accesses the component.

<apex:page controller="PrimitiveCarrierController">
  <apex:form >
   <apex:pageBlock >
      <apex:pageMessages />
      <apex:pageBlockSection columns="1">
        <apex:pageBlockSectionItem >
        <apex:outputText value="The string = {!enteredString}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <c:StringEntry theString="{!enteredString}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <apex:commandButton value="Update"/>
        </apex:pageBlockSectionItem>
      </apex:pageBlockSection>
   </apex:pageBlock>
  </apex:form>
</apex:page>

And finally the page controller. Note that enteredString is initialised to 'This is the default from the controller' so that I can check that it has changed.

public class PrimitiveCarrierController 
{
 public String enteredString {get; set;}
 
 public PrimitiveCarrierController()
 {
  enteredString='This is the default from the controller';
 }
}

I then access the page, select some values from the component and click the Update button. The
output is as follows:


As you can see, while the value of the String has changed in the component, as is shown by the value in the page message, the value in the page's controller hasn't.

Reading the Apex Developer's Guide indicates that this is because Strings are passed by value rather than by reference. This means that the String that my component controller is working on is different to the one in the page controller, so it doesn't matter what my component controller does, the String from the page controller is unaffected.

The solution is to put the String inside an object, as these are passed by reference rather than by value, so changing the value of the contained String in the component controller affects the actual object instance that the page controller is managing.

Here is the class that contains the String:

public class StringCarrier {

    public String value {get; set;}
}

The revised component:

<apex:component controller="StringEntryControllerV1">
   <apex:attribute type="StringCarrier" name="theString" description="The string to update" assignTo="{!stringVal}"/>
   <apex:outputLabel value="Select the string components"/>
   <apex:selectList value="{!chosenVals}" multiSelect="true" size="4">
     <apex:selectOption itemValue="UK" itemLabel="UK"/>
     <apex:selectOption itemValue="USA" itemLabel="USA"/>
     <apex:selectOption itemValue="Canada" itemLabel="Canada"/>
     <apex:selectOption itemValue="France" itemLabel="France"/>
     <apex:selectOption itemValue="Japan" itemLabel="Japan"/>
   </apex:selectList> 
</apex:component>

The revised component controller:

public class StringEntryControllerV1 {
 public StringCarrier stringVal {get; set;}
 public String[] chosenVals {get; 
                         set {
                                stringVal.value='';
                                for (String val : value)
                                {
                                 stringVal.value+=':' + val;
                                }
                           
                             stringVal.value=stringVal.value.substring(1);
                             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, ' Set string val to ' + stringVal.value));
                             }
                         }
 
 
}

The revised page.

<apex:page controller="PrimitiveCarrierControllerV1">
  <apex:form >
   <apex:pageBlock >
      <apex:pageMessages />
      <apex:pageBlockSection columns="1">
        <apex:pageBlockSectionItem >
        <apex:outputText value="The string = {!enteredString.value}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <c:StringEntryV1 theString="{!enteredString}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <apex:commandButton value="Update"/>
        </apex:pageBlockSectionItem>
      </apex:pageBlockSection>
   </apex:pageBlock>
  </apex:form>
</apex:page>

And the revised page controller. Note that enteredString is initialised in the same way as before. Note also that the StringCarrier property must be requires instantiation unlike a String primitive.

public class PrimitiveCarrierControllerV1 
{
 public StringCarrier enteredString {get; set;}
 
 public PrimitiveCarrierControllerV1()
 {
  enteredString=new StringCarrier();
  enteredString.value='This is the default from the controller';
 }
}

And as is shown below, the changes applied by the component propagate to the page:

Saturday, 7 May 2011

Refreshing Record Detail from Embedded Visualforce Page

Strange how things go in cycles.  This week there's been several questions on the discussion boards, asking how to refresh the standard record view page from an Visualforce page.  An example of an embedded page is shown below:


The purpose of the embedded page allows the user to choose a country ISO code and when the save button is clicked, populate the account billing country field with the full name of the account.

The controller method to carry out the save:

public PageReference save()
{
 Account acc=(Account) stdCtrl.getRecord();
 acc.BillingCountry=chosenCountry;
  
 return stdCtrl.save();
}


As this is an extension controller, the account is retrieved from the standard controller to update the field. The return value is the result of the standard controller save method, which is the standard record view page.

Unfortunately, as the Visualforce page is embedded in the standard view via an iframe, this simply puts the full record view page inside the existing record view page:


Not exactly what I had in mind!

Unfortunately there's no way to return a page reference to the browser that tells it to refresh the entire page, rather than just the iframe.   It is possible, however, to use javascript to carry out a client side redirect back to the record view.  Thus there needs to be some javascript that is conditionally rendered into the page after the save (and only after the save, otherwise the page will continually refresh) that executes the refresh.

The full revised controller is shown below - the new aspect is the refreshPage property, which is initialised to false in the constructor and set to true when the record is saved. I've also changed the controller to return null, as I need the embedded page to refresh and render the javascript.

public class EmbeddedController
{
 public ApexPages.StandardController stdCtrl {get; set;}
 public String chosenCountry {get; set;}
 public Boolean refreshPage {get; set;}
 
 public EmbeddedController(ApexPages.standardController std)
 {
  stdCtrl=std;
  refreshPage=false;
 }
 
 public PageReference save()
 {
  Account acc=(Account) stdCtrl.getRecord();
  acc.BillingCountry=chosenCountry;
  refreshPage=true;
  stdCtrl.save();
  
                return null;
 }
}


and the important new element in the page:

<apex:outputPanel rendered="{!refreshPage}">
   <script>
      window.top.location='/{!Account.id}';
   </script>
</apex:outputPanel>


Now when I click the save button in the embedded Visualforce page, the entire record view page refreshes and my chosen country appears in the billing address.

Update: 22/12/2012

A number of the comments for this post have asked about achieving this via the service cloud console.  This mechanism won't work I'm afraid, due to the browser same origin policy.  Looking at my javascript error console in chrome, I see 'Unsafe Javascript' exceptions when the refresh method fires.  I've also investigated converting the embedded page to a custom console component and using the integration toolkit to refresh the enclosing tab, but the same errors are thrown, event after whitelisting every domain involved.