Demo App – Salesforce on Heroku (Java) with Play! Framework

September 26th, 2011

At Dreamforce a couple of weeks ago, Heroku announced the public beta for the Play! Framework on their cedar stack. So if you love Java but hate the pain associated with developing web apps, then the Play Framework is for you. It’s essentially a Ruby on Rails framework for Java. It makes Java development fun again!

To get started, check out the Play! on Heroku blog post as it has everything you need to get started with a simple Hello World app. There’s also a Getting Started with Play! on Heroku/Cedar article for more info.

Once again, this is a demo Java app running on Heroku using the Play! Framework. It uses the Force.com Web Service Connector (WSC) and the Partner jar to connect to a DE salesforce.com org. It uses the web services API to query for records, retrieve records to display, create new records and update existing ones. It should be good fodder for anyone wanting to start out with Play! and Force.com.

You can run the app for yourself here.

All of the code for this app is at github so fork awey. I’ve pulled out some of the more important parts of the app for a quick peek. There’s also an overview of one of the important classes to give you something to look at.

app/controllers/Account.java

The account controller contains all of the business logic for integration with Force.com and then packages up the returns for the views.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package controllers;
 
import play.*;
import play.mvc.*;
import java.util.*;
 
import models.*;
import service.*;
import com.sforce.soap.partner.*;
import com.sforce.ws.ConnectionException;
import com.sforce.soap.partner.sobject.SObject;
 
public class Account extends Controller {
 
  public static void create() {
    render();
  }
 
