https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/scripts/shAutoloader.js

Friday, 29 April 2011

Field Level Error Messages with Visualforce - Part 2

In Part 1 of this topic, we looked at how to add field level error messages to input fields on a Visualforce page.  Things are a little more complicated if you aren't using input fields on the page - for example, if you are using input text components backed by properties from a wrapper class.  In this situation, the styling and conditional rendering of error messages must be handled by the page.

As we want to replicate the behaviour when using input fields, the first thing to look at is the styling of errors on input fields.  Viewing the source of the page shows that the style class of the input is changed to 'error', which places a red border around the input box.  This is followed by a div containing the error message:

<div class="errorMsg"><strong>Error:</strong> Either email or phone must be defined</div>

So this should be pretty straightforward to replicate.  First, we need a wrapper class where each field can have an associated error message.  Note that this is by no means the most efficient or elegant wrapper class for this purpose, but it makes it easier to understand what is happening on the page.

public class Wrapper
{
 public String FirstName {get; set;}
 public String MiddleName {get; set;}
 public String LastName {get; set;}
 public String Email {get; set;}
  
 public String FirstNameError {get; set;}
 public String MiddleNameError {get; set;}
 public String LastNameError {get; set;}
 public String EmailError {get; set;}
  
 public Wrapper()
 {
  FirstNameError='';
  MiddleNameError='';
  LastNameError='';
  EmailError='';
 }
}

The validation in this case is that one of first or middle name must be defined, so the validation in the save method is:

for (Wrapper wrap : wrappers)
{
 if ( ((null==wrap.FirstName) || (wrap.FirstName.trim().length()==0)) &&
      ((null==wrap.MiddleName) || (wrap.MiddleName.trim().length()==0)) )
 {
  wrap.FirstNameError='Either first name or middle name must be defined';
  wrap.MiddleNameError='Either first name or middle name must be defined';
 }
}

Finally, the Visualforce markup to display the error if necessary:

<apex:column headerValue="First Name"> 
   <apex:inputText value="{!wrapper.FirstName}" rendered="{!LEN(wrapper.FirstNameError)==0}"/>
   <apex:outputPanel rendered="{!LEN(wrapper.FirstNameError)!=0}">
     <apex:inputText styleClass="error" value="{!wrapper.FirstName}"/>
     <div class="errorMsg"><strong>Error:</strong>&nbsp;{!wrapper.FirstNameError}</div>
   </apex:outputPanel>  
</apex:column>

If the length of the error message associated with the wrapper property is zero (i.e. there is no error), then the input component is displayed as normal.  However, if the length of the error message is greater than zero, a composite output panel is displayed that styles the input component as an error and the error message is output beneath.

I'm duty bound to point out that using the Salesforce styles 'error' and 'errorMsg' is not officially recommended, as Salesforce may choose to change their styling at any point in time.  If I were to use this in a production system, I'd recreate (i.e. copy and paste!) the Salesforce styles in my own CSS include.

And here's the page displaying the error messages in the desired style:

Friday, 22 April 2011

Field Level Error Messages with Visualforce - Part 1

A question came up on the Visualforce discussion board this week asking how to display validation error messages for particular fields on a Visualforce page.

For simple "field required" rules, there's nothing to be done as long as you are using <apex:inputField> components - the field will be decorated with a red bar and an attempt to save a record without the field populated presents a nice error message beneath the field, as shown below:


However, if a validation rule is added - in this case one of email or phone must be defined - you get redirected to a page similar to the following:


Not the greatest user experience in the world.  Clearly handling this via validation rules alone isn't going to work, so the next option that presents itself is to carry out the validation in the controller and send an error message back to the page.  Looking in the Visualforce docs, there is an <apex:pageMessages> component that can do exactly this, we just need to traverse the contacts and add the errors to the page as follows:

Boolean error=false;
Integer idx=1;
for (Contact cand : contacts)
{
 if ( (null==cand.Email || cand.Email.length()==0) &&
      (null==cand.Phone || cand.Phone.length()==0) )
 {
  error=true;
  ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'Line ' + idx + ' - one of email or phone must be defined'));
 }
 idx++;
}

The resulting page is shown below:


Looking better, but still not the nicest page in the world.    What we really want is an error tied to the field itself, much like the required fields.  The secret here is to use the addError method available to sObject fields.  In this case, I've traversed the list of contacts and where both the email and phone are blank, added an error to each of the email and phone field detailing the error:

