Saturday, 13 August 2011

DML During Initialisation

I ran into a situation recently where I wanted to be able to insert a record as part of the construction of a Visualforce controller, which the user then edited. As we all know, DML is not allowed in the constructor, so I started investigating alternatives.

One mechanism is to create an interim Visualforce page with a page action attribute tied to a controller method that inserts the record and then redirects the user to the actual Visualforce edit page. The downside to this method is that I would end up with two pages and two controllers, as its not safe for this to be incorporated into a single controller due to the fact that order of execution cannot be guaranteed.

An alternative solution, and the route that I chose to go, is to use javascript to invoke an action method on the controller, and have this action method carry out the DML operation. In order to ensure that this doesn't happen every time that the page is refreshed, I have an "initialised" flag in my controller that is set to false in the constructor, and updated to true once the DML has taken place.

The controller is shown below:
public class InsertObjectController 
{
 public Boolean initialised{get; set;}
 public Account acc {get; set;}
 
 public InsertObjectController()
 {
  initialised=false;
 }
 
 public void init()
 {
  if (!initialised)
  {
   acc=new Account();
   acc.Name='Blog Account';
   insert acc;
   initialised=true;
  }
 }
}

On the page side, the main change was to put out a please wait spinner while the initialisation was going on, so that the user wasn't tempted to start entering data while the initialisation was taking place. The DML operation is executed via an actionfunction tied to the onload handler, and once this completes the main body of the page is rerendered, replacing the spinner with the newly inserted record detail (including the id of the account).

Note that it this will overwrite any existing onload handler - for details of how to add a function to the end of the existing onload handler see this blog post.

The page markup is shown below:

<apex:page controller="InsertObjectController">
  <apex:form >
    <apex:actionFunction name="doInit" action="{!init}" rerender="allPanel"/>
    <apex:outputPanel id="allPanel">
      <apex:outputPanel rendered="{!NOT(initialised)}">
        <p align="center" style='{font-family:"Arial", Helvetica, sans-serif; font-size:20px;}'><apex:image value="/img/loading.gif"/>&nbsp;Please wait</p>
        <script>
           window.onload=function()
           {
             doInit();
           };
        </script>
      </apex:outputPanel>
      <apex:outputPanel rendered="{!initialised}">
        <apex:pageBlock title="Account Details">
          <apex:pageBlockSection >
            <apex:inputField value="{!acc.id}"/>
            <apex:inputField value="{!acc.name}"/>
            <apex:inputField value="{!acc.type}"/>
            <apex:inputField value="{!acc.industry}"/>
          </apex:pageBlockSection>
        </apex:pageBlock>
      </apex:outputPanel>
    </apex:outputPanel>
  </apex:form>
</apex:page>

10 comments:

  1. Hey Bob,

    Very cool workaround, but there is a cleaner way to do this using the page action attribute. You can put any action function in this attribute and it will run after the constructor. You should be careful using this since it opens up some security holes as the CSRF token isn't needed to perform the action. However, in most instances the benefit outweighs the risk. There is a detailed discussion of this available on developer force at http://wiki.developerforce.com/index.php/Secure_Coding_Cross_Site_Request_Forgery#Apex_and_Visualforce_Applications.

    Here's what it would look like. Not that because tags are removed from comments I've had to strip the "<" and ">" and replace them with pipes in the visual force page code.

    public class InsertObjectController
    {
    public Account acc {get; set;}

    public InsertObjectController() { }

    public void init()
    {
    acc=new Account();
    acc.Name='Blog Account';
    insert acc;
    }
    }

    |apex:page controller="InsertObjectController" action="{!init}"\|
    |apex:form|
    |apex:pageBlock title="Account Details"|
    |apex:pageBlockSection|
    |apex:inputField value="{!acc.id}"/|
    |apex:inputField value="{!acc.name}"/|
    |apex:inputField value="{!acc.type}"/|
    |apex:inputField value="{!acc.industry}"/|
    |/apex:pageBlockSection|
    |/apex:pageBlock|
    |/apex:form|
    |/apex:page|

    ReplyDelete
  2. Actually the reason I didn't go the page action route is mentioned in the blog post - order of execution.  There's no guarantee (as far as I'm aware) that the page action will run before the getter.

    I have used it in the past without issue, but it isn't advised for this reason.

    ReplyDelete
  3. I haven't investigated this in full depth, but as the action method fires before the page renders it will fire before the getters unless the getters are referenced in the constructor or action function itself.

    ReplyDelete
  4. Hi Ralph,

    I have researched this though - I suggest you read the following discussion board thread:

    http://boards.developerforce.com/t5/Visualforce-Development/Reason-why-action-attribute-of-apex-page-should-not-be-used-for/td-p/104343

    This is the reason I was looking for another mechanism - if the Visualforce developers tell me I shouldn't use that method as the order of execution isn't guaranteed, that's good enough for me.

    ReplyDelete
  5. Alternatively is it possible to do the insert in a static block of the controller ?

    ReplyDelete
  6. My page is stuck at "loading Please Wait!". What is the problem. Is it necessary to provide some value through controller. Please help me.

    ReplyDelete
    Replies
    1. That sounds like the init action method failed to set the initialised flag. Best bet is to post your code up the to developerforce visualforce board and tweet/dm/comment the link, and I'll try to help.

      Delete
  7. BB/K-
    I am using action= on a page that is embedded in a std page. The actioned Apex code then calls another class that does a REST Callout. When a Save happens I get an initial refresh then a second refresh after getting results.

    It basically works but the waits and refreshes are clunky. Any thoughts on how to do better? I really only want the callout when email field changed and I go validate and then a rerender. Trigger seems a problem with REST and Synchronous calls..not allowed I think.



    ReplyDelete
  8. I'm doing some testing, and this seems to work fine except when I have renderAs="PDF"

    Any idea why that wouldn't work?

    Thanks,

    ReplyDelete
    Replies
    1. Yes - this solution relies on JavaScript and PDF doesn't have a JavaScript engine, so there is nothing to interpret the JavaScript and execute the code.

      Delete