Tweet |
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:
This is a great approach...
ReplyDeleteAwesome,any guidance on how to write the test case for this?
ReplyDeleteNice 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.
ReplyDeleteThis 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.
What if i want to add more lineitems to existing opportunity???
ReplyDeleteDo i use the same approah
This technique should work for that use case. Obviously you'll need to write the apex to wrap the opportunity and line items.
DeleteThanks for your reply :)
ReplyDeleteHello ,
ReplyDeleteIt is very Nice , But I need also to Add Attachments , is it possible ?
Thanks
How can i let my Wrapper also add rows that Contain also an Attachment Input File . Thanks for your precious Help!!
ReplyDeleteReally this is great logic
ReplyDeleteAwesome Bro, Nice Coding
ReplyDeleteWow.. Very handy post.
ReplyDeleteI used it in our instance to create/assign multiple tasks and assign to multiple users.
Thanks Bob..
Thanks Bob,
ReplyDeleteThis saved me tonnes of time.
Best,
Dennis
Awesum man
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThanks a lot Bob...
ReplyDeleteHi Bob,
ReplyDeleteIts 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
Hi Bob,
ReplyDeleteIt's give excellent output.
its a gd post
ReplyDeleteMinus 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?
ReplyDeleteThis 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 =)
ReplyDeleteGreat Work, Simply Brilliant
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteGreat post. Love your blog!
ReplyDeleteGuidance on getting this to test properly would be great.
ReplyDeleteHi Bob,
ReplyDeleteI 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.
Hi,
ReplyDeleteI 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...........
Thank you very much for this code.! Was able to modify and achieve my functionality!!!!
ReplyDeleteHi,
ReplyDeleteI 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.
Hi Keir,
ReplyDeleteHow 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
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.
DeleteI 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.
DeleteHi Bob,
ReplyDeletegreat 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);
}
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
DeleteThe 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.
DeleteReally Appreciate your code.
DeleteTried with a Master detail field and delete functionality
not working .
Its deleting from the wrapper ..but not refreshing in the Page .. So delete is not working .Any workaround for this . Please
great example bro
ReplyDeleteHi 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?
ReplyDeleteThis is very helpful Bob, Thank you.
ReplyDeleteI'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.
hello sir,
ReplyDeleteI 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 .
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
ReplyDeleteBob you are my savior :)
ReplyDeleteWe 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 :)
Is it possible to display blank rows on VF without using 'Add Rows' button?
ReplyDeleteThank You Bob, this worked perfectly for me. Now I need to figure out writing the test class and I will be golden.
ReplyDeletedear bob i am geting this error in this code
ReplyDeleteError: Compile Error: Invalid constructor syntax, name=value pairs can only be used for SObjects: account at line 63 column 8
dear bob i am getting this error in manage list controller code
ReplyDeleteError: Compile Error: Invalid constructor syntax, name=value pairs can only be used for SObjects: account at line 63 column 8
what can i do plese plese explain me resend the code for me
Hello,
ReplyDeleteCan i use this method for any custom object? If yes how?
Hello,
ReplyDeleteCan i use this method for custom object? If yes how?
when i click on a new button in an Custom object the above Visualforce page should appear . what should i do for that
ReplyDeletei have an Standard Object Which contains the field called Amount And i have an custom objct(Child of standars object) Which also contains field called Amount.Now,the custom Object amount should not exceed the standard object amount.and also if the amount paid in Installments it shoukd not exceed the Standard object amount
ReplyDeleteHow to do this in a lightning component Create New Record
ReplyDeleteHow to do this in a lightning component Create New Record
ReplyDeleteMr Bob - Thank you so much for sharing this! this walk through is really fantastic. I am desperately trying to figure out how can open an existing related lists onto this marvelous multi edit dynamic design -
ReplyDeleteI have users with a set of records thats are automated and require editing/deleting/adding to get fully correct. Really would appreciate you assistance