Friday, 22 July 2011

Managing a list of New Records in Visualforce

This week's post concerns bulk creation of an unknown number of sobject records - Accounts, for example. In this situation I want to display a table containing a set number of new records (5 in this case), enter the various pieces of information and save to the database en-masse.  However, I also want to be able to add or remove rows prior to saving anything, so that if I need to I can enter 1, 3, 10 etc at once.  I promise this is the last post for a while around list handling!

As the records haven't been added to the database yet, they don't have a unique identifier that I can use to locate them, so the first task is to create a wrapper class that encapsulates an Account and an identifier - I decided on an Integer.

public class AccountWrapper
{
 public Account acc {get; private set;}
 public Integer ident {get; private set;}
  
 public AccountWrapper(Integer inIdent)
 {
  ident=inIdent;
  acc=new Account(Name='Bulk Acc ' + ident);
 }
}
Note that the wrapper class provides a useful constructor, that creates the wrapped record and automatically populates the Name of the Account based on the unique identifier. I added this as the Name is a required field when used as the value for an apex:inputfield in Visualforce, so this saved me some time when testing. I could just as easily have changed the component to an apex:inputText to work around this.

As I'm only concerned with the uniqueness of the AccountWrapper within the context of my page, generating a unique identifier is as simple as defining a property in my controller:

private Integer nextIdent=0;

and then post-incrementing this each time I create a new wrapper instance. In my constructor I create the initial five records:

wrappers=new List<AccountWrapper>();
for (Integer idx=0; idx<5; idx++)
{
 wrappers.add(new AccountWrapper(nextIdent++));
}

In my page, I have an apex:pageBlockTable backed by the wrappers list. Each row of the table has a Delete button that if clicked, removes the entry from the list:
<apex:column headerValue="Action">
    <apex:commandButton value="Delete" action="{!delWrapper}" rerender="wtable">
       <apex:param name="toDelIdent" value="{!wrapper.ident}" assignTo="{!toDelIdent}"/> 
    </apex:commandButton>
 </apex:column>

The unique identifier is passed to the controller via the apex:param component. If this is the first time that you've encountered this component, you may be interested in another blog post of mine explaining its use in detail .

The action method associated with the delete simply iterates the list of wrappers and deletes the entry associated with the supplied identifier:


public void delWrapper()
{
 Integer toDelPos=-1;
 for (Integer idx=0; idx<wrappers.size(); idx++)
 {
  if (wrappers[idx].ident==toDelIdent)
  {
   toDelPos=idx;
  }
 }

 if (-1!=toDelPos)
 {
  wrappers.remove(toDelPos);
 }
}
 

The page also contains a couple more buttons, for adding one row, adding 5 rows and saving to the database.

<apex:page controller="ManageListController" tabstyle="Account">
 <apex:form >
   <apex:pageBlock title="Bulk Account Create">
      <apex:pageBlockTable value="{!wrappers}" var="wrapper" id="wtable">
         <apex:column headerValue="Ident">
            <apex:outputText value="{!wrapper.ident}"/>
         </apex:column>
         <apex:column headerValue="Name">
            <apex:inputField value="{!wrapper.acc.Name}"/>
         </apex:column>
         <apex:column headerValue="Parent">
            <apex:inputField value="{!wrapper.acc.ParentId}"/>
         </apex:column>
         <apex:column headerValue="Industry">
            <apex:inputField value="{!wrapper.acc.Industry}"/>
         </apex:column>
         <apex:column headerValue="Type">
            <apex:inputField value="{!wrapper.acc.Type}"/>
         </apex:column>
         <apex:column headerValue="Action">
            <apex:commandButton value="Delete" action="{!delWrapper}" rerender="wtable">
               <apex:param name="toDelIdent" value="{!wrapper.ident}" assignTo="{!toDelIdent}"/> 
            </apex:commandButton>
         </apex:column>
      </apex:pageBlockTable>
      <apex:commandButton value="Add Row" action="{!addRows}" rerender="wtable">
         <apex:param name="addCount" value="1" assignTo="{!addCount}"/> 
      </apex:commandButton>
      <apex:commandButton value="Add 5 Rows" action="{!addRows}" rerender="wtable">
         <apex:param name="addCount" value="5" assignTo="{!addCount}"/> 
      </apex:commandButton>
      <apex:commandButton value="Save" action="{!save}"/>
   </apex:pageBlock>
 </apex:form>
</apex:page>

Note that I'm using the apex:param technique for both the Add Row and Add 5 Rows buttons - this means I can have additional buttons for any number of rows without having to change the Apex code.


The full controller is shown below:

public class ManageListController 
{
 public List<AccountWrapper> wrappers {get; set;}
 public static Integer toDelIdent {get; set;}
 public static Integer addCount {get; set;}
 private Integer nextIdent=0;
 