  // create the record in salesforce
  public static void createSubmit() {
 
    SaveResult[] results = null;
 
    // populate the new opportunity
    SObject a = new SObject();
    a.setType("Account");
    a.setField("Name", params.get("name"));
    a.setField("BillingCity", params.get("city"));
    a.setField("BillingState", params.get("state"));
    a.setField("Phone", params.get("phone"));
    a.setField("Website", params.get("website"));
 
    SObject[] accounts = {a};
 
    // get a reference to the connection
    PartnerConnection connection = ConnectionManager.getConnectionManager().getConnection();
 
    try {
      results = connection.create(accounts);
 
      // check for any errors
      if ( results[0].isSuccess() ) {
         System.out.println("Successfully created Account: " + results[0].getId());
      } else {
         System.out.println("Error: could not create Accout: " + results[0].getErrors()[0].getMessage());
      }
 
    } catch (ConnectionException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
 
    redirect("/account/"+results[0].getId() );
  }
 
  // fetch the account and all related opportunities to display
  public static void display() {
 
    if (params.get("id") != null) {
 
      // add the account id to the array to be retrieved
      String[] accountIds = { params.get("id") };
 
      SObject[] accounts = null;
      QueryResult result = null;
 
      // get a reference to the connection
      PartnerConnection connection = ConnectionManager.getConnectionManager().getConnection();
 
      try {
        accounts = connection.retrieve("Id, Name, Phone, BillingCity, BillingState, Website","Account", accountIds);
        result = connection.query("Select id, name, stagename, amount, closeDate, probability, ordernumber__c from Opportunity where AccountId = '"+params.get("id")+"'");
      } catch (ConnectionException e) {
        e.printStackTrace();
      } catch (NullPointerException npe) {
        System.out.println("NullPointerException: "+npe.getCause().toString());
      }
 
      renderArgs.put("account", accounts[0]);
      renderArgs.put("opportunities", result.getRecords());
 
    }
 
    render();
  }
 
  // fetch the account so we can show the edit form with data
  public static void edit() {
 
    if (params.get("id") != null) {
 
      // add the account id to the array to be retrieved
      String[] accountIds = { params.get("id") };
      SObject[] accounts = null;
 
      // get a reference to the connection
      PartnerConnection connection = ConnectionManager.getConnectionManager().getConnection();
 
      try {
        accounts = connection.retrieve("Id, Name, Phone, BillingCity, BillingState, Website","Account", accountIds);
      } catch (ConnectionException e) {
        e.printStackTrace();
      } catch (NullPointerException npe) {
        System.out.println("NullPointerException: "+npe.getCause().toString());
      }
 
      renderArgs.put("account", accounts[0]);
 
    }
 
    render();
  }
 
  // update the record in salesforce
  public static void editSubmit() {
 
    SaveResult[] results = null;
 
    // populate the account to update
    SObject a = new SObject();
    a.setType("Account");
    a.setId(params.get("id"));
    a.setField("Billingcity", params.get("BillingCity"));
 
    SObject[] accounts = {a};
 
    // get a reference to the connection
    PartnerConnection connection = ConnectionManager.getConnectionManager().getConnection();
 
    try {
      results = connection.update(accounts);
 
      // check for any errors
      if ( results[0].isSuccess() ) {
         System.out.println("Successfully updated Account: " + results[0].getId());
      } else {
         System.out.println("Error: could not update Account: " + results[0].getErrors()[0].getMessage());
      }
 
    } catch (ConnectionException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
 
    redirect("/account/"+params.get("id") );
  }
 
}

app/views/display.html

The account display view displays the account details and any related opportunities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#{extends 'main.html' /}
#{set title:'Account Details' /}
 
<span class="nav"><a href="/opportunity">Search</a></span><p/>
<span class="title">Account Display</span>
<p/>
 
<table id="fancytable">
  <tr>
    <td class="label">Name</td>
    <td>${ account.getField("Name") }</td>
  </tr>
  <tr>
    <td class="label">City</td>
    <td>${ account.getField("BillingCity") }</td>
  </tr>
  <tr>
    <td class="label">State</td>
    <td>${ account.getField("BillingState") }</td>
  </tr>
  <tr>
    <td class="label">Phone</td>
    <td>${ account.getField("Phone") }</td>
  </tr>
  <tr>
    <td class="label">Website</td>
    <td>${ account.getField("Website") }</td>
  </tr>  
</table>
 
<br><a href="/opportunity/create/${ account.getField("Id") }">Create a new Opportunity</a> 
or <a href="/account/${ account.getField("Id") }/edit">edit this Account</a><p/>  
 
%{ if (opportunities.length > 0) { }%
 
  <p/><span class="heading">Opportunities for ${ account.getField("Name") }</span><br><p/>
 
  <table id="fancytable">
  <tr>
    <td class="label">Name</td>
    <td class="label">Amount</td>
    <td class="label">Stage</td>
    <td class="label">Probability</td>
    <td class="label">Close Date</td>
    <td class="label">Order</td>
  </tr>
  #{list items:opportunities, as:'o' }
    <tr>
      <td nowrap>${ o.getField("Name") }</td>
      <td>$${ o.getField("Amount") }</td>
      <td>${ o.getField("StageName") }</td>
      <td>${ o.getField("Probability") }</td>
      <td>${ o.getField("CloseDate") }</td>
      <td>${ o.getField("OrderNumber__c") }</td>
    </tr>
  #{/list}
  </table>
 
%{ } else { }%
  <p/><span class="heading">No Opportunities found for ${ account.getField("Name") }</span>
%{ } }%

Categories: Heroku, Salesforce

3 Comments

Video – Connect Your Clouds with Force.com from Dreamforce 11

September 24th, 2011

Here’s the video from my Dreamforce 11 session!

Abstract: As more and more applications move to the Cloud, Force.com integration is no longer just about connecting with on-premise systems and databases. You now also need to effectively connect with other cloud platforms. This session will show you how to integrate your Force.com application with other cloud platforms such as Amazon’s S3 storage, Google App Engine, and Microsoft Azure. The session will feature demos and code walkthroughs of the various cloud integrations.

Categories: Salesforce

No Comments

Twilio Client for Force.com BETA

September 24th, 2011

Twilio just released a new beta version of their Apex toolkit. You can fork it here on GitHub. It includes some new stuff, mainly the Capabilities class for Twilio Client. The majority of it was written by Kyle Roche so it’s pretty top notch stuff.

If you are interested in getting your hands dirty with the new toolkit, we have a Twilio Client for Salesforce Customer Portal development challenge over at CloudSpokes with $1000 up for grabs.

Categories: Salesforce, Twilio

No Comments

Session Recap – Connect Your Clouds with Force.com

September 3rd, 2011

Dreamforce 11 is over and I can finally catch my breath. Despite the un-godly start time of 8:30am Friday morning, my session went fairly well. I had a good turnout and some interesting questions.

I’ve posted the PDF of my deck to the session’s Chatter group, but it may be easier to access them from my blog. Here is the PDF and PPT version of my presentation along with the source code.

Categories: Salesforce

2 Comments

It’s Official! Heroku Loves Java

August 26th, 2011

It’s official! The Heroku blog proudly announced a couple of hours ago that “Java is the fourth official language available on the Cedar stack”. There’s even a nifty little “heroku for java in 5 minutes tutorial” that you can walk through to get up and running.

Categories: Heroku, Salesforce

No Comments

Build a Bulk Emailer for Salesforce with App Engine

August 25th, 2011

Sometimes you just want to send a crapload of email from Salesforce.com. However, like every PaaS platform there are limits baked into the multi-tenant environment so you don’t stomp on other tenants’ resources. Salesforce.com limits you to 2000 emails per day for each Salesforce license. So if you don’t have a lot of Salesforce licenses or a different kind of license, you may be out of luck if you want to send out large volumes. There are a few AppExchange products but they seem more targeted towards marketing purposes.

Google App Engine may be a good solution in this case. With Google App Engine quotas you get 7,000 Mail API calls per day free and can bump that up as high as 1.7M with a paid account.

Here’s how to roll your own basic bulk emailer using Google App Engine. Take a look at the video below, but it essentially queries Force.com for records, uses the Google Mail Java API to send out individual emails and then send an administrator a notification via Jabber (Google Talk). You can schedule your application (essentially a Servlet) to run on a timed basis using the cron service.

All of this code is at this GitHub repo.

Most of the important code is in the MailServlet class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.jeffdouglas.emailer;
 
[removed imports]
 
/**
 * MailServlet.java - a simple, schedulable servlet for sending mail
 * with salesforce.com
 * @author Jeff Douglas
 * @version 1.0
 * @see http://code.google.com/appengine/docs/java/mail/overview.html
 * for more details on using Mail with App Engine
 */
 
@SuppressWarnings("serial")
public class MailServlet extends HttpServlet {
 
    private static final Logger logger = Logger.getLogger(ConnectionManager.class.getName());
    private String jabberRecipient = "jeffdonthemic@gmail.com";
 
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
 
        resp.setContentType("text/html");
        String mailerMsg = "No contact found to email!!";
        QueryResult result = null;
 
        // get a reference to the salesforce connection
        PartnerConnection connection = ConnectionManager.getConnectionManager().getConnection();
 
        try {
            // query for contacts based upon some criteria -- emailNotSent boolean
            result = connection.query("Select Id, FirstName, LastName, Email " +
            		"FROM Contact Where Email = 'jeff@jeffdouglas.com' Limit 1");
        } catch (ConnectionException e) {
            e.printStackTrace();
            logger.severe(e.getCause().toString());
        } catch (NullPointerException npe) {
            npe.printStackTrace();
            logger.severe(npe.getCause().toString());
        }
 
        // if records were returned then send out email
        if (result != null) {
 
          for (SObject contact : result.getRecords()) {
 
            // construct the 'name' for the email recipient
            String contactName = contact.getField("FirstName").toString() + " " + 
              contact.getField("LastName").toString();
 
            logger.info("Sending emil to " + contactName + " at " + 
              contact.getField("Email").toString());
 
            /// send the email
            mailerMsg = sendMail(contact.getField("Email").toString(), contactName);
 
            // send a jabber notification of the status
            sendJabberNotification(jabberRecipient, mailerMsg);
 
          }
 
          // TODO - make a call back into salesforce and update these records
          // as having their emails sent. Implementation is up to you.
 
        } else {
          logger.warning("No results returned from salesforce");
        }
 
        resp.getOutputStream().println(mailerMsg);
 
    }  
 
    /**  
     * Sends an email
     * @param toAddress the email address of the recipient
     * @param toName the name that appears for the recipeint in their email client  
     * @return A String representing the status of the email sent  
     */  
    private String sendMail(String toAddress, String toName) {
 
      String msg = "Email sent successfully to " + toAddress;
      Properties props = new Properties();
      Session session = Session.getDefaultInstance(props, null); 
 
      String messageBody = "This is the body of my email";
 
      try {
 
          Message emailMessage = new MimeMessage(session);
          //  must be the email address of an administrator for the application. see docs
          emailMessage.setFrom(new InternetAddress("jeffdonthemic@gmail.com","Jeff Douglas"));
          emailMessage.addRecipient(Message.RecipientType.TO, 
            new InternetAddress(toAddress, toName));
          emailMessage.setSubject("My Email Subject");
          emailMessage.setText(messageBody);
          Transport.send(emailMessage);
 
      } catch (AddressException e) {
          msg = e.toString();
      } catch (MessagingException e) {
          msg = e.toString();
      } catch (UnsupportedEncodingException e) {
          msg = e.toString();
      }
 
      return msg;
    }
 
    /**  
     * Sends a message to any XMPP-compatible chat messaging service (google talk). 
     * See http://code.google.com/appengine/docs/java/xmpp/overview.html
     * for more detils
     * @param recipient the jid of the jabber recipient of the notification
     * @param msgBody the body of the message to be sent  
     */  
    private void sendJabberNotification(String recipient, String msgBody) {
 
      JID jid = new JID(recipient);
 
      com.google.appengine.api.xmpp.Message msg = new MessageBuilder()
          .withRecipientJids(jid)
          .withBody(msgBody)
          .build();
 
      boolean messageSent = false;
      XMPPService xmpp = XMPPServiceFactory.getXMPPService();
 
      if (xmpp.getPresence(jid).isAvailable()) {
          SendResponse status = xmpp.sendMessage(msg);
          messageSent = (status.getStatusMap().get(jid) == SendResponse.Status.SUCCESS);
      }
 
      logger.info("Jabber notifiation sent: " + messageSent);
 
    }
 
}

Categories: Google App Engine, Salesforce

3 Comments

How I’m Getting Ready for Dreamforce

August 23rd, 2011

Categories: Salesforce

1 Comment

Salesforce Trigger when Rollups Summaries Not Possible

August 23rd, 2011

Master-Details relationships in Force.com  are very handy but don’t fit every scenario. For instance, it’s not possible to implement a rollup summary on formula field or text fields. Here’s a small trigger that you can use for a starter for these types of situations. The code for each class is available at GitHub for your forking pleasure.

So here’s the (not very useful) use case. Sales Order is the Master object which can have multiple Sales Order Items (detail object). The Sales Order Item has a “primary” Boolean field and a “purchased country” field. Each time Sales Order Items are inserted or updated, if the Sales Order Item is marked as “primary” then the value of “purchased country” is written into the “primary country” field on the Sales Order. I’m assuming that there can only be one Sales Order Item per Sales Order that is marked as primary. Essentially this is just a quick reference on the Sales Order to see which country is primary on any of the multiple Sales Order Items. Not very useful but illustrative.

The code is broken down into a Trigger and an Apex “handler” class that implements the actual functionality. It’s best practice to only have one trigger for each object and to avoid complex logic in triggers. To simplify testing and resuse, triggers should delegate to Apex classes which contain the actual execution logic. See Mike Leach’s excellent trigger template for more info.

SalesOrderItemTrigger (source on GitHub) - Implements trigger functionality for Sales Order Items. Delegates responsibility to SalesOrderItemTriggerHandler.

1
2
3
4
5
6
7
8
9
10
11
12
13
trigger SalesOrderItemTrigger on Sales_Order_Item__c (after insert, after update) {
 
  SalesOrderItemTriggerHandler handler = new SalesOrderItemTriggerHandler();
 
  if(Trigger.isInsert && Trigger.isAfter) {
    handler.OnAfterInsert(Trigger.new);
 
  } else if(Trigger.isUpdate && Trigger.isAfter) { 
    handler.OnAfterUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap);
 
  }
 
}

