Saturday 14 May 2011

Updating Attributes in Component Controller

A while ago I was working on a  component that would allow a user to select/input a number of data values, and return these as a semi-colon separated String to the controller of the parent page.

So I created my component, taking the String as an attribute and assigning it to a variable in the component's controller.

Component:

<apex:component controller="StringEntryController">
   <apex:attribute type="String" name="theString" description="The string to update" assignTo="{!stringVal}"/>
   <apex:outputLabel value="Select the string components"/>
   <apex:selectList value="{!chosenVals}" multiSelect="true" size="4">
     <apex:selectOption itemValue="UK" itemLabel="UK"/>
     <apex:selectOption itemValue="USA" itemLabel="USA"/>
     <apex:selectOption itemValue="Canada" itemLabel="Canada"/>
     <apex:selectOption itemValue="France" itemLabel="France"/>
     <apex:selectOption itemValue="Japan" itemLabel="Japan"/>
   </apex:selectList> 
</apex:component>

controller:


public class StringEntryController {
 public String stringVal {get; set;}
 public String[] chosenVals {get; 
                         set {
                                stringVal='';
                                for (String val : value)
                                {
                                 stringVal+=':' + val;
                                }
                           
                             stringVal=stringVal.substring(1);
                             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, ' Set string val to ' + stringVal));
                             }
                         }
 
 
}

I've added a message to the page to confirm that the String has been updated with the user's selections.

Here's the page that accesses the component.

<apex:page controller="PrimitiveCarrierController">
  <apex:form >
   <apex:pageBlock >
      <apex:pageMessages />
      <apex:pageBlockSection columns="1">
        <apex:pageBlockSectionItem >
        <apex:outputText value="The string = {!enteredString}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <c:StringEntry theString="{!enteredString}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <apex:commandButton value="Update"/>
        </apex:pageBlockSectionItem>
      </apex:pageBlockSection>
   </apex:pageBlock>
  </apex:form>
</apex:page>

And finally the page controller. Note that enteredString is initialised to 'This is the default from the controller' so that I can check that it has changed.

public class PrimitiveCarrierController 
{
 public String enteredString {get; set;}
 
 public PrimitiveCarrierController()
 {
  enteredString='This is the default from the controller';
 }
}

I then access the page, select some values from the component and click the Update button. The
output is as follows:


As you can see, while the value of the String has changed in the component, as is shown by the value in the page message, the value in the page's controller hasn't.

Reading the Apex Developer's Guide indicates that this is because Strings are passed by value rather than by reference. This means that the String that my component controller is working on is different to the one in the page controller, so it doesn't matter what my component controller does, the String from the page controller is unaffected.

The solution is to put the String inside an object, as these are passed by reference rather than by value, so changing the value of the contained String in the component controller affects the actual object instance that the page controller is managing.

Here is the class that contains the String:

public class StringCarrier {

    public String value {get; set;}
}

The revised component:

<apex:component controller="StringEntryControllerV1">
   <apex:attribute type="StringCarrier" name="theString" description="The string to update" assignTo="{!stringVal}"/>
   <apex:outputLabel value="Select the string components"/>
   <apex:selectList value="{!chosenVals}" multiSelect="true" size="4">
     <apex:selectOption itemValue="UK" itemLabel="UK"/>
     <apex:selectOption itemValue="USA" itemLabel="USA"/>
     <apex:selectOption itemValue="Canada" itemLabel="Canada"/>
     <apex:selectOption itemValue="France" itemLabel="France"/>
     <apex:selectOption itemValue="Japan" itemLabel="Japan"/>
   </apex:selectList> 
</apex:component>

The revised component controller:

public class StringEntryControllerV1 {
 public StringCarrier stringVal {get; set;}
 public String[] chosenVals {get; 
                         set {
                                stringVal.value='';
                                for (String val : value)
                                {
                                 stringVal.value+=':' + val;
                                }
                           
                             stringVal.value=stringVal.value.substring(1);
                             ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, ' Set string val to ' + stringVal.value));
                             }
                         }
 
 
}

The revised page.

<apex:page controller="PrimitiveCarrierControllerV1">
  <apex:form >
   <apex:pageBlock >
      <apex:pageMessages />
      <apex:pageBlockSection columns="1">
        <apex:pageBlockSectionItem >
        <apex:outputText value="The string = {!enteredString.value}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <c:StringEntryV1 theString="{!enteredString}"/>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
        <apex:commandButton value="Update"/>
        </apex:pageBlockSectionItem>
      </apex:pageBlockSection>
   </apex:pageBlock>
  </apex:form>
</apex:page>

And the revised page controller. Note that enteredString is initialised in the same way as before. Note also that the StringCarrier property must be requires instantiation unlike a String primitive.

public class PrimitiveCarrierControllerV1 
{
 public StringCarrier enteredString {get; set;}
 
 public PrimitiveCarrierControllerV1()
 {
  enteredString=new StringCarrier();
  enteredString.value='This is the default from the controller';
 }
}

And as is shown below, the changes applied by the component propagate to the page:

3 comments:

  1. Bob - thanks for this great article. I was having trouble getting this to work until I stumbled across this article and the hint that strings as passed by value not reference. Saved me hours. Pete

    ReplyDelete
  2. Quick change to the StringCarrier class that I added:

    public class StringCarrier {
    public String value {get; set;}
    public StringCarrier(string defaultValue) {
    value = defaultValue;
    }
    }

    Which changes the page controller:

    enteredString=new StringCarrier('This is the default from the controller');

    Nothing big, but I like optimizing code.

    This article was a huge help to me, thanks!

    ReplyDelete
  3. FYI, I have an Idea on IdeaExchange to change this:
    https://sites.secure.force.com/success/ideaView?id=08730000000bCnoAAE

    ReplyDelete