0

This is a continuation of another question with code pared down as much as possible. I'm trying to create a series of pageblocktables on a VF page, each displaying some number of empty rows which users will be able to populate. The user enters the number of rows to display in an apex:input component above each table. I was advised to use a wrapper class to do this in the other question, which I've tried to implement, but I'm fairly new to coding and I'm still not totally sure what I'm doing, to put it charitably.

My question is, can I bind the input component(s) to the numOlis property of oppWrapper, so that the number of rows for each table can be independently controlled? I've found that if I set the value of numOlis in the constructor, as below:

public oppWrapper(Opportunity oppRec){
    this.oppRec = oppRec;
    this.numOlis = 2;
}

I see the value reflected in the page.

enter image description here

But I've been unable to find a way to get any user-defined values from the inputs back to the controller before the list is built. Here is the code I've got at the minute, minus irrelevant lines:

VF

<apex:page title="Clone N Opportunity Line Items" controller="OpportunityCloneNCtrlr" sidebar="false" tabStyle="Opportunity" showHeader="true" docType="HTML-5.0">
    <apex:form id="N">
        <apex:pageBlock id="pb" mode="inlineEdit">
            <apex:pageBlockSection id="pbs" columns="1">
                <apex:repeat id="oppAndOlis" var="opp" value="{!oppsList}">
                    <apex:outputPanel id="thePanel" styleClass="panelStyle">
                        How many line items would you like to create? &nbsp;
                        <apex:input id="numOlisInput" style="width:20px" value="{!opp.numOlis}" onkeydown="if(event.keyCode==13){this.blur();}">
                            <apex:actionSupport event="onchange" action="{!buildOlisList}" reRender="olis[opp]"/>
                        </apex:input> &nbsp;
                    </apex:outputPanel>
                    <apex:pageBlockTable id="olis" value="{!allOliLists[opp]}" var="oli">
                        <apex:repeat value="{!oliLabelToAPIMap}" var="label">
                            <apex:column headerValue="{!label}">
                                <apex:outputField value="{!oli[oliLabelToAPIMap[label]]}">
                                    <apex:inlineEditSupport event="ondblclick"/>
                                </apex:outputField>
                            </apex:column>
                        </apex:repeat>
                    </apex:pageBlockTable>
                </apex:repeat>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>

Controller

public class OpportunityCloneNCtrlr {
    public List<Opportunity> clonesList {get; set;}
    public List<oppWrapper> oppsList {get; set;}

    public OpportunityLineItem oliPrototype {get; set;}
    public List<OpportunityLineItem> oliPrototypes {get; set;}

    public Map<String, Schema.sObjectField> oliFieldMap {get; set;}
    public Map<String, Schema.sObjectField> oliFieldMapHelper {get; set;}
    public Map<String, String> oliLabelToAPIMap {get; set;}

    public Set<String> oliFieldNames {get; set;}

    public Map<oppWrapper, List<OpportunityLineItem>> allOliLists {get; set;}

    //Constructor
    public OpportunityCloneNCtrlr(){

        oliFieldMap = new Map<String, Schema.sObjectField>();
        oliFieldMapHelper = new Map<String, Schema.sObjectField>();
        oliFieldMapHelper = Schema.sObjectType.OpportunityLineItem.fields.getMap();

        for(String s : oliFieldMapHelper.keySet()){
            if((!oliFieldMapHelper.get(s).getDescribe().isCalculated() && oliFieldMapHelper.get(s).getDescribe().isUpdateable()) || s == 'product2Id') oliFieldMap.put(s, oliFieldMapHelper.get(s));
            else continue;
        }

        oliLabelToAPIMap = new Map<String, String>();
        for(String fieldAPI : oliFieldMap.keySet()){
            if(!oliFieldsToRemove.contains(fieldAPI)){                
                oliLabelToAPIMap.put(
                    oliFieldMap.get(fieldAPI).getDescribe().getLabel(), 
                    fieldAPI
                );
            } else{
                continue;
            }
        }
        buildOlisList();
    }

    public void buildOlisList(){
        clonesList = 
            [SELECT 
                Id,
                Name, 
                Programme_Code__c,
                Start_Date__c,
                End_Date__c 
             FROM 
                Opportunity 
             WHERE 
                Related_Programmes__c = :rpId 
                AND Id != :crId];
        oppsList = new List<oppWrapper>();

        for(Opportunity o : clonesList){
            oppWrapper opp = new oppWrapper(o);
            oppsList.add(opp);
            System.debug(opp);
        }

        oliFieldNames = new Set<String>();

        for(String s : oliFieldMap.keySet()){
             if(!oliFieldMap.get(s).getDescribe().isCalculated() && oliFieldMap.get(s).getDescribe().isUpdateable()) oliFieldNames.add(s);
            else continue;
        }

        allOliLists = new Map<oppWrapper,List<OpportunityLineItem>>();

        Integer i;
        for(oppWrapper opp : oppsList){
            oliPrototype = new OpportunityLineItem();
            oliPrototype.OpportunityId = opp.oppRec.Id;
            oliPrototypes = new List<OpportunityLineItem>();

            for(i = 0; i < opp.numOlis; i++){
                oliPrototypes.add(oliPrototype);
            }
            allOliLists.put(opp,oliPrototypes);
        }
    }

    public class oppWrapper{
        public Integer numOlis {get; set;}
        public Opportunity oppRec {get; set;}
        public oppWrapper(Opportunity oppRec){
            this.oppRec = oppRec;
            this.numOlis = numOlis;
        }
    }
}
1

In summary, each variable name only holds one value at a time. This is relevant because of this line of code right here:

oppsList = new List<oppWrapper>();

What you're essentially saying is "discard any previous values and make a new list." In this case, the previous values were the user input.

Before this line of code, assuming the list was previously built, already had the data you needed from the input fields.

You'll want to write a separate method that reflects changing the value. It will start off looking like:

public void updateOliList() {
  for(OppWrapper item: oppList) {
    OpportunityLineItem[] lines = allOliLists.get(item);
    while(lines.size() > item.numOlis) { // List too big, get rid of extras
      lines.remove(lines.size()-1);
    }
    while(lines.size() < item.numOlis) { // List too small, add rows
      lines.add(new OpportunityLineItem(...));
    }
  }
}

Note, in programming, we have Objects and Variables. A single Object can be referenced by multiple Variables, but each Variable can only hold one Object. For example, your name might be Bob, but your kids call you Dad, your parents call you Son, your wife calls you... you get the idea. Multiple names, same person (I realize this isn't a perfect analogy, but I hope you get the idea).

In this case, your "prototype" list item is problematic, because of the same effect you had in your previous question; each item in the list is actually referring to the same Object, which means that when you edit one line, you're editing all of them.

Fortunately, sobjects can be "cloned" to make new copies, so this is what you should do to avoid this problem:

        for(i = 0; i < opp.numOlis; i++){
            oliPrototypes.add(oliPrototype.clone(false, false, false, false));
        }

You can read more about the clone method in the SObject class.

  • Hi sfdcfox, I've managed to get the code working at last, following your suggestions. Thanks for a clear and thorough explanation! – James Lamberth Nov 30 '18 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.