function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
JPSeaburyJPSeabury 

Stumped on building wrapper class

I'm building a custom Visualforce PDF report that shows all the Opportunity Chatter that happened in the past week. I'd like to have the report broken out something similar to:

  • Account 1
    • Oppty 1
      • Oppty Chatter Post 1a
      • Oppty Chatter Post 1b
    • Oppty 2
      • Oppty Chatter Post 2a
      • Oppty Chatter Post 2b
      • Oppty Chatter Post 2c
  • Account 2
    • Oppty 3
      • Oppty Chatter Post 3a
      • Oppty Chatter post 3b

My current code is able to retrieve the Opportunity Chatter Posts easily enough -- but I'm stumped on figuring out how to modify it so that the chatter posts are grouped by Account, as in the example above.

Here's my current VF page:
 

<apex:page controller="ReportOpptyChatter" renderAs="PDF" showHeader="true" sidebar="false">

    <!-- Summary List of all accounts that had Opportunity Chatter this Week -->
    <table>
        <tr><td>Opportunity Chatter found for the following accounts:</td></tr>
        <tr><td>
            <apex:repeat value="{!Accounts}" var="a" >
                <li><apex:outputText value=" {!a.Name}" /> </li>
            </apex:repeat>
        </td></tr>
    </table>
    <p></p>
    
    <!-- Opportunity Chatter -->
    <table>
       <apex:repeat value="{!ChatterUpdate}" var="update" >
       <tr valign="top"><td><b>Project:</b></td><td><apex:outputField value="{!update.ParentId}" /></td><td align="right"><i>{!update.CreatedBy.Firstname} {!update.CreatedBy.Lastname}</i></td></tr>
       <tr valign="top"><td></td><td colspan="2"><apex:outputText value="{!update.Body}" escape="false" /></td></tr>
       <tr><td></td></tr>
       </apex:repeat>
    </table>
</apex:page>


Here's my current controller:

public class ReportOpptyChatter {

    // List of Opportunity Chatter Posts from Last 7 Days    
    List<OpportunityFeed> opptyChatter= [SELECT Id, Body, ParentId, CreatedDate, CreatedBy.FirstName, CreatedBy.LastName
                                         FROM   OpportunityFeed
                                         WHERE  CreatedDate = LAST_N_DAYS:7
                                         ORDER BY CreatedDate];
    
    // Parent accounts of Oppty Chatter
    public List<Account> getAccounts() {
        Set<Id> opptyIdSet = new Set<Id>();   // Which Opportunities had Chatter this past week?        
        Set<Id> acctIdSet = new Set<Id>();    // Which accounts are those opptys related to?
        
        // Interate through the Oppty Chatter to find a list of unique Opptys
        for (OpportunityFeed i : opptyChatter) {
            opptyIdSet.add(i.ParentId);
        }
        List<Opportunity> opptyList = [SELECT Id, Name, AccountId FROM Opportunity WHERE Id IN :opptyIdSet];
        
        // Itegrate through the Opptys to get a list of unique Accounts
        for (Opportunity o : opptyList) {
            acctIdSet.add(o.AccountId);
        }
        List<Account> accountList = [SELECT Id, Name FROM Account WHERE Id IN :acctIdSet ORDER BY Name];
        
        return accountList;
    }
    
    public List<OpportunityFeed> getChatterUpdate() {
        return opptyChatter;
    }

                               
}

I suspect I probably need a wrapper class, so my VF page and modify the VF page to use nested <apex:repeats> but am unsure how to tackle that.
Best Answer chosen by JPSeabury
Boss CoffeeBoss Coffee
Here's an example of how it'd work with Accounts, Opportunities, and Opportunity Feeds. Let me know if you need any more assistance!
 