SalesOrderItemTriggerHandler (source on GitHub) - Implements the functionality for the sales order item trigger after insert and after update. Looks at each sales order item and if it is marked as primary_item__c then moves the primary_country__c value from the sales order item to the associated sales order’s primary_country__c field.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public with sharing class SalesOrderItemTriggerHandler {
 
  // update the primary country when new records are inserted from trigger
  public void OnAfterInsert(List<Sales_Order_Item__c> newRecords){
    updatePrimaryCountry(newRecords); 
  }
 
  // update the primary country when records are updated from trigger  
  public void OnAfterUpdate(List<Sales_Order_Item__c> oldRecords, 
      List<Sales_Order_Item__c> updatedRecords,  Map<ID, Sales_Order_Item__c> oldMap, 
      Map<ID, Sales_Order_Item__c> newMap){
    updatePrimaryCountry(updatedRecords); 
  }
 
  // updates the sales order with the primary purchased country for the item
  private void updatePrimaryCountry(List<Sales_Order_Item__c> newRecords) {
 
    // create a new map to hold the sales order id / country values
    Map<ID,String> salesOrderCountryMap = new Map<ID,String>();
 
    // if an item is marked as primary, add the purchased country
    // to the map where the sales order id is the key 
    for (Sales_Order_Item__c soi : newRecords) {
      if (soi.Primary_Item__c)
        salesOrderCountryMap.put(soi.Sales_Order__c,soi.Purchased_Country__c);
    } 
 
    // query for the sale orders in the context to update
    List<Sales_Order__c> orders = [select id, Primary_Country__c from Sales_Order__c 
      where id IN :salesOrderCountryMap.keyset()];
 
    // add the primary country to the sales order. find it in the map
    // using the sales order's id as the key
    for (Sales_Order__c so : orders)
      so.Primary_Country__c = salesOrderCountryMap.get(so.id);
 
    // commit the records 
    update orders;
 
  }
 
}