Boolean error=false;
Integer idx=1;
for (Contact cand : contacts)
{
 if ( (null==cand.Email || cand.Email.length()==0) &&
      (null==cand.Phone || cand.Phone.length()==0) )
 {
  error=true;
  cand.email.addError('Either email or phone must be defined');
  cand.phone.addError('Either email or phone must be defined');
 }
 idx++;
}

This results in exactly the behaviour I am looking for, and even better I don't have to make any changes the page, as its standard Visualforce that displays the error:


In part 2, we'll look at how to replicate this when the inputs aren't tied to an sobject field, for example if they come from a wrapper class.

Sunday, 17 April 2011

Joli OS

I was introduced to Joli OS a couple of months ago. Its a combination of online desktop and browser based operating system that allows you to have a single view of your web applications across multiple machines. If (like me) a lot of your time is spent using web applications (e.g. Salesforce, Google Mail/Apps) then its a good way of reviving ancient machines or giving yourself a fast boot environment.

I find development of Visualforce and Apex a bit clunky using the Salesforce UI - I much prefer to use an integrated development environment.  Unfortunately I haven't received an invite to try out Brain Engine yet, but that looks highly promising for development in the cloud.

For now though, I need another solution. It turns out that Joli OS is built on the Ubuntu Linux distribution, which means that the Force.com IDE can be installed.

Pressing ALT+F1 opens a terminal. Typing "java" at the command prompt shows that Java isn't currently installed, but also shows which packages java is available from and the command to install. I chose to install the IcedTea Open Source JDK.

Once Java is installed, you simply need the standalone Force.com IDE and you are set to go.  I got a few warnings that there may be unavailable features due to using IcedTea, but I haven't noticed anything untoward as yet.

Friday, 15 April 2011

Certified Service Cloud Consultant



Last week I passed the Certified Service Cloud Consultant exam!  I based my revision on this excellent post from Jeff Douglas.  I'm obviously not as experienced as Jeff at the service cloud as I had to put a lot more time into my revision - probably around 40 hours.

I'd add the following to the list of guides/cheatsheets:

  • Salesforce Entitlements Implementation Guide
  • Salesforce Answers Implementation Guide

I found that the vast majority of the questions presented a scenario with a set of requirements and asked you to select this answers that would satisfy this.  Its very important to read the question properly as sometimes a metric or feature is mentioned in the question but as not required or interested!

And finally, many questions were around metrics or KPIs - what to measure and report on.

Please don't ask for or post any exam questions/answers, as this is contravenes the test taker agreement that we all sign up to at the beginning of the exam and devalues the certification.

Tuesday, 12 April 2011

Edit Parent and Child Records with Visualforce - Part 2

In Part 1 of this topic, I covered a simple page that allowed editing of an account and its associated contacts.  At the end of that post I promised an improved version of the page and here it is.

The page markup is shown below:

