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.

Saturday, 19 March 2011

Persisting List Edits in Visualforce

A topic that appears semi-regularly on the Visualforce discussion boards is editing the contents of a list of sobjects and how to persist that information into the Salesforce database.  There's often confusion around how to submit the changes back to the Visualforce controller.

This is a good example of how easy it is to capture complex form data from a Visualforce page into the controller.  Essentially as long as the list is a controller property that is part of the viewstate, there's nothing that has to be done to submit the changes back to the page and very little to save the changes back to the database.

Here's the output of a small Visualforce page that allows basic information for an account and up to 5 associated contacts to be edited in one go.


The contacts information is stored in a list property from the controller, and output via an apex:repeat tag:

<apex:repeat value="{!contacts}" var="Contact">
   <apex:inputField value="{!Contact.FirstName}"/>
   <apex:inputField value="{!Contact.LastName}"/>
</apex:repeat>

As you can see, there's very little to this - simply iterate the list of contacts and for each element, render inputs for the first and last name.  From the controller perspective, there's also even less - the list of contacts is a property that is initialised when the controller is constructed:

// the associated contacts
   public List<Contact> contacts {get; set;}
   
   public AccountContactsListExample(ApexPages.StandardController stdCtrl)
   {
     std=stdCtrl;
     contacts=[select id, FirstName, LastName, Email from Contact where accountid=:std.getId() order by firstname asc limit 5];
    }

In terms of hooking up the page and controller, that's all there is to it.  Each input field on the page is bound to a field from a Contact sobject in the list.  Changes that are made on the page will be reflected in the cached contacts list and are then available to be processed server side.

Here's the full page, with the button to allow saving of the changes:

<apex:page standardController="Account" extensions="AccountContactsListExample">
  <apex:form >
  <apex:outputText value="{!message}" rendered="{!LEN(message)>0}"/>
   <apex:pageBlock title="Account Detail">
      <apex:pageBlockSection title="Account">
            <apex:inputField value="{!Account.Name}"/>
            <apex:inputField value="{!Account.Description}"/>
      </apex:pageBlockSection>
      <apex:pageBlockSection title="Contacts">
         <apex:repeat value="{!contacts}" var="Contact">
            <apex:inputField value="{!Contact.FirstName}"/>
            <apex:inputField value="{!Contact.LastName}"/>
         </apex:repeat>
      </apex:pageBlockSection>
  </apex:pageBlock>
   <apex:commandButton value="Save" action="{!save}"/>
  </apex:form>
</apex:page>

and here's the full controller. The save method persists the changes to the database, simply by executing an update on the list of contacts.

public class AccountContactsListExample 
{
    private ApexPages.StandardController std;
    
    public String message{get;set;}
    
    // the associated contacts
   public List<Contact> contacts {get; set;}
   
    public AccountContactsListExample(ApexPages.StandardController stdCtrl)
    {
     std=stdCtrl;
     contacts=[select id, FirstName, LastName, Email from Contact where accountid=:std.getId() order by firstname asc limit 5];
    }

    public PageReference save()
    {
     // first save the account
     std.save();
     
     // then save the contacts
     update contacts;
     
     PageReference result=ApexPages.currentPage();
     result.setRedirect(true);
     
     return result;
    }
    
}

This also demonstrates one way to allow editing of a parent (account) record and its children (contacts).

Saturday, 12 March 2011

Deactivating Users that are Running Dashboard User

A persistent issue when deactivating users is finding out some time down the line that they were the running user for a dashboard and being presented with the familiar error message:


And yes, I know its possible to run a custom report on dashboards and specify filter criteria to check the running user, but that seems a little clunky and requires too many clicks for my liking.

The next option I considered was a before delete trigger on the User object that would block the deactivation if the user was a dashboard running user.  I've never been keen on these sorts of triggers as I don't think its the greatest experience - you complete all the work and then at the last minute the platform bounces the request.  Plus if a user needs to be deactivated quickly and is the running user for a number of dashboards, a trigger would slow the process down.

What I really wanted was a way to check if there were going to be problems before I deactivate.

The first part to the solution was to create a Visualforce page to display any dashboards that would be affected.  Its a pretty straightforward page to display either a message that the user isn't a dashboard running user, or to list the affected dashboards:

<apex:page extensions="UserDashboardController" standardcontroller="User">
   <apex:pageblock title="User Dashboard Information">
 <apex:outputpanel rendered="{!NOT(UserNoDashboards)}">
        <apex:outputtext>
           User {!User.username} is the running user for the following dashboards:


        </apex:outputtext>
  <apex:pageblocktable value="{!userDashboards}" var="db">
   <apex:column headervalue="Dashboard">
      <apex:outputlink target="_blank" value="/{!db.id}">{!db.title}</apex:outputlink>
   </apex:column>
  </apex:pageblocktable>
 </apex:outputpanel>
 <apex:outputpanel rendered="{!userNoDashboards}">
  User {!User.username} is not a running user of any dashboards.
 </apex:outputpanel>
  </apex:pageblock>
</apex:page>

The page is backed by the User standard controller and an extension controller that adds a couple of methods for retrieving dashboard information:

public class UserDashboardController 
{
 User theUser;
 
 public UserDashboardController(ApexPages.StandardController std)
 {
  theUser=(User) std.getRecord();
 }
 