 public ManageListController()
 {
  wrappers=new List<AccountWrapper>();
  for (Integer idx=0; idx<5; idx++)
  {
   wrappers.add(new AccountWrapper(nextIdent++));
  }
 }
 
 public void delWrapper()
 {
  Integer toDelPos=-1;
  for (Integer idx=0; idx<wrappers.size(); idx++)
  {
   if (wrappers[idx].ident==toDelIdent)
   {
    toDelPos=idx;
   }
  }
  
  if (-1!=toDelPos)
  {
   wrappers.remove(toDelPos);
  }
 }
 
 public void addRows()
 {
  for (Integer idx=0; idx<addCount; idx++)
  {
   wrappers.add(new AccountWrapper(nextIdent++));
  }
 }
 
 public PageReference save()
 {
  List<Account> accs=new List<Account>();
  for (AccountWrapper wrap : wrappers)
  {
   accs.add(wrap.acc);
  }
  
  insert accs;
  
  return new PageReference('/' + Schema.getGlobalDescribe().get('Account').getDescribe().getKeyPrefix() + '/o');
 }
 
 public class AccountWrapper
 {
  public Account acc {get; private set;}
  public Integer ident {get; private set;}
  
  public AccountWrapper(Integer inIdent)
  {
   ident=inIdent;
   acc=new Account(Name='Bulk Acc ' + ident);
  }
 }
}

The only additional item of interest in the controller is the returned PageReference from the save method:
return new PageReference('/' + Schema.getGlobalDescribe().get('Account').getDescribe().getKeyPrefix() + '/o');

This sends the user to the Accounts tab. I originally had the three character key prefix hardcoded, but that always feels wrong. Thus, while its highly unlikely that Salesforce will change the prefix, if they do I'm covered.

Here's some screenshots of the page in action.  First the initial page:



After I've clicked the Add 5 Rows Button



After I've deleted some entries at random:



And finally, after saving the records they are displayed in the recent list on the Accounts tab:

