+ Start a Discussion
Charles ThompsonCharles Thompson 

How to replace Related Lists (Classic) to get past some platform limitations

User-added image
I have come up with a way to replace Classic related lists on a page layout, so you can get past some of the limitations of page layouts. I hope this post will be helpful to our global ohana.

Why would you want to do this?
Related Lists can only have up to 10 columns.  My customer has a related custom object with 14 fields that should be displayed on a single line:  Year, Jan ... Dec, and Year Total.  Each monthly value is a currency field, and multi-currency is on for the Org, meaning that the currency ISO code is displayed for each of these fields.  So we also want to display the values without the ISO code.

What did I do to get around this?
My solution includes two Visualforce (VF) pages and a controller extension.
  1. View page:  A page thatdisplays the fields in a table, as read-only. On this page is a single button, [Edit] to switch to Edit mode.
  2. Edit page: A page that displays the fields in a table, as editable fields. It has three buttons: [Add] [Save] [Cancel]
  3. Apex controller extension. I used an extension because it adds to the functionality of the standard controller, which could be helpful in more complex use cases.
In my example, I have a new custom object, Budget, related to Contact in a master/detail relationship. I declined to put the related list on the Contact page layouts because I added my own VF page to the layout details section.  If you want to duplicate my code exactly in your DevEd org, you'll need to create this object and relate it to Contact.

Apex Controller Extension (BudgetExt.apxc):
public with sharing class BudgetExt {
    
/*************************************************
 * Controller extension to provide functionality required for the Budgets
 * section of Contact detail page.  The business requirement was to allow
 * the Budgets related list on Contact to display 14 columns (year
 * + 12 months + total).  The end result is an embedded VF page component on the Contact
 * page layout that displays the Budget related rows in view mode 
 * (read only), with an edit button local to the component.  Clicking Edit 
 * switches the component to Edit Mode by changing the PageReference to
 * the related Edit mode VF page.  On that page are three buttons (Add Row, 
 * Save and Cancel) plus a Delete button on each row.  Hitting Save or Cancel
 * brings you back to the View mode page.
 * 
 * Related VF pages:
 * - BudgetEdit.vfp
 * - BudgetView.vfp
 * 
 * Author: Charles Thompson, Salesforce, Nov 2017
 * *************************************************/
    
    private final Id contactID; // store the parent (Contact) ID
    public Integer index {get;set;} // store the current row being deleted
    
    // List to represent each row of the pageBlockTable of Budgets 
    // related to the parent Sub Project
    public List<Budget__c> budgets {get; set;}

    // List to hold those rows being deleted
    public List<Budget__c> budgetsToDel = new List<Budget__c>();
    
    // Variable to hold the Sub Project (parent) record
    public Contact theContact {get; set;}
    
    // Initial load of the pageBlockTable
    public BudgetExt(ApexPages.StandardController stdController){
        contactID = stdController.getId();
        this.index = 0;
        theContact = [SELECT Id
                      FROM   Contact
                      WHERE  Id = :contactID
                      LIMIT 1
                     ];
        budgets =    [SELECT Id,
                             Year__c,
                               Jan__c, Feb__c, Mar__c, Apr__c,
                               May__c, Jun__c, Jul__c, Aug__c,
                              Sep__c, Oct__c, Nov__c, Dec__c,
                             Year_Total__c
                      FROM   Budget__c
                      WHERE  Contact__c = :contactID
               ORDER BY Year__c
              ];
    }
                              
    public String getIndex(){
        return String.valueOf(index);
    }
    
    public void setIndex(String indexIn){
        this.index = Integer.valueOf(indexIn);
    }
            
    // Add an empty budget row to the table 
    // (not saved to the database until saveBuds() is called)
    public void addRow(){
        Budget__c row = new Budget__c();
        row.Contact__c = contactID;
        budgets.add(row); 
    }
    
    // delete a forecast from the table (not saved to the db until later)
    public void delRow(){
        
        Integer i = this.index-1; // Rows start at one, Lists start at zero
        
        // If the row is an existing row then add it to the list 
        // to delete from the database
        if (budgets[i].Id != null){
            budgetsToDel.add(budgets[i]);
        }
        //Remove the forecast from the table    
        budgets.remove(i);
    }
    
    public PageReference saveBuds(){
        // update and insert forecasts as per the current list
        upsert budgets;
        
        // delete any rows in the list of deletions
        if ((budgetsToDel != null) && (budgetsToDel.size() > 0)){
            delete budgetsToDel;
        }
        
        return cancelEdit();
    }
    
    public PageReference editBuds(){
        // switch to Edit mode
        PageReference editPage = new PageReference('/apex/BudgetEdit?id=' + contactID);
        return editPage;
    }

    public PageReference cancelEdit(){
        // switch back to View mode without saving anything
        PageReference viewPage = new PageReference('/apex/BudgetView?id=' + contactID);
        viewPage.setRedirect(true);
        return viewPage;
    }   
}

