To this end I've created a page and associated controller that allows a subset of details for all contacts (and the parent account) to be edited and saved in one go. This also allows new contacts to be created and existing contacts to be deleted. Below is a screen shot of the page:
The page itself is pretty clunky - clicking the Delete contact button fires the action method to carry out the delete without any user confirmation, while the New Contact button creates a new contact called "Change Me" and refreshes the page:
It does have one smarter feature though - if the page is accessed without specifying an ID, it goes into new account mode, and doesn't render any content or buttons related to contacts.
The Visualforce markup is shown below. If you are a regular reader of this blog you'll recognise it as heavily based on the sample from the Persisting List Edits in Visualforce post. Note that as usual you don't need a lot of markup to produce some pretty useful functionality.
<apex:page standardController="Account" extensions="AccountAndContactsEditExtensionV1" tabStyle="Account" title="Prototype Account Edit"> <apex:pageMessages /> <apex:form > <apex:pageBlock mode="mainDetail"> <apex:pageBlockButtons > <apex:commandButton action="{!cancel}" value="Exit" /> <apex:commandButton action="{!save}" value="Save" /> <apex:commandButton action="{!newContact}" value="New Contact" rendered="{!NOT(ISBLANK(Account.id))}"/> </apex:pageBlockButtons> <apex:repeat value="{!$ObjectType.Account.fieldSets}"> </apex:repeat> <apex:pageBlockSection title="Account Details" collapsible="true" id="mainRecord" columns="2" > <apex:inputField value="{!Account.Name}"/> <apex:inputField value="{!Account.Type}"/> <apex:inputField value="{!Account.BillingStreet}"/> <apex:inputField value="{!Account.ShippingStreet}"/> <apex:inputField value="{!Account.Industry}"/> <apex:inputField value="{!Account.Phone}"/> </apex:pageBlockSection> <apex:outputPanel id="contactList"> <apex:repeat value="{!contacts}" var="contact" > <apex:pageBlockSection columns="1" title="Contact {!contact.Name}" collapsible="true"> <apex:pageBlockSectionItem > <apex:pageBlockSection columns="2"> <apex:inputField value="{!contact.title}"/> <apex:inputField value="{!contact.phone}"/> <apex:inputField value="{!contact.FirstName}"/> <apex:inputField value="{!contact.LastName}"/> <apex:inputField value="{!contact.email}"/> </apex:pageBlockSection> </apex:pageBlockSectionItem> <apex:commandButton value="Delete Contact" action="{!deleteContact}" rerender="contactList"> <apex:param name="contactIdent" value="{!contact.id}" assignTo="{!chosenContactId}"/> </apex:commandButton> </apex:pageBlockSection> </apex:repeat> </apex:outputPanel> </apex:pageBlock> </apex:form> </apex:page>
Deleting a contact requires that the controller is notified of the ID to delete. This is accomplished via the nested param component for the "Delete Contact" button:
<apex:commandButton value="Delete Contact" action="{!deleteContact}" rerender="contactList"> <apex:param name="contactIdent" value="{!contact.id}" assignTo="{!chosenContactId}"/> </apex:commandButton>
The ID of the contact chosen to delete is propagated back to the controller via the the assignTo attribute. This means that by the time the deleteContact action method is invoked, the chosenContactId controller property will have been populated with the ID from the page. One important point to note about this - I've found that since Winter 11, you must have a rerender attribute on the commandButton for the parameter to be passed to the controller.
The controller is a little more complex, as it has action methods to support each of the buttons - Save and Exit, Save (and remain on page), Delete Contact and New Contact.
public class AccountAndContactsEditExtensionV1 { private ApexPages.StandardController std; // the associated contacts public List<Contact> contacts; // the chosen contact id - used when deleting a contact public Id chosenContactId {get; set;} public AccountAndContactsEditExtensionV1(ApexPages.StandardController stdCtrl) { std=stdCtrl; } public Account getAccount() { return (Account) std.getRecord(); } private boolean updateContacts() { boolean result=true; if (null!=contacts) { List<Contact> updConts=new List<Contact>(); try { update contacts; } catch (Exception e) { String msg=e.getMessage(); integer pos; // if its field validation, this will be added to the messages by default if (-1==(pos=msg.indexOf('FIELD_CUSTOM_VALIDATION_EXCEPTION, '))) { ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, msg)); } result=false; } } return result; } public PageReference saveAndExit() { boolean result=true; result=updateContacts(); if (result) { // call standard controller save return std.save(); } else { return null; } } public PageReference save() { Boolean result=true; PageReference pr=Page.AccountAndContactsEditV1; if (null!=getAccount().id) { result=updateContacts(); } else { pr.setRedirect(true); } if (result) { // call standard controller save, but don't capture the return value which will redirect to view page std.save(); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Changes saved')); } pr.getParameters().put('id', getAccount().id); return pr; } public void newContact() { if (updateContacts()) { Contact cont=new Contact(FirstName='Change', LastName='Me', AccountId=getAccount().id); insert cont; // null the contacts list so that it is rebuilt contacts=null; } } public void deleteContact() { if (updateContacts()) { if (null!=chosenContactId) { Contact cont=new Contact(Id=chosenContactId); delete cont; // null the contacts list so that it is rebuilt contacts=null; chosenContactId=null; } } } public List<Contact> getContacts() { if ( (null!=getAccount().id) && (contacts == null) ) { contacts=[SELECT Id, Name, Email, Phone, AccountId, Title, Salutation, OtherStreet, OtherState, OtherPostalCode, OtherPhone, OtherCountry, OtherCity, MobilePhone, MailingStreet, MailingState, MailingPostalCode, MailingCountry, MailingCity, LeadSource, LastName, HomePhone, FirstName, Fax, Description, Department FROM Contact WHERE AccountId = : getAccount().ID ORDER BY CreatedDate]; } return contacts; } }
In part 2, I'll look at improving the user experience by adding confirmation of delete and allowing the user to specify fields when creating a new contact. This page also looks like it would benefit from fieldsets, so its likely those will be introduced as well.
This comment has been removed by the author.
ReplyDeletewhile clicking on new contact button i am just doing like below written code.. then how can i delete the respective contact...i think you are doing an insert and delete dml on the contact every time.Apart from that i will do like this.. then how can i achieve the delete functionality....Hope you got my point?
ReplyDeletelist lstcons = new list();
public list getcontacts(){
contact obj = new contact();
lstcons.add(obj);
}
Can you explain what you mean by "delete the respective contact"? If you look at my code, it determines the contact to delete based on an apex:param nested in the command button, which passes the id of the contact to delete to the controller.
ReplyDelete1. by clicking on new contact initially you are inserting a dummy contact right?here I am creating a list of contact objects only but not inserting in to the database in my scenario. why because i want to eliminate unnecessary insertions and deletions.
ReplyDelete2.if that is the scenario then how can i identify my contact to delete i.e. what is the value that i can set to the param in the command button as that contact is not there in the database..got my point?how can i handle this scenario....
In this case I am inserting a dummy that the user then has to update. If you take a look at the second part of this blog, you'll see a different technique, but I don't think that is what you are looking for either.
ReplyDeleteI suspect you'll need to use a wrapper class that contains the contact and an id, where the id can simply be the position of the contact in the list. Then you can use pass back the id to the controller so that it can be removed from the list.
really superb
ReplyDeleteIf I have a a custom layout for the account detail page, what would I have to do to link to this page setup?
ReplyDeleteYou could include the visualforce in the page layout as it uses the account standard controller. However, this would only show up on the view page which I think is less than ideal from the user experience perspective.
ReplyDeleteHow would you write a test method for this? This is amazing stuff - thanks sir!
ReplyDeleteGood post and Smart Blog
ReplyDeleteThanks for your good information and i hope to subscribe and visit my blog STD Symptoms and more Nodule of Gonorrhea thanks again admin
Hi Bob, Is it possible to implement the same logic for a case where we only would display the records using Outputfield. And only for a specific field want to allow editing and updation on a cross object. To allow editing,am using inlineEditsuport, am not able to work on this,as not able to capture the record ID on which i want to do an update. ANy suggestion would be quite helpful..Thanks
ReplyDeletegood post but i want to know how to use nested repeat like nested for loop'
ReplyDeleteHi Bob
ReplyDeleteI got a requirement that one parent ,multiple child ,multiple grand child to child to insert at a time .Can you have some code to share with me it would be great full .
Hi
ReplyDeletei am getting error on this
Error: AccountAndContactsEditExtensionV1 Compile Error: Invalid constructor syntax, name=value pairs can only be used for SObjects at line 92 column 24
but it is getting from today on wards yesterday it successfully working
AND
my requirement is create account page detail and click to create new contact it will save in pageblocktable format please help me in this issue
harisramachandruni04@gmail.com