6

The view state problem we mainly run into is not properties of the controller using it up (as we use transient heavily) but the component tree that the Visualforce tags generate using it up. That is counted in the "Internal" total in the View State Tab. In Visualforce, as part of the HTML generation process and form post handling process, a server-side component tree is generated, and included in the view state.

The current problem is a page where we would like to add more extension points (for a total of about 30 across multiple tabs) based on a config data structure that is already in use by our customers e.g.:

<c:ExtensionPoint rendered="{! ... }" position="bottom" config="{! ... }"/>

but adding more exceeds the view state limit.

Only a small number of the extension points are ever used, but all of them contribute to the view state whether they are used or not. (As an aside, putting the content inline rather than in a component reduces the view state usage; can anyone explain that? Seems like perhaps the components are inlined as part of the rendering process.) The extensions do include the use of apex:inputField.

Is there a way to keep the components entirely out of the view state when they are not required?

PS

Perhaps dynamic Visualforce components in Apex is the way to go, but I've had the problem with them of field values being lost when an error occurs so am reluctant to go that route. Any other ideas?

  • The answer you're probably really not going to like would be to simply use Lightning instead (e.g. Lightning Out to a VF page). Given all the constraints you've provided, I don't think there is a server-side solution that will fully serve your purpose. – sfdcfox Nov 17 '18 at 17:59
  • @sfdcfox Yeah this is an old page/code that I don't want to do a big refactor on... – Keith C Nov 17 '18 at 19:30
  • 1
    To get the best of both worlds, have you tried JS remoting? If you have the component pull the data via JS remoting, that may help reduce your view state. If you're using JS to store some of the extraneous data, you shouldn't lose it if you switch out your button logic to use an action instead. – Jason Hardy Nov 17 '18 at 20:06
  • @JasonHardy Thanks yes that makes sense. Would be complicated though as there are editable fields. – Keith C Nov 17 '18 at 20:20
  • Not super complicated, just pull those particular fields in via JS as a part of your submit function. You wouldn't actually have to include them in a "form" at that point and that will decrease your view state. It's essentially how lightning works from a distance. Lightning essentially uses remotting to do everything with some predefined JS functions to help make your life easier. – Jason Hardy Nov 17 '18 at 20:34
2

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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