View VF page (BudgetView.vfp):
<apex:page standardController="Contact" extensions="BudgetExt">
<!------------------------------------------------------------------------ 
    This page is designed to fit as a component on the detail page of 
    the Contact page layout.  This is the View Mode version of the page, 
    containing output fields and a button for Edit.
    
    Note that the currency ISO code doesn't appear and is assumed to be 
    carried through from the parent record.
     
    The related Edit Mode page is called "BudgetEdit".
     
    Author: Charles Thompson, Salesforce Singapore, Nov 2017 
--------------------------------------------------------------------------> 

    <apex:form >
        <apex:pageBlock mode="maindetail">

            <table width="100%">
                <tr>
                    <td style="text-align:center">
                        <!-- Button... Edit (switch to Edit Mode) -->
                        <apex:commandButton value="Edit" action="{!editBuds}" 
                                            id="editButton" />
                    </td>
                </tr>
            </table>
            
            <!-- Table displaying the Annual Forecast related records -->
            <apex:pageBlockTable value="{!budgets}" var="row" id="budgetTbl" >
            
                <apex:column headerValue="Year">
                    <apex:outputText id="Year" value="{!row.Year__c}" style="width:40px" />
                </apex:column>
 
                 <!-- Currency fields from Budget (formatted as number without currency) -->               
                <apex:column headerValue="Jan" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Jan" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Jan__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Feb" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Feb" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Feb__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Mar" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Mar" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Mar__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Apr" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Apr" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Apr__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="May" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="May" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.May__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Jun" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Jun" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Jun__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Jul" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Jul" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Jul__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Aug" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Aug" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Aug__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Sep" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Sep" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Sep__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Oct" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Oct" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Oct__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Nov" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Nov" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Nov__c}"/>
                    </apex:outputText>
                </apex:column>
                <apex:column headerValue="Dec" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:outputText id="Dec" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Dec__c}"/>
                    </apex:outputText>
                </apex:column>
                
                <!-- Total (sum) for the year -->
                <apex:column headerValue="Total" style="text-align:right" 
                             headerClass="CurrencyElement" >
                    <apex:outputText id="yearTotal" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Year_Total__c}"/>
                    </apex:outputText>
               </apex:column>

            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>

</apex:page>

Edit VF page (BudgetEdit.vfp):
<apex:page id="thepage" standardController="Contact" extensions="BudgetExt">
<!------------------------------------------------------------------------ 
    This page is designed to fit as a component on the detail page of 
    the Contact page layout.  This is the Edit Mode version of the page, 
    containing input fields and buttons for Add Row, Save, Cancel and Delete.
    
    Note that the currency ISO code doesn't appear and is assumed to be 
    carried through from the parent record.
     
    The related View Mode page is called "BudgetView".
     
    Author: Charles Thompson, Salesforce Singapore, Nov 2017 