Test_SalesOrderItemTriggerHandler (source on GitHub) - Test class for SalesOrderItemTrigger and SalesOrderItemTriggerHandler. Achieves 100% code coverage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@isTest
private class Test_SalesOrderItemTriggerHandler {
 
  private static Sales_Order__c so1;
  private static Sales_Order__c so2;
 
  // set up our data for each test method
  static {
 
  	Contact c = new Contact(firstname='test',lastname='test',email='no@email.com');
  	insert c;
 
    so1 = new Sales_Order__c(name='test1',Delivery_Name__c=c.id);
    so2 = new Sales_Order__c(name='test2',Delivery_Name__c=c.id);
 
    insert new List<Sales_Order__c>{so1,so2};
 
  }
 
  static testMethod void testNewRecords() {
 
    Sales_Order_Item__c soi1 = new Sales_Order_Item__c();
    soi1.Sales_Order__c = so1.id;
    soi1.Quantity__c = 1;
    soi1.Description__c = 'test';
    soi1.Purchased_Country__c = 'Germany';
 
    Sales_Order_Item__c soi2 = new Sales_Order_Item__c();
    soi2.Sales_Order__c = so1.id;
    soi2.Quantity__c = 1;
    soi2.Description__c = 'test';
    soi2.Purchased_Country__c = 'France';
    soi2.Primary_Item__c = true;
 
    Sales_Order_Item__c soi3 = new Sales_Order_Item__c();
    soi3.Sales_Order__c = so2.id;
    soi3.Quantity__c = 1;
    soi3.Description__c = 'test';
    soi3.Purchased_Country__c = 'Germany';
    soi3.Primary_Item__c = true;
 
    Sales_Order_Item__c soi4 = new Sales_Order_Item__c();
    soi4.Sales_Order__c = so2.id;
    soi4.Quantity__c = 1;
    soi4.Description__c = 'test';
    soi4.Purchased_Country__c = 'Germany';
 
    Sales_Order_Item__c soi5 = new Sales_Order_Item__c();
    soi5.Sales_Order__c = so2.id;
    soi5.Quantity__c = 1;
    soi5.Description__c = 'test';
    soi5.Purchased_Country__c = 'Italy';
 
    insert new List<Sales_Order_Item__c>{soi1,soi2,soi3,soi4,soi5}; 
 
    System.assertEquals(2,[select count() from Sales_Order_Item__c where Sales_Order__c = :so1.id]);
    System.assertEquals(3,[select count() from Sales_Order_Item__c where Sales_Order__c = :so2.id]); 
 
    System.assertEquals('France',[select primary_country__c from Sales_Order__c where id = :so1.id].primary_country__c);
    System.assertEquals('Germany',[select primary_country__c from Sales_Order__c where id = :so2.id].primary_country__c);
 
  }
 