41 comments:

  1. This is a great approach...

    ReplyDelete
  2. Awesome,any guidance on how to write the test case for this?

    ReplyDelete
  3. Nice approach. I totally know what you mean about hard coding the return page reference url. You can even go one step farther by using the URLFOR function and passing the value it returns for the account tab as a parameter to your save method.

    This code is untested, but gives the gist of the technique:

    |apex:commandButton value="Save" action="{!save}"|
      |apex:param value={!URLFOR( $Action.Account.Tab , null)} name="retURL"/|
    |/apex:commandButton|

    Then in your save method you can grab the value passed in to the retURL param and use that for the redirect. This way if the format of the tab url ever changes from /XXX/o to something else you're code won't break. Will it ever happen? Probably not, but it does feel cleaner.

    Keep the great posts coming! Love your blog.

    ReplyDelete
  4. What if i want to add more lineitems to existing opportunity???
    Do i use the same approah

    ReplyDelete
    Replies
    1. This technique should work for that use case. Obviously you'll need to write the apex to wrap the opportunity and line items.

      Delete
  5. Thanks for your reply :)

    ReplyDelete
  6. Hello ,

    It is very Nice , But I need also to Add Attachments , is it possible ?

    Thanks

    ReplyDelete
  7. How can i let my Wrapper also add rows that Contain also an Attachment Input File . Thanks for your precious Help!!

    ReplyDelete
  8. Really this is great logic

    ReplyDelete
  9. Wow.. Very handy post.

    I used it in our instance to create/assign multiple tasks and assign to multiple users.

    Thanks Bob..

    ReplyDelete
  10. Thanks Bob,

    This saved me tonnes of time.

    Best,

    Dennis

    ReplyDelete
  11. This comment has been removed by the author.

    ReplyDelete
  12. This comment has been removed by the author.

    ReplyDelete
  13. Hi Bob,

    Its a Awesome Post.My requirement is lil bit different.I'll be having only one field (say suppose Name Field).Here When I click on Save button need to save all the Records in a Single field seperated by a semi-colon.Is it Possible

    ReplyDelete
  14. Hi Bob,

    It's give excellent output.

    ReplyDelete
  15. Minus the nice UI features of adding/deleting rows, could a Standard Set Controller do something similar with allowing input of multiple records for creation? Instead of starting with a query for Instantiation, found here , http://www.salesforce.com/us/developer/docs/pages/Content/apex_pages_standardsetcontroller.htm, could it just use a list of sObjects without the fields populated yet?

    ReplyDelete
  16. This is great! Such as time saver! I used this to make a bulk insert for opportunities. My users have been asking for this for forever =)

    ReplyDelete
  17. This comment has been removed by the author.

    ReplyDelete
  18. Guidance on getting this to test properly would be great.

    ReplyDelete
  19. Hi Bob,

    I want to insert new records and update existing records and delete newly entered records and also delete existing records if any on click of save & delete buttons respectively in your above code; so could you please help me how I can achieve this.

    ReplyDelete
  20. Hi,
    I displayed all the custom objects in an visualforce page using wrapper class.I try to give edit and delete functionalities but these objects related id's are not available.How to get these all custom objects related ids using wrapper class?

    please help me...........

    ReplyDelete
  21. Thank you very much for this code.! Was able to modify and achieve my functionality!!!!

    ReplyDelete
  22. Hi,
    I am using this functionality in my Vf page,I am using outputpanel for popup and using this pageBlockTable inside that.But thing is that Sometimes the row is inserting,sometimes not.Because the value is not incrementing in add account function.

    ReplyDelete
  23. Hi Keir,

    How would you go about modifying this to support bulk creation of detail records related to a master? Using the example of Company and Employee, where Employee is a detail of master Company, how would I pass the Company name without manually entering it for each record? Ideally, I would like to click a list button from the Employee related list on the Company detail page, mass enter the employees, and return to the Company detail page.

    Thank you,

    Tyler

    ReplyDelete
    Replies
    1. The easiest way to do this is to pass the id of the company on the URL. If you want to use a list button, define it as a URL (otherwise it has to use the standard list controller for the employee which probably isn't what you want). You'll need to set the URL to something like '/apex/?CompanyId={!Company__c.Id}', and in the controller constructor you can pull the parameter via ApexPages.CurrentPage().getParameters().get('CompanyId'). Once you have the ID you can SOQL back the name etc as you need to. Finally, change the save method to add the companyid to each of the employee records before inserting.

      Delete
    2. I truly appreciate your assistance. Your blogs and help across the internet have helped me immensely over the course of my SFDC career. Thank you for all that you do.

      Delete
  24. Hi Bob,
    great post and info as always. I did come up with a question though. I am using this and it is working great except for one thing. If I add a row and then try to delete it right away, it is making me enter a required field (a lookup that is to a master field in a master-detail relationship) for the object contained within the wrapper. this is before saving and committing to the database. If I enter a value that meets the filter criteria, it will let me delete (without committing to the database). Seems odd that it enforces this required field before simply removing that record from the list. Do you know of anyway around this? the part of my delWrapper is slightly different due to needing to also handle existing records. that part is as follows:

    if (-1!=toDelPos)
    {

    if(wrappers.get(toDelPos).isSaved)
    {
    delete wrappers.get(toDelPos).oppComp;
    }
    wrappers.remove(toDelPos);
    }

    ReplyDelete
    Replies
    1. it looks as if it is not even getting to the delWrapper method but rather it is getting hung up on the Wrappers getter method. also, as a side note, the dml above is now in try/catch, just did not have it initially during testing

      Delete
    2. The requiredness of a field is enforced before the server side code is executed. If you wrap the delete button in an action region that will avoid posting back the contents of the row and should allow the delete to succeed. This does mean that any changes you have made elsewhere would not be saved though.

      Delete
  25. Hi Bob, thanks for this and it works great. I have a question though as I need some more functionality and am not too experienced in VF and apex (yet, but getting there). My case requires me to do a mass insert of a custom object which is linked to an Opportunity (each row has a lookup to an Opp). I therefore require a button on Opportunity listview, where I can select multiple Opps, then click the button which will display my new VF page to insert a new item for each selected Opp. How can I create this button on the Opp listview and get it to display each selected Opp in the new VF mass insert page table?

    ReplyDelete
  26. This is very helpful Bob, Thank you.

    I'm using the list to create new detail records from a Quote master record. I have a url button on the Quote layout that calls the above page with /apex/theListPage?id={!Quote.Id}. I have read many posts but still can't get my head around how to get the Quote Id into my new detail records. The page won't save currently becase the mandatory Quote Id value isn't set on the new records.

    Thought I had it by passing the parameter into the constructor but apparently this isn't allowed as I received an 'Unknown Constructor' error.

    Any suggestions would be greatly appreciated.

    Many Thanks,
    Mike.

    ReplyDelete
  27. hello sir,
    I need a help , i used your code and works fine and thanks for this code to share.
    Now , what i want that i have only one field that is need to repeat and others are need not to be repeated , now how can i save data
    with one field is repeated and other are not .

    ReplyDelete
  28. Hi Bob, can we display the records in columns instead of rows i mean like time sheet i want to display 1 week dates as column headers and column values are hours. how can we do it in salesforce plz help me on this

    ReplyDelete
  29. Bob you are my savior :)
    We had a similar requirement... and I was trying my own logic which totally messed up with apex:repeat tags.... :(
    When I was looking for some hints to perfect code this page was the exact model I was looking for :)
    Simply crisp and clean shot.... I was able to change the obj and some new fields had all my custom code brought into this controller... it worked way more than perfect...... my clients loved it :)
    Thanks a lot for all your blog contributions :)

    ReplyDelete