I'm having some trouble with what I thought would turn out to be a simple form built upon a custom visualforce page. The back story here is that I need to display a dynamic number of records to a visualforce page in a table and receive text input associated to each of the rows of that data. The input will then be used to record new records that will relate to the records already in the table.

I've gotten as far as displaying the dynamic component to the page, collecting the data from the input fields, and pushing all the input to a webservice method within that page controller. However, now I can't seem to figure out how to reference the variables in the global class that is the page controller but also houses the webservice method. Below is my code. I'm still learning the ropes to apex, visualforce, and javascript so feedback on any of this is much encouraged!

 * Author: Andrew Bettke
 * Date: 12/8/14
 * Last Revision: 12/9/14
 * The controller handles the 'SIBInvoiceWizard' custom visualforce page. This page will be used by the account department to mass create SIB Invoices
 * (along with billing event records) quickly and seamlessly from one page. The page will be accessed from a button on any active client account detail
 * page. From there, the page will dynamically displays editable data tables for each savings item that can billed for that month. After submitting the
 * form with, the controller will analyze the data and created invoice and billing event records accordingly. 

global class SIBInvoiceWizard {

    //The account for which we will be generating an invoice.
    public Account account {get; set;}
    public List<Location_Audits__c> audits {get; set;}
    public List<Opportunity> savingsItems {get; set;}
    public List<Id> savingsItemsCummulative {get; set;}

    //Upon intitialization, grab the account that we will be dealing with.
    public SIBInvoiceWizard(){
        //Use the accound ID that was passed from the account detail page to the 'SIBInvoiceWizard' page to pull the account record. 
        //We'll use the account record later on to search for relevant audits and savings items.
        account = [SELECT ID, Name FROM Account Where Id = :ApexPages.currentPage().getParameters().get('accId')];
        savingsItemsCummulative = null;     

    //Dynamic pageBlock component. If no audits or savings items are found then display output messages to notify the user that the wizard is
    //unavailable to them at this time. If audits and savings are found then create all necessary editable data tables. 
    public Component.Apex.PageBlock getEditableDataTable(){

        //Creation of the dynamic pageBlock to be returned.
        Component.Apex.PageBlock pageBlock = new Component.Apex.PageBlock();

        //Set the title to the name of the account we are dealing with. 
        pageBlock.title = account.Name;
        pageBlock.Id = 'accountData';
        //Grab a list of all audits that are currently processing savings items.
        audits = [SELECT Id, Name, Stage__c, Audit_Type__c FROM Location_Audits__c 
                                           WHERE Account__r.Id = :account.Id AND Stage__c = 'Audit Complete - Processing Savings'];
        //If we found any audits processing savings items...
        if(audits.size() > 0){

            for(Location_Audits__c audit : audits){

                Component.Apex.PageblockSection auditSection = new Component.Apex.PageblockSection(columns=1);
                auditSection.title = audit.Name + ' - ' + audit.Audit_Type__c;

                savingsItems = [SELECT Id, Name, Savings_Source__c, Type, Amount, CV__c, CV_Total_Billed__c, CV_Remaining__c FROM Opportunity
                                                  WHERE Audit_Number__c = :audit.Id AND StageName = 'Finalized Savings' AND CV_Remaining__c > 0];
                for(Opportunity si : savingsItems){
                if(savingsItems.size() > 0){
                    Component.Apex.PageblockTable table = new Component.Apex.PageblockTable();
                    table.title = audit.Name + audit.Audit_Type__c;
                    table.value = savingsItems;
                    table.var = 'si';

                    Component.Apex.Column savingsName = new Component.Apex.Column(headerValue='Opportunity Name');
                    savingsName.expressions.value = '{!si.Name}';
                    Component.Apex.Column savingsSource = new Component.Apex.Column(headerValue='Savings Source');
                    savingsSource.expressions.value = '{!si.Savings_Source__c}';
                    Component.Apex.Column savingsType = new Component.Apex.Column(headerValue='Type');
                    savingsType.expressions.value = '{!si.Type}';
                    Component.Apex.Column savingsAmount = new Component.Apex.Column(headerValue='Savings Amount');
                    savingsAmount.expressions.value = '{!si.Amount}';
                    Component.Apex.Column savingsCV = new Component.Apex.Column(headerValue='CV');
                    savingsCV.expressions.value = '{!si.CV__c}';
                    Component.Apex.Column savingsCVBilled = new Component.Apex.Column(headerValue='CV - Total Billed');
                    savingsCVBilled.expressions.value = '{!si.CV_Total_Billed__c}';
                    Component.Apex.Column savingsCVRemaining = new Component.Apex.Column(headerValue='CV - Remaining');
                    savingsCVRemaining.expressions.value = '{!si.CV_Remaining__c}';
                    Component.Apex.Column savingsCVBilling = new Component.Apex.Column(headerValue='Amount Billing');

                    Component.Apex.InputText savingsCVBillingInput = new Component.Apex.InputText();


                    Component.Apex.outputText noSavings = new Component.Apex.outputText();
                    noSavings.value = 'There are no savings items finalized for ' + audit.Name + ' ('+audit.Audit_Type__c + '). ' +
                                      'Check back again later after savings have been finalized to generate billing events.';


        //If we found no audits processing savings items then return the pageBlock to notify the user.
            //Create new outputText component to add to the dynamic pageBlock
            Component.Apex.outputText noAudits = new Component.Apex.outputText();

            //Set text displayed to the user.
            noAudits.value = 'There are no audits processing savings at this time. '+ 
                             'Check back again later after savings have been finalized to generate an invoice.';
            //Add the outputText to the dynamic component.

        return pageBlock;

    webservice static void SIBInvoiceWizardFormSubmit(List<String> billingAmounts, List<Id> savingsItemsToRelate){
        Savings_Item_Billing_Event__c sibe = new Savings_Item_Billing_Event__c();


<apex:page controller="SIBInvoiceWizard" >

        <apex:includeScript value="/soap/ajax/29.0/connection.js"/>
        <apex:includeScript value="/soap/ajax/29.0/apex.js"/>

    <script type="text/javascript">

        function formSubmission(){
            if(confirm("Have you reviewed your work? Are you sure you wish to submit these amounts?")){

        function getFormData(){
            var inputTextElements = document.getElementById("j_id0:j_id5:accountData").getElementsByTagName("input");
            var inputTextAmounts = new Array();
            for(i = 0; i < inputTextElements.length; i++){
                    alert("An illegal character was used at item: " + inputTextElements[i].value + ". Please fix and resubmit.");
                } else{

            sforce.connection.sessionId = "{!$Api.Session_ID}";
            sforce.apex.execute("SIBInvoiceWizard", "SIBInvoiceWizardFormSubmit",{billingAmounts:inputTextAmounts}); 



    <apex:sectionHeader title="SIB Invoice Wizard - (Currently In Development)"/>
        <apex:form onsubmit="">
            <apex:dynamicComponent componentValue="{!editableDataTable}"/>
            <apex:commandButton value="Submit" rerender="None" onclick="formSubmission();"/>
        <apex:pageMessages ></apex:pageMessages>

  • 1
    You have to pass all the data you need into the static method; the method cannot access any of the (non-static) fields of the controller class.
    – Keith C
    Dec 11 '14 at 0:06
  • What if I make this method into a remote action instead of a webservice? would I then have access to all of my instance variables previously defined? Dec 11 '14 at 15:45
  • A @RemoteAction is also a static controller method. If you really want to do this you need to use a mechanism that pushes the view state back to the controller (populating the non-static fields) such as apex:actionFunction. Your controller has an unusual design...
    – Keith C
    Dec 11 '14 at 16:17
  • Ahh Ok, this makes sense now. As to the unusual design I'd assume as much. I've only ever written a couple controllers so I'm still getting the hang of it. If you don't mind me asking, how would you have written the controller? Dec 11 '14 at 22:30
  • See answer for how I would have (attempted) to write this.
    – Keith C
    Dec 11 '14 at 23:44

I may be missing something that has forced you to use the component classes or some other aspect of what you are doing.

But I would use normal Visualforce markup:

    <apex:pageBlock title="{!account.Name}">
        <apex:repeat value="{!audits}" var="audit">
            <apex:pageBlockSection title="{!audit.Name + ' - ' + audit.Audit_Type__c}">
                <apex:pageBlockTable value="{!audit.Opportunities}" var="si">
                    <apex:column value="{!si.Name}"/>
                    <apex:column value="{!si.Savings_Source__c}"/>
                    <apex:column headValue="Amount Billing">
                        <apex:inputField value="{!si.ExtraCustomFieldToHoldAmountBilling__c"/>
    <apex:commandButton value="Submit" action="{!normalControllerMethod}"/>

with the Location_Audits__c and Opportunity records queried together via a relationship query (or they could be connected via an Apex wrapper class):

public Location_Audits__c[] audits {
    get {
        if (audits == null) {
            audits = [
                    SELECT Id, Name, Stage__c, Audit_Type__c,
                            (SELECT Id, Name, Savings_Source__c, Type, ... FROM Opportunities)
                    FROM Location_Audits__c 
                    WHERE Account__r.Id = :account.Id
                    AND Stage__c = 'Audit Complete - Processing Savings'
        return audits;
    private set;

Then the normal page submit would automatically transfer the updates back to the controller and the Opportunity.ExtraCustomFieldToHoldAmountBilling__c can then be grabbed in the normalControllerMethod and transferred into whatever object you want to put it.

  • Oh I see what you're doing here. The query for the dynamic number of records that will be in the table is tied to the actual get method. I never thought of this. One question about the query in the controller class: I see you have the query for the opportunities embedded inside the query for the audits. Is this just put at the end of the select statement? How does this syntax usually work? Dec 15 '14 at 14:16
  • @AndrewBettke This is a very useful feature of SOQL. You can query one level of child objects down in the select and also up to 5 levels up in the select with the __r relationship fields then available linking the objects together. Well worth reading the Relationship Queries page I link to in the answer because there are some subtleties including exactly what the relationship name to use is.
    – Keith C
    Dec 15 '14 at 14:27
  • This is great! This technique is shedding light on a lot of things. My knowledge of Visualforce and apex is an accumulation of scouring internet forums and documentation. Dec 15 '14 at 14:38
  • As for the field 'Amount Billing', I actually need to create a new invoice record and several new billing event records based on if that field is filled in or not. For this, I'm guessing I'll just need to make a for look to create (n) amount of billing event records so that I can pull those fields into the table as an the extra field? Or should I just keep it as inputText and create the records later based on if the inputText is filled in or not? Dec 15 '14 at 14:40
  • @AndrewBettke I'm unclear what you are trying to do so can't really advise...
    – Keith C
    Dec 15 '14 at 14:43

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.