This is probably impossible without using a controller extension (nor a custom controller). I was able to accomplish this with a relatively simple Visualforce page and controller extension.
For the examples I'll be giving, I'm working with the OpportunityLineItem
object. This is because my org currently has something like 10 different fieldsets on this object, making it easy to fiddle with building an example page/controller extension. The fieldsets I'll be referencing are
- Enhanced_Services_Enhanced_Services_Shar
- Network_Internet_Internet_Bandwidth
- Enhanced_Services_Internet_QoS
- Managed_Services_Collocation_Power
- Network_Ethernet_EVC
I was able to get somewhat close to accomplishing this with no code outside of the Visualforce page by using the $objectType global variable with a dynamic reference like this:
<apex:repeat value="{!$ObjectType.OpportunityLineItem.Fieldsets['Network_Ethernet_EVC']}" var="f2">
<apex:inputField value="{!OpportunityLineItem[f2]}" />
</apex:repeat>
I ended up running into two issues with this approach.
- I couldn't find a way to dynamically generate the name of the fieldset
- If we could do that, the dynamic call
{!$ObjectType.OpportunityLineItem.Fieldsets['Network_Ethernet_EVC']}
is treated differently than the equivalent static call {!$ObjectType.OpportunityLineItem.Fieldsets.Network_Ethernet_EVC}
is treated.
- This results in an
SObject row was retrieved via SOQL without querying the requested field
error
Now, on to the example using a controller extension that I got to work
Controller Extension:
public class OLIControllerExtensionTest{
private ApexPages.StandardController con;
public OpportunityLineItem oli {public get; private set;}
// This map explicitly maps values of some arbitrary field to names of fieldsets
private static Map<String, String> prodTypeToFieldset = new Map<String, String>{
'1' => 'Enhanced_Services_Enhanced_Services_Shar',
'2' => 'Network_Internet_Internet_Bandwidth',
'3' => 'Enhanced_Services_Internet_QoS',
'4' => 'Managed_Services_Collocation_Power',
'5' => 'Network_Ethernet_EVC'
};
// Just grab all the fieldsets in one go.
private static Map<String, Schema.Fieldset> fieldsets = Schema.SObjectType.OpportunityLineItem.fieldSets.getMap();
public OLIControllerExtensionTest(ApexPages.StandardController controller){
this.con = controller;
// This is a fairly simple way of querying EVERY field on an sObject.
// We only need the fields used in the fieldsets, but that would take
// more lines of code.
String query = 'SELECT ' + String.join(new List<String>(Schema.SObjectType.OpportunityLineItem.fields.getMap().keySet()), ', ') + ' FROM OpportunityLineItem WHERE Id = \'' + controller.getId() + '\'';
List<OpportunityLineItem> oliList = (List<OpportunityLineItem>)database.query(query);
this.oli = oliList[0];
}
public List<Schema.FieldsetMember> getFieldset(){
// Use the 'prototype' object from the controller.
// If we don't, we'd be stuck with the initial value of prodType__c, and calling this method again (which happens during the re-render)
// would never return anything different (than the fieldset that was returned when the page was first rendered)
OpportunityLineItem tempOLI = (OpportunityLineItem)(this.con.getRecord());
if(tempOLI == null || tempOLI.prodType__c == null){
return OLIControllerExtensionTest.fieldsets.get('Network_Internet_IP_Addresses').getFields();
}
return OLIControllerExtensionTest.fieldsets.get(OLIControllerExtensionTest.prodTypeToFieldset.get(tempOLI.prodType__c)).getFields();
}
public String getCurrentValue(){
return String.valueOf(this.con.getRecord().get('prodType__c'));
}
}
Visualforce Page:
<apex:page standardController="OpportunityLineItem" showHeader="true" standardstylesheets="true" extensions="OLIControllerExtensionTest" title="Opportunity Product: {!opportunityLineItem.PricebookEntry.Product2.Name}">
<apex:form>
<apex:pageBlock>
<apex:pageBlockSection>
<apex:inputField value="{!OpportunityLineItem.prodType__c}">
<apex:actionSupport event="onchange" rerender="dynamicFields"/>
</apex:inputField>
</apex:pageBlockSection>
<apex:pageBlockSection id="dynamicFields">
<apex:inputField value="{!oli['Id']}" />
<apex:repeat value="{!Fieldset}" var="f">
<apex:inputField value="{!oli[f]}" />
</apex:repeat>
<apex:outputText label="Current Value" value="{!currentValue}" />
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
The important things in the page and the controller extension that make everything work are the following:
The <apex:actionSupport>
, placed in-between <apex:inputField></apex:inputField>
.
- This allows us to re-render a block, as
<apex:inputField>
doesn't have a rerender
attribute on its own, and a partial re-rendering of the page is required to have a change to a value on the page affect the layout of the page. Re-rendering the 'dynamicFields'block will end up calling the controller extension's getFieldset()
method again.
In the controller extension's getFieldset()
method, using the object instance returned by the getRecord()
method of the standard controller.
The apex StandardController
(and StandardSetController
as well) maintain a 'prototype object' in memory, which is accessible through the getRecord()
method of the controller.
For input fields on a Visualforce page, changes to those fields are stored in the prototype object until the save()
method is called. When Save()
is called, Salesforce uses the prototype object to update the record(s) initially passed to the controller.
Before the record is saved, the only way to access the values the user has input is through that prototype object. You can (and I have, in the past) maintain your own 'prototype' object in a controller/extension to achieve the same thing. Doing this would be helpful if you wanted to apply this to a Visualforce page that could create a new record (instead of only updating an existing record) but, for this question, this would just add unnecessary code.
Using {!oli[f]}
for the fields being dynamically displayed based on a fieldset
The standard controller only pulls in fields that can be determined statically at compilation. When dynamically choosing fieldsets (like we're doing here), you need to explicitly query all the fields that you can possibly reference. The controller extension does this, and stores the result in the oli
variable. If we didn't do this, you'd run into the SObject row was retrieved via SOQL without querying the requested field
error.
This could be worked around by using the standard controller's addFields()
method, but addFields()
causes serious issues when writing unit tests (so I avoid using it)
If you are using a static reference to a fieldset in an <apex:repeat>
like <apex:repeat value="{!$ObjectType.OpportunityLineItem.Fieldsets.Network_Ethernet_EVC}" var="f">
, Salesforce can statically determine what those fields are. In this case, using <apex:inputField value="{!OpportunityLineItem[f]}"/>
would work just fine.
Explicitly making the map from field values to fieldset names
- This gives the solution flexibility. Set the keys to the
StageName
values instead, and you can use that field to dynamically choose a fieldset
- The weakness here though is that the map is hard-coded, any changes would require a deployment. There's probably a way to use a Custom Setting or Custom Metadata type to store this information, but I don't have the time to investigate that
checkStage()
method (Javascript) being calledonUpdate
of your Stage input field? I'd imagine both of these would be very helpful for someone looking to answer your question. Also, is making a controller extension not an option?apex:repeat
, each rendered based on the value ofstagename
? use onchange on stageName to rerender the pageBlock