<apex:page standardController="Account"
           extensions="AccountAndContactsEditExtension"
           tabStyle="Account" title="Prototype Account Edit">
    <apex:pageMessages />
    <apex:form >
        <apex:pageBlock mode="mainDetail">
            <apex:pageBlockButtons location="top">
                <apex:commandButton action="{!cancel}" value="Exit" />
                <apex:commandButton action="{!save}" value="Save" />
                <apex:commandButton action="{!newContact}" value="New Contact" rendered="{!NOT(ISBLANK(Account.id))}" onclick="showpopup(); return false;"/>
            </apex:pageBlockButtons>
            <apex:pageBlockSection title="Account Details" collapsible="true" id="mainRecord" columns="2" >          
            <apex:repeat value="{!$ObjectType.Account.FieldSets.AccountsAndContactsEdit}" var="field">
               <apex:inputField value="{!Account[field]}" />
            </apex:repeat>
            </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:repeat value="{!$ObjectType.Contact.FieldSets.AccountsAndContactsEdit}" var="field">
                     <apex:inputField value="{!contact[field]}" />
                    </apex:repeat>
                              </apex:pageBlockSection>
                           </apex:pageBlockSectionItem>
                        </apex:pageBlockSection>
                       <div style="text-align:center">
                           <apex:commandButton value="Delete This Contact" onclick="idToDelete='{!contact.id}'; showpopup('deletecontent'); return false;"/>
                       </div>
                </apex:repeat>
            </apex:outputPanel>
            
       </apex:pageBlock>
  <apex:outputPanel id="addPanel">
  <apex:actionRegion id="popupRegion">
   <div id="opaque"/>
      <div id="popupcontent" class="popupcontent" style="width: 250px; height: 100px;">
     Please enter the new contact details<br/>
     <apex:outputLabel value="First Name: "/><apex:inputText id="newfirst" value="{!newContactFirstName}"/><br/>
    <apex:outputLabel value="Last Name: "/><apex:inputText id="newlast" value="{!newContactLastName}"/>
    <br/>
        <apex:commandButton id="cancelBtn" value="Cancel" onclick="hidepopup(); return false;"/>
       <apex:commandButton id="confirmBtn" action="{!newContact}" value="Create" rerender="contactList, addPanel" onclick="hidepopup();" status="working"/>
      </div>
   </apex:actionRegion>
   </apex:outputPanel>
  <apex:actionRegion id="deleteRegion">
      <div id="deletecontent" class="popupcontent" style="width: 250px; height: 100px;">
     Are you sure you wish to delete contact?
    <br/>
        <apex:commandButton id="cancelDelBtn" value="Cancel" onclick="hidepopup('deletecontent'); return false;"/>
       <apex:commandButton id="confirmDelBtn" value="Delete" rerender="contactList" onclick="hidepopup('deletecontent'); alert('Deleting contact ' + idToDelete); deleteContact(idToDelete); return false;" status="working"/>
      </div>
   </apex:actionRegion>
      <apex:actionFunction name="deleteContact" action="{!deleteContact}" rerender="contactList" status="working">
         <apex:param name="contactIdent" value="" assignTo="{!chosenContactId}"/>
      </apex:actionFunction>
    </apex:form>        
   <div id="workingcontent" class="popupcontent" style="width:150px; height:50px; margin-top:-100px; marginleft:-100px">
        <p align="center" style='{font-family:"Arial", Helvetica, sans-serif; font-size:20px;}'><apex:image value="/img/loading.gif"/>&nbsp;Please wait</p>
   </div>

  <apex:actionStatus id="working" onstart="showpopup('workingcontent');" onstop="hidepopup('workingcontent');" />
  <script>
   function showpopup(popupname)
   {
      var name="popupcontent";
      if (popupname)
      {
         name=popupname;
      }
      var popUp = document.getElementById(name);
      popUp.style.display = "block";
      document.getElementById('opaque').style.display='block';
   }
   
   function hidepopup(popupname)
   {
      var name="popupcontent";
      if (popupname)
      {
         name=popupname;
      }
      var popUp = document.getElementById(name);
      popUp.style.display = "none";
      document.getElementById('opaque').style.display='none';
   }
   
   var idToDelete;
   
  </script>
  <style>
  .popupcontent{
   position: fixed;
   top: 50%;
   left: 50%;
   margin-top: -100px;
   margin-left: -100px;
   display: none;
   overflow: auto;
   border:1px solid #CCC;
   background-color:white;
   border:3px solid #333;
   z-index:100;
   padding:5px;
   line-height:20px;
   font-size: 14px;
}
#opaque {
    position: fixed;
    top: 0px;
    left: 0px;
    width: 100%;
    height: 100%;
    z-index: 1;
    display: none;
    background-color: gray;
    filter: alpha(opacity=30);
    opacity: 0.3;
    -moz-opacity:0.3;
    -khtml-opacity:0.3
}
* html #opaque {
    position: absolute;
}
  </style>
</apex:page>

While it looks like the page is a good deal more complex, most of the additional markup is layered "popup" content or styling. Walking through the highlights we have:

<apex:commandButton action="{!newContact}" value="New Contact" rendered="{!NOT(ISBLANK(Account.id))}" onclick="showpopup(); return false;"/>

The New Contact command button now invokes some javascript to popup a layer. The user can enter the first and last name for the new contact and continue to create, or cancel out. The user's view is shown below- a definite improvement on the original which simply created a new contact called "Change Me".



Next up is the account detail - as threatened, this has been altered to use a field set rather than a hardcoded set of input fields - much more flexible:

<apex:repeat value="{!$ObjectType.Account.FieldSets.AccountsAndContactsEdit}" var="field">
     <apex:inputField value="{!Account[field]}" />
</apex:repeat>

Same goes for the contacts:

<apex:repeat value="{!$ObjectType.Contact.FieldSets.AccountsAndContactsEdit}" var="field">
 <apex:inputField value="{!contact[field]}" />
</apex:repeat>



Pressing the delete key caused an immediate delete of the contact - not so good if the user hit the button by mistake. Again, I've utilised javascript to popup a layer asking for confirmation.

<apex:commandButton value="Delete This Contact" onclick="idToDelete='{!contact.id}'; showpopup('deletecontent'); return false;"/>

Note that I've had to capture the id that has been chosen for deleting into a javascript variable - idToDelete. This used to be passed to the controller via an apex:param component on the commandbutton, but as the form isn't submitted until the user confirms, it has to be retained and passed as a parameter from the Delete button when the user confirms.