public static void setUpWrappers() {
        // Map of Opportunity Chatter Posts from Last 7 Days    
        Map<Id,OpportunityFeed> opptyChatter= new Map<Id,OpportunityFeed>([
            SELECT Id, Body, ParentId, CreatedDate, CreatedBy.FirstName, CreatedBy.LastName
            FROM   OpportunityFeed
            WHERE  CreatedDate = LAST_N_DAYS:7
            ORDER BY CreatedDate
        ]);
        
        // Fetch a map of the relevant Opportunities
        Map<Id,Opportunity> opportunityMap = new Map<Id,Opportunity>([
            SELECT Id, Name, AccountId, Account.Name 
            FROM Opportunity 
            WHERE Id IN (SELECT ParentId FROM OpportunityFeed WHERE Id IN :opptyChatter.keySet())
        ]);
        
        // Finally, fetch a map of the relevant Accounts
        Map<Id,Account> accountMap = new Map<Id,Account>([
            SELECT Id, Name
            FROM Account
            WHERE Id IN (SELECT AccountId FROM Opportunity WHERE Id IN :opportunityMap.keySet())
        ]);
        
        // What we want to do is first group these chatter posts by Opportunity
        // So we'll create a map as the following -- key: Opportunity Id -> value: Opportunity Wrapper (which holds the list of respective opportunity chatter posts)
        Map<Id,OpportunityWrapper> oppIdToPosts = new Map<Id,OpportunityWrapper>();
        // Now populate the oppIdToPosts map
        OpportunityWrapper tempOppWrapper;
        for(OpportunityFeed oppChat : opptyChatter.values()) {
            // We'll see if there's an entry in the map for this Opportunity Id
            tempOppWrapper = oppIdToPosts.get(oppChat.ParentId);
            if(tempOppWrapper != null) {
                // If we've already made an entry under this Opportunity Id, then we'll just add to the list of posts
                tempOppWrapper.chatPosts.add(oppChat);
                oppIdToPosts.put(oppChat.ParentId, tempOppWrapper);
            } else {
                // Otherwise, we need to make an initial entry
                tempOppWrapper = new OpportunityWrapper();
                tempOppWrapper.oppRecord = opportunityMap.get(oppChat.ParentId);
                tempOppWrapper.chatPosts = new List<OpportunityFeed>();
                tempOppWrapper.chatPosts.add(oppChat);
                oppIdToPosts.put(oppChat.ParentId, tempOppWrapper);
            }
        }
        
        // In a similar manner, we'll group these Opportunity Wrappers by Account
        Map<Id,AccountWrapper> accIdToOpps = new Map<Id,AccountWrapper>();
        AccountWrapper tempAccWrapper;
        // This time we'll loop through the values of the map we created earlier
        for(OpportunityWrapper oppWrap : oppIdToPosts.values()) {
            // We'll see if there's an entry in the map for this Account Id
            tempAccWrapper = accIdToOpps.get(oppWrap.oppRecord.AccountId);
            if(tempAccWrapper != null) {
                // If we've already made an entry under this Account Id, then we'll just add to the list of opportunity wrappers
                tempAccWrapper.oppWraps.add(oppWrap);
                accIdToOpps.put(oppWrap.oppRecord.AccountId, tempAccWrapper);
            } else {
                // Otherwise, we need to make an initial entry
                tempAccWrapper = new AccountWrapper();
                tempAccWrapper.accRecord = accountMap.get(oppWrap.oppRecord.AccountId);
                tempAccWrapper.oppWraps = new List<OpportunityWrapper>();
                tempAccWrapper.oppWraps.add(oppWrap);
                accIdToOpps.put(oppWrap.oppRecord.AccountId, tempAccWrapper);
            }
        }
        
        // The final map "accIdToOpps" should be finished
        // You can iterate through each AccountWrapper by accessing the values via accIdToOpps.values()
        // Each wrapper will hold the Account record itself, which will hold desired fields via the query made earlier
        // They will also hold a list of OpportunityWrappers, which will hold information about their respective Opportunity and Opportunity Chatter Posts
        
        // Here's an example of looping through each level
        for(AccountWrapper accWrap : accIdToOpps.values()) {
            System.debug(accWrap.accRecord.Name);
            for(OpportunityWrapper innerOppWrap : accWrap.oppWraps) {
                System.debug('--- ' + innerOppWrap.oppRecord.Name);
                for(OpportunityFeed innerOppFeed : innerOppWrap.chatPosts) {
                    System.debug('--- ++ ' + innerOppFeed.Id + ': ' + innerOppFeed.CreatedBy.FirstName);
                }
            }
        }
    }
    
    // Wrapper for a single Opportunity that holds all respective opportunity chatter posts
    public class OpportunityWrapper {
        Opportunity oppRecord {get;set;}
        List<OpportunityFeed> chatPosts {get;set;}
    }
    
    // Wrapper for a single Account that holds all respective opportunities (as wrappers)
    public class AccountWrapper {
        Account accRecord {get;set;}
        List<OpportunityWrapper> oppWraps {get;set;}
    }

All Answers

Boss CoffeeBoss Coffee
Here's an example of how it'd work with Accounts, Opportunities, and Opportunity Feeds. Let me know if you need any more assistance!
 
