Saturday, 26 March 2011

Edit Parent and Child Records with Visualforce - Part 1

One area where, in my opinion, Visualforce can really improve the user experience is editing parent and child records in a single page.  Think about how many clicks are required to edit and save each contact associated with an account on a one by one basis.

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.

11 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. while 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?


    list lstcons = new list();
    public list getcontacts(){
    contact obj = new contact();
    lstcons.add(obj);
    }

    ReplyDelete
  3. 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.

    ReplyDelete
  4. 1. 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.
    2.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....

    ReplyDelete
  5. 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.

    I 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.

    ReplyDelete
  6. If I have a a custom layout for the account detail page, what would I have to do to link to this page setup?

    ReplyDelete
  7. You 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.

    ReplyDelete
  8. How would you write a test method for this? This is amazing stuff - thanks sir!

    ReplyDelete
  9. Good post and Smart Blog
    Thanks for your good information and i hope to subscribe and visit my blog STD Symptoms and more Nodule of Gonorrhea thanks again admin

    ReplyDelete
  10. 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

    ReplyDelete