Mass Contact Transfer – Part 2 (The Controller)

mgsmith | Monday, April 20th, 2009 | 18 Comments »

Click on the SourceCode tab above to view the full sourcecode for the application or to install a complete working version into your environment.

In the last blog post I went into some of the details behind the Mass Contact Transfer VisualForce page. In this post, I’ll delve into the page Controller and supporting classes that contain the business logic for the page.

The transferContacts class is the controller for the VisualForce page. The controller has four main functions:

  • Provide the get/set methods for fields used on the VisualForce page
  • Create picklists of users for the To and From user fields
  • A doSearch() method that looks for records based on the criteria entered
  • A doTransfer() method to transfer the selected contacts to the new users

Get/Set methods

Generally these are very simple methods to either set the value of a public variable or return that value. Get/Set methods are always preceded by the word get or set followed by the name of the variable. On the VisualForce page these are referenced as {!fieldname}. For example, using value=”!fromUserID}” on a VisualForce page will call the getfromUserID() method to retrieve the value and setfromUserId() to set the value when the form is submitted.

// Variables
public string fromUserID;
public string toUserID;

// Get/Set Methods for the FROM and TO UserID's
public String getfromUserID()   { return fromUserID; }
public String gettoUserID()     { return toUserID; }
public void setfromUserID(string userID)  { this.fromUserID = userID; }
public void settoUserID(string userID)    { this.toUserID = userID; }

The From and To User picklist fields

Rather than require users to use a search lookup for users on the transfer form, I implemented this using two picklists for the From and To users. The FromUsers list includes all Users while the ToUsers lists only includes Active users. The setup behind the two picklists fields is very straight forward.

VisualForce page code for a picklist. The codeblock below accesses the getFromUsers() method to fill the values in the tag.

<apex:selectList value="{!fromUserID}" size="1" required="false"  id="fromUserID">
<apex:selectOptions value="{!FromUsers}"></apex:selectOptions>
</apex:selectList>

Apex Controller code to return the picklist of values. The getFromUsers() method returns a List object of Users sorted by Name. The SelectOption list type accepts two values – an ID and a display name. When a FromUser name is selected from the list, the Apex Controller will be able to access the ID of that user by referencing the toUserID variable. This is set by the page to the ID of the selected when the form is submitted.

public List<SelectOption> getFromUsers() {
    List<SelectOption> options = new List<SelectOption>();
    options.add(new selectOption('', '--- select a User ---'));
    for (User u : [Select ID, Name FROM User Order by Name]) {
            options.add(new selectOption(u.ID, u.Name));
    }
    return options;
}

Searching for Contacts

The doSearch() method is called when the [Find] button is clicked on the page.

<apex:commandButton title="Find" value="Find" action="{!doSearch}"/>

The overall goal of the method is to build an SOQL string, run the Query, and display a list of contacts on the page. As discussed in Part 1, the searchCriteria() class contains the methods necessary to generate the actual WHERE clause parts to build the query string. The logic behind this is shown below.

To start, a base SOQL string is built selecting all the fields in the Contact object along with key fields from the Account, Owner, Account.Owner, CreatedBy.Owner, and LastModifiedBy.Owner relationships. The initial contents of the WHERE exclude Contacts where the current owner is the owner to transfer contacts to (in other words, they own the contacts already).

// Build the base SOQL String, querying the standard Contact fields
// WHERE the current OwnerID = the selected value
string cSOQL = 'SELECT ' + contactFieldsList + ', Account.Name, Account.Site, Account.Owner.Name, '  +
'Account.Industry, Account.Type, Account.Owner.Alias, ' +
'Owner.Name, Owner.Alias, CreatedBy.Name, CreatedBy.Alias, LastModifiedBy.Name, LastModifiedBy.Alias ' +
'FROM Contact WHERE OwnerID <> \'' + toUserID + '\' ';

If a To Owner was selected, add to the WHERE clause to restrict the list of contacts owned by the To User ID.

