[SalesForce] Field Values In Dynamic Component Not Retaining After Error

I updated the question due to the issue being specifically for a dynamic component

Currently, I have a custom VisualForce page used to fill out a new Opportunity. Everything works as intended except for a certain scenario.

The issue is when you select save when all the required fields aren't populated. When you choose to save and not all of the fields are populated, some of the fields are retained (specifically lookup fields) and others (date/time picklists and text boxes) are not retained after the page is refreshed due to a page error. Please note: the values are being put out by a dynamic component.

I understand partially why this happens (some values are not stored in the server and certain ones are not) but, I do not know exactly how to work-around the issue. I have come up with a few decent attempts but I haven't had any success.

Has anyone found a way to work-around this issue?

Here is some code to better understand and I figure I will get asked to share the code anyway.

Controller Code

public with sharing class ExampleController
{
    private ApexPages.StandardController InheritedStandardController { get; set; }

    public Component.Apex.PageBlockSection DynamicSection { get { return this.CreateDynamicSection(); } }

    public Opportunity CurrentOpportunity { get; set; }

    public ExampleController(ApexPages.StandardController InheritedStandardController)
    {
        this.InheritedStandardController = InheritedStandardController;
        this.CurrentOpportunity = (Opportunity)InheritedStandardController.getRecord();
    }

    private Component.Apex.PageBlockSection CreateDynamicSection()
    {
        Component.Apex.PageBlockSection InternalDynamicSection = new Component.Apex.PageBlockSection();
        InternalDynamicSection.expressions.value= '{!Opportunity}';

        for(Schema.FieldSetMember Field : Schema.SObjectType.Contact.fieldSets.getMap().get('OpportunityFields').getFields())
        {
            Component.Apex.Column OpportunityColumn = new Component.Apex.Column();
            OpportunityColumn.Facets.Header = new Component.Apex.OutputText(value = this.FieldLabelCorrection(Field.getLabel()));
            OpportunityColumn.expressions.value = '{!Opportunity.' + Field.getFieldPath() + '}';
            InternalDynamicSection.childComponents.add(OpportunityColumn);
        }

        return InternalDynamicSection;
    }

    public PageReference save()
    {
        this.OpportunityName = this.CurrentOpportunity.Name;
        if(this.OpportunitySavedProperly() == false)
            return new PageReference( '/apex/EditNewPage');

        PageReference CorrectRedirectionPage = new PageReference( '/' + this.CurrentOpportunity.Id);
        CorrectRedirectionPage.setRedirect(true);
        return CorrectRedirectionPage;
    }


    private Boolean OpportunitySavedProperly()
    {
        try
        {
            Utility.UpsertWithoutSahring(this.CurrentOpportunity);
            return true;
        }
        catch(Exception ExceptionWasThrown)
        { 
            ApexPages.Message FailureMessage = new ApexPages.Message(ApexPages.Severity.FATAL, ExceptionWasThrown.getDmlMessage(0)) ;
            ApexPages.addMessage(FailureMessage);
            return false;
        }
    }
}

public without sharing class Utility
{
    public static void UpsertWithoutSahring(Opportunity SomeOpportunity)
    {
        try
        {
            UPSERT SomeOpportunity;
        }
        cacth(Exception DMLFailure){ throw DMLFailure; }
    }
}

VisualForce Page

<apex:page tabStyle="Opportunity" standardController="Opportunity" extensions="ExampleController">
    <apex:pageBlock title="Opportunity Edit" tabStyle="Opportunity" id="OpportunityInfo">

        <apex:pageBlockButtons>
            <apex:commandButton action="{save}" value="Save"/>
        </apex:pageBlockButtons>

        <apex:pagemessages />

        <apex:dynamicComponent componentValue="{!DynamicSection}"/>

    </apex:pageBlock>
</apex:page>


Solutions I Have Tried

The first thing I tried was changing the PageReference return null like so:

public PageReference save()
{
    if(this.OpportunitySaved() == false)
        return null;

    PageReference CorrectRedirectionPage = new PageReference( '/' + this.CurrentOpportunity.Id);
    CorrectRedirectionPage.setRedirect(true);
    return CorrectRedirectionPage;
}

But it did not work that way.

I tried using methods with apex:params but was unsuccessful and from the input that Matt Lacey gave, that would not bear the fruit I was looking for.

I also tried to see if I could save the information somewhere prior to a save. So I had to alter my code like the following:

Apex Code

public Opportunity CloningOpportunity { get; set; }

public PageReference RestoreFromClone()
{
    if(this.CloningOpportunity != null)
    {
        this.CurrentOpportunity.Name = this.CloningOpportunity.Name;
        this.CloningOpportunity = null;
    }

    return null;
}

public PageReference CloneCurrentOpportunity()
{
    this.CloningOpportunity = this.CurrentOpportunity.clone(false, true, false, false);
    return null;
}

Added in VisualForce

<apex:page tabStyle="Opportunity" standardController="Opportunity" extensions="ExampleController" action="{!RestoreFromClone}">
    <apex:actionFunction name="SpecialtySave" action="{!save}"/>
    <apex:actionFunction name="CloneInCaseOfCancel" action="{!CloneCurrentOpportunity}" immediate="true"/>

        ....
        <apex:commandButton action="{CloneAndSave}" value="Save"/>

        <script>
            function CloneAndSave()
            {
                CloneInCaseOfCancel();
                SpecialtySave();
            }
        </script>
        ....

What I was expecting is, the CloneInCaseOfFailure method to clone the Opportunity and stick it to the view state (hence why I chose that function to be immediate), the if there was a failure on save, it would hit the RestoreFromClone method and restore the current opportunity with the clone.

Problem is, despite everything, the clone still remains null. I've tried making it static and it still doesn't stick to the view state.

Hopefully, this will help find a solution, since the only other way to resolve this is by moving out of the dynamic component entirely (which I do not want to do).

Apologies for not posting that this was in a dynamic component sooner. This may have clarified a few things.

Best Answer

We have used dynamic components in one piece of code and based on that experience since then have avoided them.

When normal Visualforce tags are used, the component tree is part of the serialized view state and that is necessary for the entered values to be displayed under error conditions. (The life cycle is likely to be this or similar.)

But you will notice that you have to use get methods for dynamic components and can't hold (non-transient) references to them. When we created a case on this (in 2013), the Salesforce support response was:

... dynamic components are not serializable ... is completely something working as designed ...

(Our assumption is that if they can't be serializable as controller fields they probably can't be serialized as part of the page component tree either.)

The bottom line appears to be that dynamic components don't behave well when errors occur. So the straightforward choices are to either just live with that or to re-implement without the dynamic components.

PS

Had another case where dynamic components were the simplest way and have successfully (so far) worked around the field value not being retained problem via JavaScript. See the answer to How to keep components out of the Visualforce view state when they are not needed?.

Related Topic