  static testMethod void testUpdatedRecords() {
 
    Sales_Order_Item__c soi1 = new Sales_Order_Item__c();
    soi1.Sales_Order__c = so1.id;
    soi1.Quantity__c = 1;
    soi1.Description__c = 'test';
    soi1.Purchased_Country__c = 'Germany';
 
    Sales_Order_Item__c soi2 = new Sales_Order_Item__c();
    soi2.Sales_Order__c = so1.id;
    soi2.Quantity__c = 1;
    soi2.Description__c = 'test';
    soi2.Purchased_Country__c = 'France';
    soi2.Primary_Item__c = true;
 
    insert new List<Sales_Order_Item__c>{soi1,soi2}; 
 
    // assert that the country = France
    System.assertEquals('France',[select primary_country__c from Sales_Order__c where id = :so1.id].primary_country__c);
 
    List<Sales_Order_Item__c> items = [select id, purchased_country__c from Sales_Order_Item__c 
      where Sales_Order__c = :so1.id and primary_item__c = true];
    // change the primary country on the sales order item. should trigger update
    items.get(0).purchased_country__c = 'Denmark';
 
    update items;
    // assert that the country was successfully changed to Denmark
    System.assertEquals('Denmark',[select primary_country__c from Sales_Order__c where id = :so1.id].primary_country__c);
 
  }
 
}

