Tuesday, March 6, 2012

Visualforce custom related list component

     I came across a business need that was interesting the other day and thought that I would post my solution in order to help others with the same need.  The solution was actually not very complex, but I found it interesting in how it shows the strength of dynamic VF and some weakness of combining dynamic VF with a custom component.

     Some quick background, the company that I wrote this for has bastardized the Opportunity object and uses it as a Lead, Opportunity, and Quote object all rolled into one.  Since they sell highly customized products, the opportunity object has several record types and many custom fields to allow all this functionality.  What I was doing was adding some fields that are needed on every record type of opportunity.  Since most of the standard page layouts for the record types were already fairly cluttered, it was decided that we would put these fields on a custom VF page that would be accessed from a button on the Opportunity.

    So far so good, but then the project expanded and we needed to add another opportunity child object, however we wanted the related list to be on this custom VF page, but not on the main opportunity pages.  I tried the tag <apex:relatedList>  and found something interesting.  That tag took all of its setting for the related list from the default page layout for the record type of the record.  When I would remove the related list from the default page layout, the related list on my VF page would only show the name field for the child records.  As far as I can find with internet searches, reading the documentation and even message board postings, there is no way around this behavior.

     Looking at the situation, I decided that I could code my own related list, but I decided that I would attempt to make a custom component out of it, so that I could reuse it in any similar situations.  I decided that the custom component would use dynamic apex to generate the VF tags making it generic as possible.  In essence I wanted to be able to do this:

<c:customRelatedList object = "my_Child__c" fields = "ID,Name,my_Field_1__c" lookup  = "myLookupField__c"></c:customRelatedList>

     Here is where I ran into problems.  Specifically I ran into problems with when the attributes are bound to variables in the controller of the custom component.  Essentially, from what I gather, the members of the controller class are not set unit after the component is rendered.  Basically it seems to me that the components controller is initialized, the VF tags in the component are rendered, then the attributes are bound to the member variables, finally all this is returned to the VF page.

     This proved problematic because I could not access the fields attribute when the component called the dynamic VF method, and so I had no way to tell the VF how many fields were being displayed on the related list, or any way to set the column headers to the column name.  My solution to this, is to use URL parameters and set the appropriate member variables in the constructor.  Not the best situation, but the only one that was possible without hard coding the data in the controller class.  Both ways would have their pluses and minuses, and I believe that the controller could be easily adapted to be hard coded instead of using URL parameters.

Anyway, I know that that was a little long winded, but here is the code that I ended up using, please comment if you have any suggestions or questions:



/*Class CustomRelatedList

* Driver class responsible for creating and displaying customized related
* lists on Visualforce pages
* Data Members:
*   objectType - the object to query for the related list records
*   fields - A single string containing all the API field names separated
*            by commas.
*   relatedField - the name of the field that contains lookup information
*                  to the main record displayed on the page.
*   thisID - A string containing the id of the main record on the page.
*   fieldList - a list of strings where each member contains the API name of
*               a field to display on the related list.
*   query - a string that contains the query that retrieves the records
*           for the related list.
*   objPrefix - a string that contains the three letter system prefix for
*
*   data - a list of sObjects that contain the data returned by the query.
*   oppName - the name of the opportunity being acted on in this particular
*             implementation. Used to set a field on the child object when
*             the 'New' button is pressed
*  
*
* Methods:
*   CustomRelatedList() - constructor, sets up the memeber variables.
*   Component.Apex.Pageblock getOutput() - outputs the related list to
*                         the calling custom component.       
*   Component.Apex.OutputText getError() - a bebugging method to output
*                         any necessary debugging data.
*   public string createQuery() - creats the SOQL query for the related records.3
*   sObject[] getRecords () - returns the records for display.
*   string[] splitFields() - splits a comma seperated list of fields into an array.
*   public string getObjectCode() - returns the three character object code for the related
*                         object.
*   public pageReference newQ() - returns a page reference to the new record page of the
*                         related object.
*   public string fieldCleanUp (string s) - returns a readabe field name when given an
*                         API field name by removing all underscores and the last '__c'
*   static testMethod void testCustomRelatedList() - test method for the class
*
* Created February 20, 2012 by Jacob Gmerek, change log to follow
*  
*/