--------------------------------------------------------------------------> 
    <apex:form>
        <apex:pageBlock mode="maindetail">

            <table width="100%">
                <tr>
                    <td style="text-align:center">
                        <!-- Buttons... Add Row / Save / Cancel -->
                        <apex:commandButton value="Add Row" action="{!addRow}" 
                                            id="addButton" />
                        <apex:commandButton value="Save" action="{!saveBuds}" 
                                            id="saveButton" />
                        <apex:commandButton value="Cancel" action="{!cancelEdit}" 
                                            id="cancelButton" />
                    </td>
                </tr>
            </table>
            
            <!-- Table displaying the related records -->
            <apex:pageBlockTable value="{!budgets}" var="row" id="budgets" >

                <!-- Counter variable to store an index value for each line -->
                <apex:variable var="i" value="{!0}" />
                <!-- Delete button -->
                <apex:column >
                    <apex:commandButton value="X" style="color: red" 
                                        action="{!delRow}" reRender="budgets"
                                        immediate="true" >
                        <!-- populate Apex index variable -->
                        <apex:param name="index" value="{!i}" assignTo="{!index}"/> 
                    </apex:commandButton>
                    <apex:variable var="i" value="{!i + 1}" />
                </apex:column>
                <apex:column headerValue="Year" 
                             title="Enter the forecast by month for this year and next year">
                    <apex:inputField value="{!row.Year__c}" style="width:60px"  />
                </apex:column>
 
                 <!-- Currency fields from Annual Forecast (the currency indicator doesn't appear) -->               
                <apex:column headerValue="Jan" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Jan__c}" 
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Feb" style="text-align:right" 
                             headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Feb__c}" 
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Mar" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Mar__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Apr" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Apr__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="May" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.May__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Jun" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Jun__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Jul" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Jul__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Aug" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Aug__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Sep" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Sep__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Oct" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Oct__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Nov" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Nov__c}"  
                                     style="text-align:right; width:65px" />
                </apex:column>
                <apex:column headerValue="Dec" style="text-align:right"  
                                     headerClass="CurrencyElement">
                    <apex:inputField value="{!row.Dec__c}"  
                                     style="text-align:right; width:65px" />
               </apex:column>
                <!-- Total (sum) for the year -->
                <apex:column headerValue="Total" style="text-align:right" 
                             headerClass="CurrencyElement" >
                    <apex:outputText id="yearTotal" value="{0, number, ###,###,##0}" >
                        <apex:param value="{!row.Year_Total__c}"/>
                    </apex:outputText>
               </apex:column>

            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>

</apex:page>

I hope this helps someone in the future, even if Lightning Experience makes this solution somewhat useless.
 
Charles ThompsonCharles Thompson
I used this same technique to get past a limitation on the detail page layout renderer:  Detail page sections only work well with one or two columns.
In that case, I wanted four fields side-by-side on a row.  Instead of using a pageBlockTable, I used a pageBlockSection to display the fields as I needed.  Otherwise, the same View/Edit mode logic works fine.
Charles ThompsonCharles Thompson
The Budget custom object is available in my Bitbucket repository (https://bitbucket.org/charles-thompson/contact-budget-sample-vf-apex/raw/94032fe175ac6d009894bb77c7b4663b4df57892/src/objects/Budget__c.object" target="_blank).  Feel free to explore the metadata and import it into your Force.com IDE project.
Charles ThompsonCharles Thompson

Update:  I encountered a further use case.  The code above will only refresh the VF component within the parent page layout.  What if you have rollup summary fields or formulas on the parent record?  They won't be recalculated until you refresh the whole page.

In this case, I have added a couple more items to the code.
VF View page:  no changes required
VF Edit page:  The code for the Save button needs to change (you only need to refresh the whole page when you save the child records).  The apex:commandButton doesn't change, but we need to add a new object to the page that will execute a short Javascript function.  New code:

                        <apex:commandButton value="Save" action="{!saveBuds}" 
                                            id="saveButton" />

                        <!-- on save, rerender the whole page -->
                        <apex:outputPanel rendered="{!refreshPage}" >
                            <script>
                                window.top.location='/{!opportunity.Id}';
                            </script>
                        </apex:outputPanel>

Apex controller extension:  The save method needs to return a pagereference as follows (add this to the end of the saveBuds() method):

        // switch back to View mode and refresh the whole page
        refreshPage = true;
        sc.save(); // save the Contact page to ensure formulas are updated
        PageReference fullPage = new PageReference('/' + contactID);
        return fullPage;

Kudos for this to Bob Buzzard Blog from May 2011:
https://bobbuzzard.blogspot.sg/2011/05/refreshing-record-detail-from-embedded.html#comment-form

One last thing:  I have tested this behaviour in Lightning and it works.  Although the embedded VF pages aren't implemented as Lightning components, nor do they follow the Lightning Design System (SLDS), they still appear the same in either Classic or Lightning Experience.