Categories: Apex, Code Sample, Salesforce

4 Comments

Win $1000 for your Artwork!

August 19th, 2011

Popart

Hop on over to http://popart.appirio.com for a chance to win $1000. Here’s how it works. Enter a caption in the pop art image of your choice and submit the entry. It will create an image and send it over to our Pop Art Picassa Gallery. Get people to vote for your images and you can win tickets to our invite only Dreamforce SFMOMA party or even $1000!!!

Check out some of the submitted artwork. Pretty funny stuff.

Popart was created from the Art Meets Text Mashup CloudSpokes contest! Created by the Force.com community for the Force.com community.

Categories: Appirio, Salesforce

No Comments

Roll Your Own Salesforce “Lookup” Popup Window

August 12th, 2011

Let’s talk about the standard salesforce.com “lookup” popup window for a few minutes. You know what I’m talking about.. this button right here

Custom lookup button

It’s a handy little button that pops up whenever you need to search for related records. It does a pretty good job but it has some serious drawbacks:

  1. It’s virtually impossible to modify your search criteria. What if you want your users to do some crazy search based upon custom logic or search a field that is not typically available? Sorry… you are out of luck. Not possible.
  2. It’s terrible for creating new records. Let’s say that a user searches for a specific related record and it (absolutely) doesn’t exist. To create the new record they need to close the lookup window, navigate to the tab to create the new related record, create the new record, then go back to the original record they were either editing or creating, pop up the lookup window again and find their newly created record. Wow! That’s a lot of work.
  3. “Quick Create” is evil! You can enable the “Quick Create” option for an entire org but don’t do it! It displays, by default, on the tab home pages for leads, accounts, contacts, forecasts, and opportunities! The major problems are that you can only create new records for these 5 objects (what about the other ones!?), you can’t customize the fields on the page and validation rules don’t fire (can you say, “bad data”).

Quick Create is Bad

Here’s the Solution!

I have some good news and some bad news. For standard page layouts I can’t help you. Go vote for this idea and this idea. However, for Visualforce page I have a solution to all of these problems with code!

Custom Lookup

Here’s how it looks. It may be easier to watch it full screen at YouTube.