Public Class CustomRelatedList{

    public String objectType {get; set;}
    public String fields {get; set;}
    public String relatedField {get; set;}
    public String thisID {get; set;}
    public String[] fieldList = new string[]{};
    public String query{get; set;}
    public String objPrefix {get; set;}
    public sObject[] data {get; set;}
    public string oppName {get; set;}
       
    public CustomRelatedList() {
      //get URL parameters
      thisID = System.currentPageReference().getParameters().get('id');
      objectType = System.currentPageReference().getParameters().get('RelatedObj');
      relatedField = System.currentPageReference().getParameters().get('RelatedField');
      fields = System.currentPageReference().getParameters().get('Fields');
      //Set up other data members
      query = this.createQuery();
      fieldList = this.splitFields();
      objPrefix = this.getObjectCode();
      system.debug ('Query---->' + query);
      data = Database.query(query);
      List o = new List([select name from opportunity where ID =: thisID LIMIT 1]);
      if (o.size() > 0){
      oppName = o[0].name;
      }
    }
    Public Component.Apex.Pageblock getOutput(){
     //set up outer components   
     Component.Apex.Pageblock myPageBlock = new Component.Apex.Pageblock();
     myPageBlock.title = 'Opportunity Questions';

     Component.Apex.PageblockTable myPageBlockTable = new Component.Apex.PageblockTable();
     myPageBlockTable.expressions.value = '{!records}';
     myPageBlockTable.var = 'Record';

     Component.Apex.PageblockButtons myPageblockButtons = new Component.Apex.PageblockButtons();
     myPageblockButtons.location = 'Top';
     Component.Apex.CommandButton newButton = new Component.Apex.CommandButton();
     newButton.expressions.action = '{!newQ}';
     newButton.value = 'New';

     myPageBlock.childComponents.add(myPageblockButtons);
     myPageBlock.childComponents.add(myPageBlockTable);
     myPageblockButtons.childComponents.add(newButton);
     //Loop through the field list to set up the columns
     for (integer i = 0; i < fieldList.size(); i++){

        Component.Apex.Column col = new Component.Apex.Column();
        Component.Apex.outputField out = new Component.Apex.outputField();
        out.Expressions.value = '{!Record.' + fieldList[i] + '}';
        col.headerValue = fieldCleanUp(fieldList[i]);

        //Set the name field as a hyperlink
        if (fieldList[i] == 'Name'){   
          Component.Apex.outputLink Link = new Component.Apex.outputLink(); 
          Link.expressions.Value = '/{!Record.ID}';
          myPageBlockTable.childComponents.add(col);
          Link.childComponents.add(out);
          col.childComponents.add(Link);
        }//end if
        else{
          myPageBlockTable.childComponents.add(col);
          col.childComponents.add(out);
        }//end else
      }//end for
    return myPageBlock;
    }
    Public Component.Apex.OutputText getError(){
      Component.Apex.outputText err = new Component.Apex.outputText();
      err.value = ''; //use for debugging
      return err;
    }

    public string createQuery(){
      return 'Select ' + fields + ' from ' + objectType + ' WHERE ' + relatedField + ' = \'' + thisID + '\'';
    }

    public sObject[] getRecords (){
      return data;
    }
    public string[] splitFields(){
      return fields.split(',', 0);
    } 

    public string getObjectCode(){
      Map objs = schema.getGlobalDescribe();
      return objs.get(objectType).getDescribe().getKeyPrefix();
    }

    public pageReference newQ(){
    // there are some hard coded parameters to  fill out the lookup/detail field
    // when creating a new child. 
      return new pagereference('/'+ objPrefix + '/e?retURL=' + thisID + '&CF00NR0000000tCFQ_lkid=' + thisID + '&CF00NR0000000tCFQ=' + oppName);
    }

    public string fieldCleanUp (string s){
      s = s.replace('__c', '');
      s = s.replace('_', ' ');
      return s;
    }

    static testMethod void testCustomRelatedList(){
 
      account a = new account(Name ='test',BillingState='OH');
      insert a;

      contact c = new contact (accountID = a.id, mailingState = 'OH', lastName = 'test');
      insert c;
      pageReference pageRef = new pageReference('/apex/myPage?id=' + a.id + '&RelatedObj=Contact&fields=id,name&RelatedField=AccountId');
      test.setCurrentPageReference(pageRef);

      CustomRelatedList rel = new CustomRelatedList();
      rel.getOutput();
      rel.getError();

      contact[] cList = rel.getRecords();
      system.assertEquals(cList[0].id, c.id);

      pageReference p = rel.newQ();
      system.assertEquals(p, new pageReference('/'+ rel.objPrefix + '/e?retURL=' + rel.thisID + '&CF00NR0000000tCFQ_lkid=' + rel.thisID + '&CF00NR0000000tCFQ=' + rel.oppName));
    }
}

And here is the actual component, it is pretty straight forward, but I thought that I would provide it anyway:

<apex:component controller="CustomRelatedList">
  <apex:dynamicComponent componentValue="{!output}"/>
  <apex:dynamicComponent componentValue="{!error}"/>
</apex:component>

Finally, here is an example URL from my org that calls the appropriate VF page and supplies the parameters to show the related list.
https://na3.salesforce.com/apex/SalesTracking?id={!Opportunity.Id}&RelatedObj=Opportunity_Questions__c&Fields=Name,Question__c, Answer__c,Asking_Department__c,Completed_Date__c&RelatedField=Opportunity__c



2 comments:

Anonymous said...

Beautiful code, saved my VF page. Leonardo

Unknown said...

I am getting this error "The attempted delete was invalid for your session. Please confirm your delete.", when I used this code sample, and tried to delete a record in the custom related list section. How do I fix this error? Please help me.