I've gone with generating the components in Apex and hooking them into the page via an apex:dynamicComponent. This solves my view state problem: each unused extension point now consumes 30 bytes (an empty Component.Apex.OutputField
) instead of 550 bytes (for the inline nested components and more for a reference to the same markup in a component). Moving all the extension points to this pattern will reduce the view state use by about 9k - always a good thing.
But as some of the fields are editable, this known problem Field Values In Dynamic Component Not Retaining After Error remains. The "save" controller action is never reached i.e. the field type checking is done up front. But a JavaScript workaround seems to work.
So the page with the JavaScript workaround is:
...
<apex:commandButton
value="Save"
action="{!save}"
oncomplete="cvRestoreDynamicFields();"
/>
...
<apex:dynamicComponent componentValue="{! Medical_top_extension }"/>
...
<apex:dynamicComponent componentValue="{! Medical_middle_extension }"/>
...
<apex:dynamicComponent componentValue="{! Medical_bottom_extension }"/>
...
<script>
// Key is field id; value is field value
var cvDynamicFieldChanges = {};
// Track value changes of dynamic fields
function cvDynamicFieldOnChange(field) {
cvDynamicFieldChanges[field.id] = field.type === 'checkbox' ? field.checked : field.value;
}
// When a dynamic field has invalid syntax on e.g. a date the posted values are lost so restore them
function cvRestoreDynamicFields() {
for (var id in cvDynamicFieldChanges) {
if (cvDynamicFieldChanges.hasOwnProperty(id)) {
var field = document.getElementById(id);
var value = cvDynamicFieldChanges[id];
if (field.type === 'checkbox') field.checked = value;
else field.value = value;
}
}
cvDynamicFieldChanges = {};
}
</script>
using this controller (unless there is an answer to Any way to pass parameters to apex:dynamicComponent?):
...
public ApexPages.Component getMedical_top_extension() {
return extension.component('Medical', 'top');
}
public ApexPages.Component getMedical_middle_extension() {
return extension.component('Medical', 'middle');
}
public ApexPages.Component getMedical_bottom_extension() {
return extension.component('Medical', 'bottom');
}
...
and this dynamic component generation code (messy data structure references in that and used in the Visualforce markup):
...
// For use via apex:dynamicComponent: only create complex component graph when it will be used to save view state
public ApexPages.Component component(String sectionName, String positionName) {
if (extensionAvailable && extensionsRequiredMap.get(sectionName)) {
Extension e = extensionsMap.get(sectionName);
for (ExtraSection s : e.extraSections) {
if (s.position == positionName) {
// Add the components that iterate over the data model as they will be used
return createDynamicComponents(sectionName, positionName);
}
}
}
// apex:dynamicComponent handles null OK and zero view state used
return null;
}
//
// Dynamically created version of:
//
// <apex:repeat value="{! extension.extensionsMap['Medical'].extraSections }" var="s">
// <apex:pageBlockSection title="{! s.sectionTitle }" columns="2" rendered="{! s.position == 'top' }">
// <apex:repeat value="{! s.sectionExtraFields }" var="f">
// <apex:inputField value="{! f.sob[f.fieldName] }" rendered="{! !f.readOnly && !f.useLabel }"/>
// <apex:inputField value="{! f.sob[f.fieldName] }" label="{! f.label } " rendered="{! !f.readOnly && f.useLabel }"/>
// <apex:outputField value="{! f.sob[f.fieldName] }" rendered="{! f.readOnly && !f.useLabel }"/>
// <apex:outputField value="{! f.sob[f.fieldName] }" label="{ !f.label }" rendered="{! f.readOnly && f.useLabel }"/>
// </apex:repeat>
// </apex:pageBlockSection>
// </apex:repeat>
//
private ApexPages.Component createDynamicComponents(String sectionName, String positionName) {
// Root of data structure field from controller is called "extension"
Component.Apex.Repeat sectionRepeat = new Component.Apex.Repeat(var = 's');
sectionRepeat.expressions.value = '{! extension.extensionsMap[\'' + sectionName + '\'].extraSections }';
Component.Apex.PageBlockSection section = new Component.Apex.PageBlockSection(columns = 2);
section.expressions.title = '{! s.sectionTitle }';
section.expressions.rendered = '{! s.position == \'' + positionName + '\'}';
sectionRepeat.childComponents.add(section);
Component.Apex.Repeat fieldRepeat = new Component.Apex.Repeat(var = 'f');
fieldRepeat.expressions.value = '{! s.sectionExtraFields }';
section.childComponents.add(fieldRepeat);
Component.Apex.InputField f0 = new Component.Apex.InputField(onchange = 'cvDynamicFieldOnChange(this);');
f0.expressions.value = '{! f.sob[f.fieldName] }';
f0.expressions.rendered = '{! !f.readOnly && !f.useLabel }';
fieldRepeat.childComponents.add(f0);
Component.Apex.InputField f1 = new Component.Apex.InputField(onchange = 'cvDynamicFieldOnChange(this);');
f1.expressions.value = '{! f.sob[f.fieldName] }';
f1.expressions.rendered = '{! !f.readOnly && f.useLabel }';
f1.expressions.label = '{! f.label }';
fieldRepeat.childComponents.add(f1);
Component.Apex.OutputField f2 = new Component.Apex.OutputField();
f2.expressions.value = '{! f.sob[f.fieldName] }';
f2.expressions.rendered = '{! f.readOnly && !f.useLabel }';
fieldRepeat.childComponents.add(f2);
Component.Apex.OutputField f3 = new Component.Apex.OutputField();
f3.expressions.value = '{! f.sob[f.fieldName] }';
f3.expressions.rendered = '{! f.readOnly && f.useLabel }';
f3.expressions.label = '{! f.label }';
fieldRepeat.childComponents.add(f3);
return sectionRepeat;
}
...