Here’s the code you need to accomplish this. You need two Visualforce pages (the record you are editing and the popup window) and two Apex controllers (a simple one for the record you are editing and the controller for the search and new record popup).

MyCustomLookupController - code at github

Here’s the Apex controller for the record you are either creating or editing. This is an extremely simple controller that just creates a new contact so you can use the lookup for the related account field.

1
2
3
4
5
6
7
8
9
public with sharing class MyCustomLookupController {
 
  public Contact contact {get;set;}
 
  public MyCustomLookupController() {
    contact = new Contact();
  }
 
}

MyCustomLookup - code at github

This is the “magical” Visualforce page that uses jQuery to intercept the popup and instead of showing the standard salesforce.com pop, shows our custom popup instead. The user experience is seamless.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<apex:page controller="MyCustomLookupController" id="Page" tabstyle="Contact">
 
  <script type="text/javascript"> 
  function openLookup(baseURL, width, modified, searchParam){
    var originalbaseURL = baseURL;
    var originalwidth = width;
    var originalmodified = modified;
    var originalsearchParam = searchParam;
 
    var lookupType = baseURL.substr(baseURL.length-3, 3);
    if (modified == '1') baseURL = baseURL + searchParam;
 
    var isCustomLookup = false;
 
    // Following "001" is the lookup type for Account object so change this as per your standard or custom object
    if(lookupType == "001"){
 
      var urlArr = baseURL.split("&");
      var txtId = '';
      if(urlArr.length > 2) {
        urlArr = urlArr[1].split('=');
        txtId = urlArr[1];
      }
 
      // Following is the url of Custom Lookup page. You need to change that accordingly
      baseURL = "/apex/CustomAccountLookup?txt=" + txtId;
 
      // Following is the id of apex:form control "myForm". You need to change that accordingly
      baseURL = baseURL + "&frm=" + escapeUTF("{!$Component.myForm}");
      if (modified == '1') {
        baseURL = baseURL + "&lksearch=" + searchParam;
      }
 
      // Following is the ID of inputField that is the lookup to be customized as custom lookup
      if(txtId.indexOf('Account') > -1 ){
        isCustomLookup = true;
      }
    }
 
 
    if(isCustomLookup == true){
      openPopup(baseURL, "lookup", 350, 480, "width="+width+",height=480,toolbar=no,status=no,directories=no,menubar=no,resizable=yes,scrollable=no", true);
    }
    else {
      if (modified == '1') originalbaseURL = originalbaseURL + originalsearchParam;
      openPopup(originalbaseURL, "lookup", 350, 480, "width="+originalwidth+",height=480,toolbar=no,status=no,directories=no,menubar=no,resizable=yes,scrollable=no", true);
    } 
  }
</script>
 
<apex:sectionHeader title="Demo"  subtitle="Custom Lookup" />
 
  <apex:form id="myForm">  
    <apex:PageBlock id="PageBlock">    
      <apex:pageBlockSection columns="1" title="Custom Lookup">
        <apex:inputField id="Account" value="{!contact.AccountId}"  />
      </apex:pageBlockSection>
    </apex:PageBlock>
  </apex:form>
 
</apex:page>

CustomAccountLookupController - code at github

The Apex controller for the custom popup window is yours to customize. I know what you are thinking, “Free at last! Free at last! Thank God Almighty, we are free at last!” This class has all of your custom search functionality plus the method to create a new account. This is demo code so the search is very limited and does not prevent soql injections.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public with sharing class CustomAccountLookupController {
 
  public Account account {get;set;} // new account to create
  public List<Account> results{get;set;} // search results
  public string searchString{get;set;} // search keyword
 
  public CustomAccountLookupController() {
    account = new Account();
    // get the current search string
    searchString = System.currentPageReference().getParameters().get('lksrch');
    runSearch();  
  }
 
  // performs the keyword search
  public PageReference search() {
    runSearch();
    return null;
  }
 
  // prepare the query and issue the search command
  private void runSearch() {
    // TODO prepare query string for complex serarches & prevent injections
    results = performSearch(searchString);               
  } 
 
  // run the search and return the records found. 
  private List<Account> performSearch(string searchString) {
 
    String soql = 'select id, name from account';
    if(searchString != '' && searchString != null)
      soql = soql +  ' where name LIKE \'%' + searchString +'%\'';
    soql = soql + ' limit 25';
    System.debug(soql);
    return database.query(soql); 
 
  }
 
  // save the new account record
  public PageReference saveAccount() {
    insert account;
    // reset the account
    account = new Account();
    return null;
  }
 
  // used by the visualforce page to send the link to the right dom element
  public string getFormTag() {
    return System.currentPageReference().getParameters().get('frm');
  }
 
  // used by the visualforce page to send the link to the right dom element for the text box
  public string getTextBox() {
    return System.currentPageReference().getParameters().get('txt');
  }
 
}