As seen by the user:



Finally, there are the popup layers. Note that each is contained in its own actionregion tag - this ensures that only the additional information captured in the layer is submitted back, rather than the account specific information etc.

<apex:outputPanel id="addPanel">
  <apex:actionRegion id="popupRegion">
   <div id="opaque"/>
      <div id="popupcontent" class="popupcontent" style="width: 250px; height: 100px;">
     Please enter the new contact details<br/>
     <apex:outputLabel value="First Name: "/><apex:inputText id="newfirst" value="{!newContactFirstName}"/><br/>
    <apex:outputLabel value="Last Name: "/><apex:inputText id="newlast" value="{!newContactLastName}"/>
    <br/>
        <apex:commandButton id="cancelBtn" value="Cancel" onclick="hidepopup(); return false;"/>
       <apex:commandButton id="confirmBtn" action="{!newContact}" value="Create" rerender="contactList, addPanel" onclick="hidepopup();" status="working"/>
      </div>
   </apex:actionRegion>
   </apex:outputPanel>

The controller hasn't changed an awful lot - there's just a couple of additional properties to capture the first and last name of the contact:

public class AccountAndContactsEditExtension {

    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 String newContactFirstName {get; set;}
    public String newContactLastName {get; set;}
    
    public AccountAndContactsEditExtension()
    {
    }
    
    public AccountAndContactsEditExtension(ApexPages.StandardController stdCtrl)
    {
     std=stdCtrl;
    }
    
    public Account getAccount()
    {
     return (Account) std.getRecord();
    }

 public SObject getSobject()
 {
  return std.getRecord();
 }
     
    private boolean updateContacts()
    {
        boolean result=true;
        if (null!=contacts)
           {
              // TODO: should work out what's changed and then save, easier to update everything for prototype
           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.AccountAndContactsEdit;
     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=newContactFirstName, LastName=newContactLastName, AccountId=getAccount().id);
          insert cont;
        
          newContactFirstName=null;
          newContactLastName=null;
          contacts=null;
       }
    }
    
    public void deleteContact()
    {
       if (updateContacts())
       {
          if (null!=chosenContactId)
          {
             Contact cont=new Contact(Id=chosenContactId);
              delete cont;
       
              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;
    }
}

and that's all there is to it - a real payoff in a much improved user experience with a small amount of development effort.

Friday, 1 April 2011

Dynamic Visualforce Bindings and AddFields Method

One of the new features in Spring 11 is Dynamic Visualforce Bindings, which allows you to determine the fields to display at runtime rather than at compile time.

As part of this, there's a new method on the ApexPages.StandardController class - addFields - that allows you to add fields in to the record being managed by name.  Below is an example page and controller that use this:

Page:

<apex:page standardController="Account" extensions="AddFieldsController">
   <apex:outputField value="{!Account['Name']}" /><br/>
   <apex:outputField value="{!Account['BillingCity']}" /><br/>
</apex:page>

Controller:

public class AddFieldsController 
{
 public AddFieldsController(ApexPages.StandardController stdCtrl)
 {
  stdCtrl.addFields(new List<String>{'Name', 'BillingCity'});
  Account acc=(Account) stdCtrl.getRecord();
 }
}

This all works correctly and the name and City is dynamically retrieved and displayed on the page.

Introducing the following test into the equation:

public static testMethod void testController()
{
 Account acc=new Account(name='Name');
 insert acc;
 AddFieldsController controller=new AddFieldsController(new ApexPages.StandardController(acc));
 System.debug('#### account = ' + acc);
}

causes an error to be reported - |System.SObjectException: You cannot call addFields after you've already loaded the data. This must be the first thing in your constructor

This is unexpected, firstly as it works fine when run live, and secondly as the addFields is the first thing in my constructor. So it looks like this is either a bug or something missing from the documentation.

Luckily this is easily solved - first I change the constructor so that it doesn't execute the addFields method if I am running a test:

public AddFieldsController(ApexPages.StandardController stdCtrl)
{
 if (!Test.isRunningTest())
 {
  stdCtrl.addFields(new List<String>{'Name', 'BillingCity'});
 }
 Account acc=(Account) stdCtrl.getRecord();
}


And secondly I change my test so that it populates every field the controller is expecting to add in:

public static testMethod void testController()
{
 Account acc=new Account(name='Name', BillingCity='Test');
 insert acc;
 AddFieldsController controller=new AddFieldsController(new ApexPages.StandardController(acc));
 System.debug('#### account = ' + acc);
}