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);
}