public static void setUpWrappers() {
        // Map of Opportunity Chatter Posts from Last 7 Days    
        Map<Id,OpportunityFeed> opptyChatter= new Map<Id,OpportunityFeed>([
            SELECT Id, Body, ParentId, CreatedDate, CreatedBy.FirstName, CreatedBy.LastName
            FROM   OpportunityFeed
            WHERE  CreatedDate = LAST_N_DAYS:7
            ORDER BY CreatedDate
        ]);
        
        // Fetch a map of the relevant Opportunities
        Map<Id,Opportunity> opportunityMap = new Map<Id,Opportunity>([
            SELECT Id, Name, AccountId, Account.Name 
            FROM Opportunity 
            WHERE Id IN (SELECT ParentId FROM OpportunityFeed WHERE Id IN :opptyChatter.keySet())
        ]);
        
        // Finally, fetch a map of the relevant Accounts
        Map<Id,Account> accountMap = new Map<Id,Account>([
            SELECT Id, Name
            FROM Account
            WHERE Id IN (SELECT AccountId FROM Opportunity WHERE Id IN :opportunityMap.keySet())
        ]);
        
        // What we want to do is first group these chatter posts by Opportunity
        // So we'll create a map as the following -- key: Opportunity Id -> value: Opportunity Wrapper (which holds the list of respective opportunity chatter posts)
        Map<Id,OpportunityWrapper> oppIdToPosts = new Map<Id,OpportunityWrapper>();
        // Now populate the oppIdToPosts map
        OpportunityWrapper tempOppWrapper;
        for(OpportunityFeed oppChat : opptyChatter.values()) {
            // We'll see if there's an entry in the map for this Opportunity Id
            tempOppWrapper = oppIdToPosts.get(oppChat.ParentId);
            if(tempOppWrapper != null) {
                // If we've already made an entry under this Opportunity Id, then we'll just add to the list of posts
                tempOppWrapper.chatPosts.add(oppChat);
                oppIdToPosts.put(oppChat.ParentId, tempOppWrapper);
            } else {
                // Otherwise, we need to make an initial entry
                tempOppWrapper = new OpportunityWrapper();
                tempOppWrapper.oppRecord = opportunityMap.get(oppChat.ParentId);
                tempOppWrapper.chatPosts = new List<OpportunityFeed>();
                tempOppWrapper.chatPosts.add(oppChat);
                oppIdToPosts.put(oppChat.ParentId, tempOppWrapper);
            }
        }
        
        // In a similar manner, we'll group these Opportunity Wrappers by Account
        Map<Id,AccountWrapper> accIdToOpps = new Map<Id,AccountWrapper>();
        AccountWrapper tempAccWrapper;
        // This time we'll loop through the values of the map we created earlier
        for(OpportunityWrapper oppWrap : oppIdToPosts.values()) {
            // We'll see if there's an entry in the map for this Account Id
            tempAccWrapper = accIdToOpps.get(oppWrap.oppRecord.AccountId);
            if(tempAccWrapper != null) {
                // If we've already made an entry under this Account Id, then we'll just add to the list of opportunity wrappers
                tempAccWrapper.oppWraps.add(oppWrap);
                accIdToOpps.put(oppWrap.oppRecord.AccountId, tempAccWrapper);
            } else {
                // Otherwise, we need to make an initial entry
                tempAccWrapper = new AccountWrapper();
                tempAccWrapper.accRecord = accountMap.get(oppWrap.oppRecord.AccountId);
                tempAccWrapper.oppWraps = new List<OpportunityWrapper>();
                tempAccWrapper.oppWraps.add(oppWrap);
                accIdToOpps.put(oppWrap.oppRecord.AccountId, tempAccWrapper);
            }
        }
        
        // The final map "accIdToOpps" should be finished
        // You can iterate through each AccountWrapper by accessing the values via accIdToOpps.values()
        // Each wrapper will hold the Account record itself, which will hold desired fields via the query made earlier
        // They will also hold a list of OpportunityWrappers, which will hold information about their respective Opportunity and Opportunity Chatter Posts
        
        // Here's an example of looping through each level
        for(AccountWrapper accWrap : accIdToOpps.values()) {
            System.debug(accWrap.accRecord.Name);
            for(OpportunityWrapper innerOppWrap : accWrap.oppWraps) {
                System.debug('--- ' + innerOppWrap.oppRecord.Name);
                for(OpportunityFeed innerOppFeed : innerOppWrap.chatPosts) {
                    System.debug('--- ++ ' + innerOppFeed.Id + ': ' + innerOppFeed.CreatedBy.FirstName);
                }
            }
        }
    }
    
    // Wrapper for a single Opportunity that holds all respective opportunity chatter posts
    public class OpportunityWrapper {
        Opportunity oppRecord {get;set;}
        List<OpportunityFeed> chatPosts {get;set;}
    }
    
    // Wrapper for a single Account that holds all respective opportunities (as wrappers)
    public class AccountWrapper {
        Account accRecord {get;set;}
        List<OpportunityWrapper> oppWraps {get;set;}
    }
This was selected as the best answer
JPSeaburyJPSeabury
IMMENSELY HELPFUL, @Boss Coffee -- particularly your comments walking through the setup. I digested this quickly -- now I'm going to dive in to a sandbox and play with it some, but it definitely helps me understand the construct of the wrapper better. Thank you!
JPSeaburyJPSeabury