 public List<Dashboard> getUserDashboards()
 {
  List<Dashboard> dbs=[select id, Title from Dashboard 
                       where Type='SpecifiedUser' 
                         and RunningUserId=:theUser.id];
                  
  
  return dbs;
 }
 
 public Boolean getUserNoDashboards()
 {
  return getUserDashboards().isEmpty();
 }

}

Accessing the page with the id of the user I'm considering deactivating shows that they are the running user for one dashboard:


Unfortunately, Visualforce pages can't be embedded into User page layouts, so I need another mechanism to gain access to the Visualforce page. The solution is to create a custom link (Setup -> Customize -> Users -> Custom Links) that opens the Visualforce page in a new window. The configuration for my Custom Link is shown below:



I can then add this custom link to my User page layout and the next time I'm considering deactivating a user, I simply navigate to the user's detail record, click the link and the details appear in a popup window.


It would be quite simple to extend this to output other information pertinent to deactivating a user - the accounts that the user is the owner of for example.

Saturday, 5 March 2011

Visualforce Dynamic Map Bindings

A new feature in Spring 11 is Dynamic Visualforce Bindings.  Most of the documentation on this is around determining the record fields to display at runtime rather than compile time.  There's also a small section about using this for Lists and Maps.  Lists have been usable in a variety of ways such as apex:repeat and apex:pageBlockTable tags, but being able to retrieve the value from a map based on a key in a Visualforce page is something I've been working around for a while.  The workarounds haven't been unwieldy or difficult, but they've always been a compromise.

I've created a small example to demonstrate the usefulness of this new feature.  This is a page that displays either all accounts in the system:



 or just those that begin with a particular letter,



depending on the option that the user selects from a list.  Prior to Spring 11, I'd have to generate the list of accounts based on the user's selection server side.  As I said earlier, not a lot of work but I'd still prefer not to have to do it.

With the new feature, I can simply set up all my data in a map when constructing the page controller and then dynamically render the appropriate value from the map, based on the key, which is the user's selection.

The page markup is shown below:

<apex:page controller="DynamicBindingsMapExample">
  <apex:form >
    <apex:actionFunction name="redraw_accounts" rerender="accs" status="status"/>
    <apex:pageBlock title="Criteria">
       <apex:outputLabel value="Starting Letter"/>
       <apex:selectList value="{!selectedKey}" size="1" onchange="redraw_accounts()">
          <apex:selectOptions value="{!keys}" />
       </apex:selectList>
    </apex:pageBlock>
    <apex:pageBlock title="Accounts">
       <apex:actionstatus id="status">
          <apex:facet name="start"/>
          <apex:facet name="stop">
             <apex:outputPanel id="accs">
                <apex:pageBlockTable value="{!accountsMap[selectedKey]}" var="acc">
                   <apex:column value="{!acc.name}"/>
                   <apex:column value="{!acc.BillingStreet}"/>
                   <apex:column value="{!acc.BillingCity}"/>
                   <apex:column value="{!acc.BillingPostalCode}"/>
                </apex:pageBlockTable>
             </apex:outputPanel>
          </apex:facet>
       </apex:actionstatus>
    </apex:pageBlock>
  </apex:form>
</apex:page>


The accounts are made available to the page via a map property on the controller that associates a letter (or the String 'All') with a list of Accounts starting with that letter (or every account in the case of 'All').

public Map<String, List<Account>> accountsMap {get; set;}

The user's selection is stored in the selectedKey controller property and this is used to extract the appropriate accounts from the map in the pageblocktable component:

<apex:pageBlockTable value="{!accountsMap[selectedKey]}" var="acc">

When the user's selection changes, the redraw actionfunction is executed to redraw the pageBlockTable containing the accounts. Note that there is no action attribute specified, as I haven't had to write any server side code to change the accounts displayed - everything is handled client side.

<apex:actionFunction name="redraw_accounts" rerender="accs" status="status"/>



Update 12/02/2012 - as requested by Raj in the comments, here is the controller class:

public class DynamicBindingsMapExample 
{
    public Map> accountsMap {get; set;}
    public List keys {get; set;}
    public String selectedKey {get; set;}
    public Map accsByName {get; set;}
    
    public Set getMapKeys()
    {
    	return accountsMap.keySet();
    }
    
    public DynamicBindingsMapExample()
    {
    	accsByName=new Map();
    	List sortedKeys=new List();
    	accountsMap=new Map>();
    	accountsMap.put('All', new List());
    	List accs=[select id, Name, BillingStreet, BillingCity, BillingPostalCode 
    	                    from Account
    	                    order by Name asc];
    	                    
    	                    
    	for (Account acc : accs)
    	{
    		accountsMap.get('All').add(acc);
    		String start=acc.Name.substring(0,1);
    		List accsFromMap=accountsMap.get(start);
    		if (null==accsFromMap)
    		{
    			accsFromMap=new List();
    			accountsMap.put(start, accsFromMap);
    		}
    		accsFromMap.add(acc);
    		accsByName.put(acc.name, acc);
    	}
    	
    	keys=new List();
    	for (String key : accountsMap.keySet())
    	{
    		if (key != 'All')
    		{
    			sortedKeys.add(key);
    		}
    	}
    	sortedKeys.sort();
    	sortedKeys.add(0, 'All');
    	
    	for (String key : sortedKeys)
    	{
    		keys.add(new SelectOption(key, key));
    	}
    	
    	selectedKey='All';
    }
}