// If a From User was selected, add this to the criteria
if (fromUserID <> null) cSOQL += ' AND OwnerID = \'' + fromUserID + '\' ';

For each line of Criteria, call the BuildWhereClause() method in the searchCriteria class.

// For each criteria line item, call the method to build the where clause component
for (searchCriteria cl : criteriaLine) {
cSOQL += cl.buildWhereClause(DebugMode);
}

Finally, order the list by Account Name and then Contact Name and limit the list to the first 250 contacts.

// Sort the results and limit to the first 250 rows
cSOQL += ' ORDER BY Account.Name, Name LIMIT 250' ;

searchCriteria class

The BuildWhereClause() to build the WHERE clause component for each of the search criteria lines is where it got complex. This method had to generate code to handle different field types (Text, PickList, Boolean, Number, Date, DateTime, etc.), different operators (=, >, <, contains, IN, etc.) and different types of values. Most of the logic is fairly straight forward, checking the field type and the operator and generating errors where the two are not compatible (BOOLEAN and ‘Less Than’, for example).

The complex part of the logic is to handle Date and DateTime field types. The most recent version of the application now supports the date format of the current user when parsing the value entered. Where this became tedious was for DateTime types. For example, you might expect the following SOQL WHERE block to only retrieve Contacts created on April 1, 2009: WHERE CreatedDate = 2009-04-01. However, CreatedDate is a DateTime field and requires a timestamp, not just a date. In order to properly query Contacts created on 4/1/09 the actual WHERE clause needs to looks like: WHERE (CreatedDate >= 2009-04-01T00:00:00Z AND CreatedDate <= 2009-04-01T23:59:59Z)

Transferring the Contacts

The final step in the process is to transfer the selected contacts to the ‘To User’. The toTransfer() method starts by building a list of Contact ID’s for selected Contacts. Part 1 of this blog post goes into the checkbox on each Contact. Only checked contacts are transferred. The list of ID’s is passed to a Query to select the Contacts. Finally, the OwnerID for each Contact is changed and the Database.Update() method is called to do the actual transfer.

// Build a list of Contact ID's to transfer ownership for
List<string> IDs = New List<string>();
for (transferContactSearchResults c : searchResults) {
if (c.selected) IDs.add(c.contact.ID) ;
}

// Query the contacts being transferred
List<Contact> contacts = [SELECT ID, OwnerID, Name, Account.Name, Title, Owner.Alias FROM Contact WHERE ID IN :IDs];
for (Contact c : contacts) {
c.ownerID = toUserID ;
}

// Process Errors and Count the Number of Records Transferred
Integer transferCount = 0;
List<database.saveresult> srs = database.update(contacts);
for (database.saveresult sr : srs) {
if (!sr.isSuccess()) {
      	ApexPages.AddMessage(new ApexPages.Message(ApexPages.Severity.FATAL, sr.getId() + '/' + sr.getErrors()[0].getMessage() ));
} else {
      	transferCount++;
      }
}

At the very end, the doSearch() method is called to look for additional contacts if there were more than 250 returned in the first query call.

Quick links to Class source code:

The final part of this blog post series will go into building the test class.

Share