The Apex works brilliantly. I return a Map of AccountWrapper objects back to the Visualforce page. How do I "unwind" that to display the values of each map entry?  In this VF page, I'm just going for the Account Names in the outer map (start small, right?), but I'll eventually want to iterate through the inner map.

<apex:page controller="MapAccCont">
    <apex:form >
        <apex:repeat value="{!mapToAccount}" var="accNum">
            <apex:outputText value="{!accNum}" /> <br/> <br/>
            <apex:outputText value="{!mapToAccount[accNum]}" /> <br/> <br/>
            <!-- <apex:outputText value="{!accNum.accRecord.Name}" /> <br/> <br/> -->
        </apex:repeat>
    </apex:form>
</apex:page>

To give an idea of what the output (and the values of the map), this is the current output:

001P000001ThJxAIAV

AccountWrapper:[accRecord=Account:{Id=001P000001ThJxAIAV, Name=Stark Enterprises}, oppWraps=(OpportunityWrapper:[chatPosts=(OpportunityFeed:{Id=0D5P000000L4sxxKAB, Body=<p>This is a test Chatter Post for Acct: Stark Enterprises, Oppty: Ironman Prototype</p>, ParentId=006P0000009gJTtIAM, CreatedDate=2019-11-14 19:41:43, CreatedById=005f2000008ucvaAAA, IsRichText=true}), oppRecord=Opportunity:{Id=006P0000009gJTtIAM, Name=Ironman Prototype, AccountId=001P000001ThJxAIAV, RecordTypeId=012f20000006h63AAA}])]

001P000001ThLFUIA3

AccountWrapper:[accRecord=Account:{Id=001P000001ThLFUIA3, Name=Eagles}, oppWraps=(OpportunityWrapper:[chatPosts=(OpportunityFeed:{Id=0D5P000000L4t6uKAB, Body=<p>On a dark desert highway</p>, ParentId=006P0000009gKxGIAU, CreatedDate=2019-11-14 20:08:13, CreatedById=005f2000008ucvaAAA, IsRichText=true}), oppRecord=Opportunity:{Id=006P0000009gKxGIAU, Name=Hotel California, AccountId=001P000001ThLFUIA3, RecordTypeId=012f20000006h63AAA}], OpportunityWrapper:[chatPosts=(OpportunityFeed:{Id=0D5P000000L4t7iKAB, Body=<p>Well I'm runnin' down the road tryin' to loosen my load</p>, ParentId=006P0000009gL59IAE, CreatedDate=2019-11-14 20:10:06, CreatedById=005f2000008ucvaAAA, IsRichText=true}), oppRecord=Opportunity:{Id=006P0000009gL59IAE, Name=Take It Easy, AccountId=001P000001ThLFUIA3, RecordTypeId=012f20000006h63AAA}], OpportunityWrapper:[chatPosts=(OpportunityFeed:{Id=0D5P000000L4tA3KAJ, Body=<p>Look at us baby, up all night</p>, ParentId=006P0000009gLVwIAM, CreatedDate=2019-11-14 20:15:44, CreatedById=005f2000008ucvaAAA, IsRichText=true}), oppRecord=Opportunity:{Id=006P0000009gLVwIAM, Name=I Cant Tell You Why, AccountId=001P000001ThLFUIA3, RecordTypeId=012f20000006h63AAA}])]

Boss CoffeeBoss Coffee
Ah, you'll also need to make those variables public in the wrapper class so they can be accessible from VF.
    // Wrapper for a single Opportunity that holds all respective opportunity chatter posts
    public class OpportunityWrapper {
        public Opportunity oppRecord {get;set;}
        public List<OpportunityFeed> chatPosts {get;set;}
    }
    
    // Wrapper for a single Account that holds all respective opportunities (as wrappers)
    public class AccountWrapper {
        public Account accRecord {get;set;}
        public List<OpportunityWrapper> oppWraps {get;set;}
    }

Then the VFP will be something like this.
    <apex:form >
        <apex:repeat value="{!accWraps}" var="accNum">
            <apex:outputText value="{!accNum}" /> <br/> <br/>
            <apex:outputText value="{!accWraps[accNum].accRecord.Name}" /> <br/> <br/>
            
            <!-- Get the opportunity wrappers -->
            <apex:repeat value="{!accWraps[accNum].oppWraps}" var="innerOpps">
                <apex:outputText value="{!innerOpps.oppRecord.Name}" /><br/> <br/>
                <apex:outputText value="{!innerOpps}" /><br/> <br/>
                
                <!-- Get the posts belonging to opportunity -->
                <apex:repeat value="{!innerOpps.chatPosts}" var="innerPost">
                    <apex:outputText value="{!innerPost.CreatedDate}" />
                </apex:repeat>
            </apex:repeat>
            
        </apex:repeat>
    </apex:form>