Take the 2-minute tour ×
Salesforce Stack Exchange is a question and answer site for Salesforce administrators, implementation experts, developers and anybody in-between. It's 100% free, no registration required.

I'm troubleshooting a custom grid editing VF page that was built by someone else. I'm getting conflicting results when I look at View State vs. the system log. Also I'm wondering what commandLink and commandButton do behind the scenes.

The grid displays custom related records to a Case record. There is a delete commandLink on each row. Clicking the delete link does not immediately delete the record - you need to click a Save commandButton to commit all changes and deletions. The Save button is a pageBlockButton.

I have found that if there are 3 or more rows in the grid and I delete one row at or near the top and click Save to commit the changes, one field in all the rows below the deletion is updated with wrong information. However, if I don't click Save, the data looks correct.

The field is Product and it is a lookup field. Other fields in the row are not affected.

Example:
Step one:
Row 1, Product is A
Row 2, Product is B
Row 3, Product is C
Row 4, Product is D
Row 5, Product is E

Step two:
I delete Row 2 BUT I DO NOT CLICK SAVE. The screen refreshes and I see, correctly:
Row 1, Product is A
Row 2, Product is C
Row 3, Product is D
Row 4, Product is E

Step three:
Now I click Save and the screen refreshes:
Row 1, Product is A
Row 2, Product is C
Row 3, Product is C
Row 4, Product is D

There is only one apex:form on the page. The controller is maintaining two lists - one of rows to display, and one of rows for DML deletion.

Here are things I don't understand:

  • In step 2, the view state show 5 items in the related record list, even though the system log shows only 4 items. The system log show each item has the correct Product__c. Why isn't the view state updated also, especially since I saw the page refresh?

  • Why is the delete commandLink rerendering the screen in step 2? Is that typical operation for commandLink? The way it is built, it is calling a public void method in the controller, not a public PageReference, so there is no return statement in it. I have not been able to find any explicit rerender in the controller method.

  • I have created a dummy Save button that does nothing. When I click the dummy button immediately after clicking the delete commandLink, I see in the system log that the Product__c are incorrect as in step 3 above. I can't understand how that happens, since I see the correct values on the screen just before I click the dummy button, and the system log confirms it.

The code is a monster. Here's the grid portion of the Visualforce page:

            <!-- This is a row counter used as a List index. -->
            <apex:variable value="{!0}" var="rowNbr" />
            <apex:pageBlockTable title="Defective Products" value="{!caseProducts}" var="caseProduct" id="caseProductsPageBlockTable" >
                <apex:column >
                    <apex:commandLink action="{!deleteCaseProductLink}" immediate="true" value="Delete" onclick="return confirmProductDelete()">
                        <apex:param name="{!ROW_NBR}" value="{!rowNbr}" />
                    </apex:commandLink>
                </apex:column>

                <apex:column headerValue="Row" width="60px">
                    <apex:outputText value="{!rowNbr}" style="width:60px"/>
                </apex:column>

                <apex:column headerValue="Product ID" width="60px">
                    <apex:outputField value="{!caseProduct.Product__c}" style="width:60px"/>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Document_Number__c.label}" width="60px" rendered="{!OR(caseRecordTypeName = RECORD_TYPE_RETURNS, caseRecordTypeName = RECORD_TYPE_LOGISTICS)}">
                    <apex:inputField id="caseProductDocumentNumber" value="{!caseProduct.Document_Number__c}" style="width:60px"/>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Product__c.label}" width="60px" >
                    <apex:inputField value="{!caseProduct.Product__c}" style="width:60px" id="caseProduct">
                        <apex:actionSupport event="onchange"
                            immediate="true" 
                            action="{!rerenderCaseProductDescription}" 
                            rerender="productOwnedPageBlockSectionDD"
                            focus="{!IF(OR(caseRecordTypeName = RECORD_TYPE_RETURNS, caseRecordTypeName = RECORD_TYPE_LOGISTICS), 'caseProductGrillPurchasedStore', 'caseProductQuantity')}">
                            <apex:param name="{!ROW_NBR}" value="{!rowNbr}" />
                        </apex:actionSupport>
                    </apex:inputField>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Product_Description__c.label}" >
                    <apex:outputText value="{!caseProduct.Product_Description__c}"/>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Grill_Purchased_Store__c.label}" width="100px" rendered="{!OR(caseRecordTypeName = RECORD_TYPE_RETURNS, caseRecordTypeName = RECORD_TYPE_LOGISTICS)}">
                    <apex:inputField id="caseProductGrillPurchasedStore" value="{!caseProduct.Grill_Purchased_Store__c}" style="width:100px">
                        <apex:actionSupport event="onchange"
                            immediate="true" 
                            action="{!rerenderGrillPurchasedStore}" 
                            rerender="productOwnedPageBlockSectionDD"
                            focus="caseProductQuantity">
                            <apex:param name="{!ROW_NBR}" value="{!rowNbr}" />
                        </apex:actionSupport>
                    </apex:inputField>
                </apex:column>

                <apex:column headerValue="Qty" width="20px" >
                    <apex:inputField id="caseProductQuantity" value="{!caseProduct.Quantity__c}" style="width:20px"/>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Return_Store__c.label}" width="100px" rendered="{!(caseRecordTypeName = RECORD_TYPE_RETURNS)}">
                    <apex:inputField id="caseProductReturnStore" value="{!caseProduct.Return_Store__c}" style="width:100px">
                        <apex:actionSupport event="onchange"
                            immediate="true" 
                            action="{!rerenderReturnStore}" 
                            rerender="productOwnedPageBlockSectionDD"
                            focus="caseProductSerial">
                            <apex:param name="{!ROW_NBR}" value="{!rowNbr}" />
                        </apex:actionSupport>
                    </apex:inputField>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Model__c.label}" width="60px" rendered="{!caseRecordTypeName = RECORD_TYPE_DEALER_DISTRIBUTOR}">
                    <apex:inputField id="caseProductModel" value="{!caseProduct.Model__c}" style="width:60px"/>
                </apex:column>

                <apex:column headerValue="{!$ObjectType.CaseProduct__c.fields.Serial__c.label} (or not required)" width="90px">
                    <apex:inputField id="caseProductSerial" value="{!caseProduct.Serial__c}" style="width:90px"/>
                    <apex:inputField id="caseProductSerialNotRequired" value="{!caseProduct.Serial_Not_Required__c}"/>
                    <!-- Row counter incremented here so it is visible to all record types. -->
                    <apex:variable var="rowNbr" value="{!rowNbr + 1}" />
                </apex:column>

            </apex:pageBlockTable>

        </apex:pageBlockSection>