CustomAccountLookup - code at github

Any finally the Visualforce page for the popup itself. It contains a tabbed interface easily allowing a user to search for records and create new ones. Make sure you look at the code for the second tab for creating a new record. I have better things to do than change the fields on the input form every time a new field is created or something is made required. The solution is to use field sets! So when an administrator makes a change, they can simply update the field set and the popup reflects the change accordingly. Life is good.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<apex:page controller="CustomAccountLookupController"
  title="Search" 
  showHeader="false" 
  sideBar="false" 
  tabStyle="Account" 
  id="pg">
 
  <apex:form >
  <apex:outputPanel id="page" layout="block" style="margin:5px;padding:10px;padding-top:2px;">
    <apex:tabPanel switchType="client" selectedTab="name1" id="tabbedPanel">
 
      <!-- SEARCH TAB -->
      <apex:tab label="Search" name="tab1" id="tabOne">
 
        <apex:actionRegion >  
          <apex:outputPanel id="top" layout="block" style="margin:5px;padding:10px;padding-top:2px;">
            <apex:outputLabel value="Search" style="font-weight:Bold;padding-right:10px;" for="txtSearch"/>
            <apex:inputText id="txtSearch" value="{!searchString}" />
              <span style="padding-left:5px"><apex:commandButton id="btnGo" value="Go" action="{!Search}" rerender="searchResults"></apex:commandButton></span>
          </apex:outputPanel>
 
          <apex:outputPanel id="pnlSearchResults" style="margin:10px;height:350px;overflow-Y:auto;" layout="block">
            <apex:pageBlock id="searchResults"> 
              <apex:pageBlockTable value="{!results}" var="a" id="tblResults">
                <apex:column >
                  <apex:facet name="header">
                    <apex:outputPanel >Name</apex:outputPanel>
                  </apex:facet>
                   <apex:outputLink value="javascript:top.window.opener.lookupPick2('{!FormTag}','{!TextBox}_lkid','{!TextBox}','{!a.Id}','{!a.Name}', false)" rendered="{!NOT(ISNULL(a.Id))}">{!a.Name}</apex:outputLink>     
                </apex:column>
              </apex:pageBlockTable>
            </apex:pageBlock>
          </apex:outputPanel>
        </apex:actionRegion>
 
      </apex:tab>
 
      <!-- NEW ACCOUNT TAB -->
      <apex:tab label="New Account" name="tab2" id="tabTwo">
 
        <apex:pageBlock id="newAccount" title="New Account" >
 
          <apex:pageBlockButtons >
            <apex:commandButton action="{!saveAccount}" value="Save"/>
          </apex:pageBlockButtons>
          <apex:pageMessages />
 
          <apex:pageBlockSection columns="2">
            <apex:repeat value="{!$ObjectType.Account.FieldSets.CustomAccountLookup}" var="f">
              <apex:inputField value="{!Account[f]}"/>
            </apex:repeat>
          </apex:pageBlockSection> 
        </apex:pageBlock>
 
      </apex:tab>
    </apex:tabPanel>
  </apex:outputPanel>
  </apex:form>
</apex:page>

Categories: Apex, Salesforce, Visualforce

14 Comments

Feed

http://blog.jeffdouglas.com /

WordPress Appliance - Powered by TurnKey Linux