18 Comments

  1. Vin D'Amico says:

    This is a wonderful tool. It allows you to quickly search for and transfer contacts. Well done!

    Just be aware that activities and events associated with the contacts are not re-assigned. That one addition would make this truly awesome.

    Thanks!

  2. Nathalie says:

    Installation fails with the test module – here is the error message:

    Package components error list:

    Problem Component Detail
    transfercontacts_test.myUnitTest() Apex Classes(01p00000000CjP2) System.Exception: Too many query rows: 501
    Class.transferContacts.getFromUsers: line 93, column 23
    Class.transferContacts_Test.myUnitTest: line 80, column 9

  3. mgsmith says:

    Nathalie,

    Seems that you have a quite a bit of active users, which is causing the Test method to fail due to SalesForce governor limits on test methods. I updated the code to have a ‘test’ mode where it limits the # of records retrieved.

    Mike

  4. Ken says:

    Fails to install with the message:
    Missing feature Apex Classes Installing this package requires the following feature and its associated permissions: Apex Classes

  5. Ken says:

    I should mention that this is for Professional edition, which is supposed to be supported.

  6. draicairl says:

    I found this site using google.com And i want to thank you for your work. You have done really very good site. Great work, great site! Thank you!

    Sorry for offtopic

  7. Mark says:

    I installed the app from AppExchange without any issues but when I open the Mass Transfer Contacts tab, I get the following error:

    System.Exception: collection exceeds maximum size: 1001

    An unexpected error has occurred. Your solution provider has been notified. (f2b)

  8. pk says:

    I am looking for a code which can replicate the product search feature similar to when you add multiple products to an opportunity or price book.

    In SFDC it shows the more filters and less filters link. Can you give some idea how to get a exact replica of it in a custom visualforce page.

  9. Craig says:

    I was trying to install the app and ran into the following error:

    System.DmlException: Insert failed. First exception on row 0; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, Email is REQUIRED: []
    (f2b)
    External entry point

    Do I have to remove the validation rule completely or is there a workaround?

  10. JordanA says:

    I tried installing the Mass Transfer Contact app, and I recieved the following error message

    System.DmlException: Insert failed. First exception on row 0; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, Account is required when creating Contacts. Please enter an Account name.: [AccountId]
    (f2b)
    External entry point

    If I could get the app to work it would save me a great deal of time. What do I have to do to install it?

    Thank you for your time,
    Jordan.

  11. mgsmith says:

    Jordan,

    That error is coming from one of your validation rules. Just check the box for “ignore Apex Errors during installation” while installing. The reason the error appears is because the custom validation rule is preventing the application’s unit test from creating dummy contacts to test the application code coverage. The ‘dummy contacts’ are not saved in your database, it’s just part of the “Unit Test” process.

    Best Regards,

    Mike

  12. Chris M says:

    This seems to be great, except that I really want to use it for transferring Contacts owned by a Site user, which I cannot choose.

    All of our Self Service Portal Users are created by a Site, and we need to make it easy to re-assign them owners, hence this approach.

  13. mgsmith says:

    Chris,

    Interesting issue. I’ve been pretty tied up on a couple of projects going live in October, but I’ll add this as something to look into when I have some more time.

    Best Regards,

    Mike

  14. MA says:

    App installed OK but when go to run the transfer, get this error:

    Invalid parameter for function URLFOR
    Error is in expression ‘{!URLFOR($Action.Contact.View, Results.contact.ID)}’ in component in page f2b:transfercontacts

    Is there a way to override?

  15. Jurgen Berckmans says:

    Hello
    I installed the package and was able to transfer exactly 1 contact in a successful way .
    When trying to transfer a bunch of contacts with the app , and also when trying to transfer a single contact afterwards I get the following error message.

    Invalid parameter for function URLFOR
    Error is in expression ‘{!URLFOR($Action.Contact.View, Results.contact.ID)}’ in component in page f2b:transfercontacts

    Could you please help me or indicate where to look ?

    Thanks a lot
    Jurgen Berckmans

  16. Jurgen Berckmans says:

    Hi,

    I didn’t get this error message any longer.
    Which is good.
    In the meanwhile we identified the 2 contacts causing the issue and were able to transfer them with your app. However I’m not sure whether we 100% tackled the issue which I think might be caused by custom development of a trigger on our end . Also you need to know we use the account team funtionality withhin our company.

    The biggest problem ( before we identified the contacts causing most likely the error message in your app) was that all the other ‘good’ contacts were not transferred either. We managed to transfer those last week with trial and error method. SO now I can’t confirm to you if the error will no longer appear in case there are some ‘bad’ contacts amongst a larger bunch of ‘good’ contacts.
    We will continue to use your real nice app and get back with more feedback if needed when doing future conversions.
    Thanks a lot for your swift response.

    Regards
    Jurgen

Leave a Reply