And here are portions of the controller. First, the properties and the constructor:

public without sharing class CaseProductController {

    //temporary code for troubleshooting the item overwrite issue after a delete.
    //2/8/2013
    public void saveTEST() {
        system.debug('*** saveTEST-caseProducts: ' + caseProducts);
//      return null;        
    }

    public static String RECORD_TYPE_DEALER_DISTRIBUTOR {get{return 'Dealer/Distributor';} private set;}
    public static String RECORD_TYPE_RESIDENTIAL {get{return 'Residential';} private set;}
    public static String RECORD_TYPE_RETURNS {get{return 'Returns';} private set;}
    public static String RECORD_TYPE_LOGISTICS {get{return 'Logistics';} private set;}
    public static String ROW_NBR {get{return 'rowNbr';} private set;}

    // 'case' is a researved word so I used caseMR 'MR' stands for 'Master Record'.
    public Case caseMR {get; private set;}
    public List<CaseProduct__c> caseProducts {get; private set;}
    public List<CaseReplacementProduct__c> caseReplacementProducts {get; private set;}
    public CaseComment caseComment {get; private set;}

    public String caseRecordTypeName {get; private set;}
    public Product_Owned__c productOwned {get; private set;}
    public Decimal subTotal {get; private set;}
    public Decimal grandTotal {get; private set;}

    private List<CaseProduct__c> caseProductsToDelete;
    private List<CaseReplacementProduct__c> caseReplacementProductsToDelete;


    // Constructor
    public CaseProductController(ApexPages.StandardController stdController) {
        this.caseMR = (Case)stdController.getRecord();
        System.debug('MikeP-CaseProductController()-caseMR=' + caseMR);

        // Check if we have a Case record
        if (this.caseMR.Id == null) {
            // Must be a NEW Case

            // Setup the new CaseProduct__c record
            initCaseProductRow();
            // Setup the new CaseReplacementProduct__c record
            initCaseReplacementProductRow();
            // Setup the CaseComment record
            initCaseCommentRow();

            setContact();

        } else {
            // Must be an EXISTING Case

            // Select the Case record with all the fields we need to edit
            selectCaseMR(this.caseMR.Id);
            // select the related CaseProduct__c Records
            selectCaseProducts(this.caseMR.Id);
            // select the related CaseReplacementProduct__c Records
            selectCaseReplacementProducts(this.caseMR.Id);
            // select the related CaseComment Record
            selectCaseComment(this.caseMR.Id);

        }

        setProductOwned(this.caseMR.Product_Owned_ID__c);

        System.debug('MikeP-CaseProductController()-this.caseMR=' + this.caseMR);
        System.debug('MikeP-CaseProductController()-this.caseProducts=' + this.caseProducts);
        System.debug('MikeP-CaseProductController()-this.caseReplacementProducts=' + this.caseReplacementProducts);
        System.debug('MikeP-CaseProductController()-this.caseComment=' + this.caseComment);

        selectCaseRecordTypeName();
    }
}

And here is the delete commandLink method.

public void deleteCaseProductLink() {
system.debug('*** deleteCaseProductLink-caseProducts before: ' + caseProducts);
        System.debug('MikeP-deleteCaseProductLink()-this.caseProducts.size()=' + this.caseProducts.size());
        Integer rowNbr = Decimal.valueOf(Apexpages.currentPage().getParameters().get(ROW_NBR)).intValue();
        System.debug('MikeP-deleteCaseProductLink()-rowNbr=' + rowNbr);
        if (rowNbr != null) {
            if (this.caseProductsToDelete == null) {
                this.caseProductsToDelete = new List<CaseProduct__c>();
            }
            this.caseProductsToDelete.add(this.caseProducts[rowNbr]);
            this.caseProducts.remove(rowNbr);
        }
system.debug('*** deleteCaseProductLink-caseProducts after: ' + caseProducts);
    }
share|improve this question
    
This is going to be hard to debug w/o any code and it seems like you have at least a couple of questions in there. For your second bulletpoint I can tell you that calling a void method is the same as calling a pageReference method that returns null which re-renders the current page. –  Greg Grinberg Feb 9 '13 at 2:20
    
Thanks for your reply. I added some code to the question. –  David Feb 9 '13 at 7:14
add comment

1 Answer

After several hours of removing code sections and testing, I think I found the issue -- when I removed the immediate="true" from the delete commandLink, the problem went away. Makes me curious as to what exactly "immediate" is doing in addition to avoiding field validation.

share|improve this answer
    
Take a look at this thread on the developer boards. Immediate=true bypasses both validation and the set-value phase. It is best used for things like Cancel buttons. –  Daniel Hoechst Feb 11 '13 at 16:23
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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