Stat Tracker

Monday, December 16, 2013

Capturing Signatures with HTML5 Canvas in Salesforce 1 Mobile

Recently Salesforce.com released Salesforce 1, their latest mobile application. Salesforce 1 releases a number of new features that enable developers to create mobile applications. One person I spoke with recently at Dreamforce wanted to know how they could capture signatures in the mobile application. With HTML5 Canvas, Visualforce, and a little JavaScript, you can easily roll your own lightweight signature capture functionality in Salesforce 1.

Here is a brief demonstration video:


So I have included the entire source code below, but a few items about the tricky parts.

1. You will need to setup JavaScript event listeners on the canvas for touchstart, touchmove, and touchend. That is what the canvas will execute when you touch and drag your finger on it.

2. You will need to use JavaScript Remoting to ensure that you properly pass the Canvas content into your Apex Controller so that it can save it. The canvas can be converted into a Base64 String with the Canvase.toDataURI() method. That is how you get the bytes from the Canvas into an Attachment in Salesforce.com.

These are illustrated in the sample code.

And here is the source code for the VF and Apex. If you put these into a Visualforce Tab, and make it enabled for Salesforce 1 Mobile, then you easily reuse this sample code.

jQuery: http://code.jquery.com/jquery-2.1.1.min.js

jQuery Mobile Resources (Download Links)
Version 1.3.2: http://jquerymobile.com/resources/download/jquery.mobile-1.3.2.zip

Latest Version 1.4.4: http://jquerymobile.com/resources/download/jquery.mobile-1.4.4.zip

Source Code:

Visualforce Page Code:

<apex:page controller="AnyObjectSignatureController" showheader="false" sidebar="false" standardStylesheets="false">
<script>var $j = jQuery.noConflict();</script>
<apex:stylesheet value="{!URLFOR($Resource.jquerymobile,'/jquerymobile/jquery.mobile-1.3.2.min.css')}"/>
<apex:includeScript value="{!URLFOR($Resource.jquery)}"  />
<apex:includeScript value="{!URLFOR($Resource.jquerymobile,'/jquerymobile/jquery.mobile-1.3.2.min.js')}"/>

<div data-role="page" id="signatureCaptureHome"> 
<div data-role="content">
<input id="accountNameId" type="text" name="accountName"/>
<input type="button" name="findAccountBtn" onclick="findAccounts();" value="Find Accounts"/>
<h1 id="recordSigId">Record Signature:</h1>
<canvas id="signatureCanvas" height="200px" width="300px"/>
<input id="saveSigButton" type="button" name="SigCap" onclick="saveSignature();" value="Capture Signature"></input>
</div> 
</div> 
<div data-role="page" id="signatureCaptureHome"> 
<div data-role="content">
<input id="accountNameId" type="text" name="accountName"/>
<input type="button" name="findAccountBtn" onclick="findAccounts();" value="Find Accounts"/>
</div> 
</div> 

<script>

    var canvas;
    var context;
    var drawingUtil;
    var isDrawing = false;
    var accountId = '';

function DrawingUtil() 
{
    isDrawing = false;
    canvas.addEventListener("touchstart",start,false);
    canvas.addEventListener("touchmove",draw,false);
    canvas.addEventListener("touchend",stop,false);
    context.strokeStyle = "#FFF";  
}

//Start Event for Signature Captuare on HTML5 Canvas
function start(event) 
{
    isDrawing = true;
    canvas = document.getElementById("signatureCanvas");
    context = canvas.getContext("2d");    
    context.strokeStyle = "rgba(155,0,0,0.5)";      
    context.beginPath();
     context.moveTo(event.touches[0].pageX - canvas.getBoundingClientRect().left,event.touches[0].pageY - canvas.getBoundingClientRect().top);
}

//Event while someone is drawing to caputre the path while they draw....
function draw(event) {
    event.preventDefault();
    if(isDrawing) {     
        context.lineTo(event.touches[0].pageX - canvas.getBoundingClientRect().left,event.touches[0].pageY - canvas.getBoundingClientRect().top);
        context.stroke();
    }
}


//Event when someone stops drawing their signature line
function stop(event) {
    if(isDrawing) {
        context.stroke();
        context.closePath();
        isDrawing = false;
    }
}

canvas = document.getElementById("signatureCanvas");
context = canvas.getContext("2d");
drawingUtil = new DrawingUtil(canvas);

function saveSignature()
{
var strDataURI = canvas.toDataURL();
    // alert(strDataURI);
    strDataURI = strDataURI.replace(/^data:image\/(png|jpg);base64,/, "");
//alert(strDataURI);
AnyObjectSignatureController.saveSignature(strDataURI,accountId,processResult);
}

function processResult(result)
{
alert(JSON.stringify(result));
}

function findAccounts()
{
var nameValue = document.getElementById("accountNameId").value;
AnyObjectSignatureController.findAccounts(nameValue, processSearchResult);

function processSearchResult(result)
{
$j = jQuery.noConflict();
//$j("#accountList").html("");
$j.each(result, function(i, record) {accountId = record.Id; $j("#recordSigId").html("Record Signature: " + record.Name);});
$j("#recordSigId").trigger("update");
//$j("#accountList").trigger("update");
//alert(JSON.stringify(result));
}


</script>

</apex:page>

Apex Controller:
global with sharing class AnyObjectSignatureController 
{
public AnyObjectSignatureController()
{
}
@RemoteAction
global static List<Account> findAccounts(String name)
{
name = '%' + name + '%';
List<Account> accounts = [Select Id, Name from Account where Name like :name];
return accounts;
}
@RemoteAction
global static String saveSignature(String signatureBody, String parentId) 
{
try
{
system.debug('Record Id == ' + parentId);
system.debug(signatureBody);
Attachment a = new Attachment();
a.ParentId = parentId;
a.Body = EncodingUtil.base64Decode(signatureBody);
a.ContentType = 'image/png';
a.Name = 'Signature Capture.png';
insert a;
return '{success:true, attachId:' + a.Id + '}';
}catch(Exception e)
{
return JSON.serialize(e);
}
return null;
}

}

Sunday, October 13, 2013

Dreamforce 2013 Sessions - Lets Rock

Dreamforce is upon us! In just a few weeks San Francisco will turn into the mecca for cloud computing with almost 100,000 cloud devotees making the annual pilgrimage.  This will be my third year presenting at Dreamforce and I can honestly say each year the Developer Zone has gotten better and better. This year I will be presenting or contributing on four different sessions. And for the first time I'll be co-presenting a session with another person! I'm excited to be working with Tim McDonald on our administrator and developer session.

Come check out the sessions, contribute to the conversations on the chatter feeds, and get your brain ready for data downloads!

Case Study: Building a Mobile App for Field Services

Wednesday, November 20th: 4:00 PM - 4:30 PM
Moscone Center West, Mobile Theater

Description

The Salesforce Platform allows you to architect complete solutions for entire lines of business, whether it's desktop or mobile users. Join us as we focus on how users can build a fully featured mobile solution for Field Service engineers. 

By dissecting an HTML 5 Hybrid Application for the Service Cloud, you'll get exposure to building a complete mobile application using the jQuery Mobile for UI, Salesforce Mobile SDK for Security &amp; REST API Access, NFC Phonegap Plugin for Serial Number Scanning and Automatic Case Assignment, HTML5 Canvas for Signature Capture Camera Access for Case Documentation &amp; Attachemnt on the Case in Salesforce, and Chatter API for Social Feeds on Cases.
Speakers:
Cory CowgillThe Warranty Group

Apex Trigger Debugging: Solving the Hard Problems

Wednesday, November 20th: 11:45 AM - 12:30 PM
Moscone Center West, 2020
Full

Description

Apex Triggers can be your best friend or your worst enemy. When a trigger is firing properly your data is under control and remains sane, but when a trigger doesn't fire properly, your users can be faced with the frustration of exceptions when saving a record, or worse: incorrect data. Join us to learn tips and tricks on how to debug and solve the most complex issues, including: Ambiguous Field Validation, After Insert Activity Errors, and SOQL and Governor Limit Errors. You'll learn the origins of these kinds of advanced trigger issues and gain solutions for avoiding them.
Speakers:
Cory Cowgill

Clicks AND Code: A Dreamforce Session for Administrators AND Developers

Wednesday, November 20th: 9:00 AM - 9:50 AM
The Westin St. Francis San Francisco, California West
Full

Description

Administrators seem to have adopted the mantra of “Clicks not Code.” However, more often than not, the customization of the Salesforce Platform through the use of code provided by a developer is not only necessary, but required for a successful implementation. Join us as we present best practices for administrators to use when engaging their developer counterparts, while providing some tips and tricks for developers to quickly respond to the requests placed before them.
Speakers:
Cory CowgillWest Monroe Partners
Tim McDonaldNew Tangram, LLC

Force.com Careers: How Do I Get There From Here?

Thursday, November 21st: 11:00 AM - 11:45 AM
Moscone Center West, 2020

Description

Do you love developing on the Salesforce Platform, but wonder what the next steps are for your career? Join our panelists to hear about various career paths, including Consultant, Architect, Product Manager, and AppExchange Developer, to name a few. These experts will share the pros and cons of their careers and also the path to get there.
Speakers:
Carolina Ruiz MedinaFinancialForce.com
Cory CowgillWest Monroe Partners
Leah McGowen-Haresalesforce.com
Cheryl Porrosalesforce.com
Ayori Selassiesalesforce.com
Andy OgnenoffCloud Sherpas
Kevin O'HaraLevelEleven


Can't get into one of my sessions because its full? Don't worry, sessions, presentations and source code will be distributed to the general public after the sessions. Have a question for me? Hit me up in the Developer Zone during the conference. I usually camp out there either by the hackathon, theaters, or coffee station. Hit me up in dreamforce chatter or on twitter @corycowgill and I'm happy to discuss anything Force.com related.

Looking forward to another awesome year!



Wednesday, July 24, 2013

Force.com Data Model - Enumeration Tables versus Picklists

The Salesforce Platform allows customers to build robust, relational data models to suit any need. In fact, with tools like Schema Builder it is so simple to get started building that it can be a bit of a double-edged sword. The simplicity allows functions that once rested solely in the hands of a Database administrator to be performed by a Business Analyst, or even the End Users. However, with great power always comes great responsibility.

The number one problem I have encountered working with clients who have performed Salesforce.com self-implementations is data model related. There are several common mistakes self implementers should avoid. In this blog post I'll be discussing how heavy data normalization can work against you on the platform.

This problem often occurs when the implementation was run by an internal IT team who have traditional SQL skill sets. They will create a custom object for every single enumeration table they think is needed without regards to how SFDC relational data models actually work (picklists for example).
Heavily Normalized Data Model in MySQL

This leads to headaches when building standard reports, and usability issues when viewing and editing data with standard SFDC pages. For the above example the mult-select picklist for "Payment Options" would show up as a Related List, and the Marketing Status would show up as a Lookup. If we created this same data model in SFDC it would look like this:
Erroneously built Data Model in SFDC - Heavily Normalized Data Model in SFDC

And this would manifest itself on the Standard UI as this:

Illustration: Ugly UI

The Related List at the bottom "allows" for the multi select picklist, and the lookup in the detail section allows for the lookup. This is very nasty for end users! Imagine if you had dozens of multi-select picklists! You would have dozens of related lists! And the users would have to click multiple times to enter a payment option, and they need to do a lookup each time for the marketing status.

Not to mention they can't easily filter on Payment Options for List Views and Reports.

This can easily be corrected by using picklist and multi-select picklists in SFDC. If we use those types of fields our data model removes those 3 objects and everything resides on the Company object. The correct data model looks like this:
Who! Only 1 object! 


And it manifests itself in the UI like this:
Much Cleaner!

This is much cleaner in the UI, allows easy reporting and filtering, and saves us 3 objects we don't have to build on the back end.

In future posts I'll discuss the inverse problem where data is too heavily de-normalized on the platform.

In short, the key to building successful data models on the platform is to delicately balance the need for custom objects ("tables") and features of the Salesforce platform (picklists, multi-select picklists, record types, etc).




Sunday, June 16, 2013

Inserting PDF Attachments into Salesforce.com using Talend and iText

I recently presented at the local Chicago Force.com Developer Group on using Talend to move data into and out of Salesforce.com. One question I fielded was how about moving documents into Salesforce.com. Yes you can use Talend to do this! For this demonstration I am going to use a few components and show you how to get the class path to execute properly for the ETL job.

Use Case:


For this demonstration, we are going to dynamically generate PDF content inside our ETL job. We are going to extract all our Account records from SFDC and dynamically generate PDF content from the fields on the Account records. We will then inser that data into an Attachment record for each of the Accounts as a PDF document created using iText.

Step 1 - Create Your Salesforce.com Metadata Connection

The first step is to create your connection for SFDC metadata. I have a video on YouTube which shows you how can do this. For this example you will want to pull in the Accounts and Attachements objects from SFDC. After following the instructions in the video for creating your SFDC metadata connections you should have them in your repository like figure below.



Step 2 - Download iText PDF Library

You will need the iText PDF library for the portions of the ETL job which will generate dynamic PDF content. You will also need to add the iText jar file to you java build path. You can do this by using the User Libraries feature as shown below:

Talend -> Preferences -> Java -> Build Path -> User Libraries


Step 3 - Create a new Job Design and Load Java Libraries

Talend allows you to load external Java libraries into your job which can have code executed inside your job. The component to load external Java libraries is called tLibraryLoad. You should use this component as one of the first steps in your job to load the any dependent jars you need. In this case, we are going to load the iText PDF library, as well as Apache Commons Codec so that we can Base64 encode our Attachment file content (more on that later).

Using tLibraryLoad component to load Java jar files.



Step 4 - Query SFDC Account Records

This step we simply use the tSalesforceInput component to read Accounts. For details you can see the video above on how to the Account records.


Step 4 - Create the Account Record PDF using tJavaRow

The tJavaRow component is a very powerful component. It allows you to code functionality into your ETL job using Java. In this example we are using the iText PDF Library to generate a simple PDF. The thing to remember about tJavaRow is that the code will execute for each record in the input step.

When you add and connect your tJavaRow component to the tSalesforceInput component, the first thing you need to do is click on the Advanced Settings tab and add any import for libraries that you need. These classes should reside in the jars you loaded in the previous step inside tLibraryLoad component.

Import Libraries for iText and Apache Commons Codec


Once you have the imports setup like above, you can then go to the Basic Settings tab. This will present you with the tJavaRow code editor where you can add your code. The first thing I do is click "Sync Columns" and "Generate Code" buttons. This will automatically create the code to simply move all the data from the input into your output row for this component.

Auto Code Generation and Column Sync


After I have let Talend do the heavy lifting of generating my getter and setter code, I then add two new fields. One called 'FileName' and one called "Content_Body". These fields will hold the filename in the format "Account Name.pdf" and the actual file content as a Base64 encoded string.

Add Fields to the Mapping for the Content and File Name



 The Salesforce API's use Base64 Strings to encode and pass file content. That is the reason we need to use Apache Commons in our job, to convert the PDF bytes into a format that SFDC and ingest. Using the output and input variable in the code, we can generate a PDF using the simple iText PDF objects. Here is the complete code. All this code goes inside the tJavaRow component.


//Code generated according to input schema and output schema
output_row.Id = input_row.Id;
output_row.IsDeleted = input_row.IsDeleted;
output_row.MasterRecordId = input_row.MasterRecordId;
output_row.Name = input_row.Name;
output_row.Type = input_row.Type;
output_row.ParentId = input_row.ParentId;
output_row.BillingStreet = input_row.BillingStreet;
output_row.BillingCity = input_row.BillingCity;
output_row.BillingState = input_row.BillingState;
output_row.BillingPostalCode = input_row.BillingPostalCode;
output_row.BillingCountry = input_row.BillingCountry;
output_row.ShippingStreet = input_row.ShippingStreet;
output_row.ShippingCity = input_row.ShippingCity;
output_row.ShippingState = input_row.ShippingState;
output_row.ShippingPostalCode = input_row.ShippingPostalCode;
output_row.ShippingCountry = input_row.ShippingCountry;
output_row.Phone = input_row.Phone;
output_row.Fax = input_row.Fax;
output_row.AccountNumber = input_row.AccountNumber;
output_row.Website = input_row.Website;
output_row.Sic = input_row.Sic;
output_row.Industry = input_row.Industry;
output_row.AnnualRevenue = input_row.AnnualRevenue;
output_row.NumberOfEmployees = input_row.NumberOfEmployees;
output_row.Ownership = input_row.Ownership;
output_row.TickerSymbol = input_row.TickerSymbol;
output_row.Description = input_row.Description;
output_row.Rating = input_row.Rating;
output_row.Site = input_row.Site;
output_row.OwnerId = input_row.OwnerId;
output_row.CreatedDate = input_row.CreatedDate;
output_row.CreatedById = input_row.CreatedById;
output_row.LastModifiedDate = input_row.LastModifiedDate;
output_row.LastModifiedById = input_row.LastModifiedById;
output_row.SystemModstamp = input_row.SystemModstamp;
output_row.LastActivityDate = input_row.LastActivityDate;
output_row.CustomerPriority__c = input_row.CustomerPriority__c;
output_row.SLA__c = input_row.SLA__c;
output_row.Active__c = input_row.Active__c;
output_row.NumberofLocations__c = input_row.NumberofLocations__c;
output_row.UpsellOpportunity__c = input_row.UpsellOpportunity__c;
output_row.SLASerialNumber__c = input_row.SLASerialNumber__c;
output_row.SLAExpirationDate__c = input_row.SLAExpirationDate__c;
output_row.Location__Latitude__s = input_row.Location__Latitude__s;
output_row.Location__Longitude__s = input_row.Location__Longitude__s;

//CREATE PDF CODE - FROM ITEXT PDF EXAMPLE        
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Document document = new Document();
        // step 2
        PdfWriter.getInstance(document, bos);
        // step 3
        document.open();
        // step 4
        document.add(new Paragraph("Account Name: " + output_row.Name + "/nAccountID: " + output_row.Id));
        // step 5
        document.close();
        
        byte[] bytes = bos.toByteArray();
        String stringToStore = new String(Base64.encodeBase64String(bytes));
        output_row.Content_Body = stringToStore;

        output_row.FileName = input_row.Name + ".pdf";



Step 5 - Insert the PDF into the Attachment Record 



Finally we pass along the values using a tMap component and the tSalesforceOutput component for Attachments. We map the output row fields from the tJavaRow onto our tSalesforceOutput row.

tMap Component


The complete demo job looks like this:



Step 6 - Export the Job and Run


The last step is exporting the job and running it on your machine. If you export the job design as a zip, you can get to the AccountPDFJob.sh file which executes the job. This video shows it running and you can see the final generate PDF attachment in SFDC.


I hope this helps folks. There are a lot of uses for this. You could generate PDF documents out of SFDC for all sorts of things with this approach. You could build a job that merges your SFDC data with an ERP and create a PDF invoice for example. Or you could use the Chatter objects and post files to a persons Chatter feed. There are lots of scenarios.






Saturday, June 8, 2013

My Salesforce Mobile Developer Challenge Entry 2013

I just entered my Developer Force 2013 Mobile Challenge entry. For this challenge I built a HTML5 Hybrid Application leveraging Salesforce Mobile SDK, Cordova (PhoneGap), NFC Plugin, and HTML5 Canvas for signature capture. Here is the demo video:


And here is the SourceCode: https://github.com/corycowgill/MobileFieldServiceApp

The general idea for the application is a mobile application that field engineers could use. The application uses NFC technology to scan a tagged device and display the relevant case information to the engineer. The general idea being that if you can use NFC tags on your products, it would allow the Field Service agent to simply touch his phone or table to the device they are servicing and see all the details automatically.

Here are some additional screenshots to show the simple UI using jQuery Mobile.





I also added HTML5 Canvas to do signature capture and upload as a attachment to the Case in SFDC.

There was so much more I wanted to do but just didn't have the time. Therefore I decided to focus my time implementing the NFC plugin and functionality. My current client work just didn't afford me the time I would have liked to dedicate to this personal project. For example I would have liked to add:

1. SmartStore for Offline Access. For example, when working at a client site in a basement or where signal strength is weak.

2. GPS and Geolocation. Basically for address directions / map and checkin.

3. Checkin / Checkout status pushes for Chatter.

4. Add backend Javascript Framework instead of inline HTML / JS that I used to rapidly build this project. I just didn't have the time to learn a new framework like Angular or Backbone.js. Next Time!

5. Additional Error Handling Logic

6. User Interface Updates & Design - Add a little flair.

I had a lot of fun creating this challenge. I may re-vist this just to complete those items above.


Tuesday, May 7, 2013

Salesforce Mobile Developer Challenge 2013!

The Salesforce Mobile Developer Challenge 2013 is live. I'm pretty excited to see the entries that folks come up with this year. You have until June 3 to finish your applications so you have a few weeks to get moving. There are a number of Mobile Packs that you can use to get started rapidly building applications.

I have started on my entry last weekend. Once I submit my application on June 3rd I'll post my GitHub Repo and the YouTube video on this entry so folks can see what I built and how.








Monday, March 18, 2013

Person Account & Contact Formula Fields

With the latest Spring 13 release of Salesforce.com Formula Fields were enabled for Person Accounts. For those who are unfamiliar, Person Accounts are the B2C (Business to Consumer) feature for managing accounts. It is often used in Financial Services implementations where a sales person may sell to both Institutions as well as individual investors. Person Accounts blend Accounts and Contacts objects in the Salesforce.com front end to appear as one record.

Under the hood Person Accounts are actually two distinct records, one Account and one Contact. The Account object has a field called "PersonContactId" which links the Account to the contact record. Therefore there is a link from the Account to the Contact record (Account.PersonContactId), and a link from the Contact record to the Account (Contact.AccountId). A number of fields from the Contact record are then made available on the Account object. Standard Fields from the Contact are available on the Account via the "Person" prefix. For example, Account.PersonMailingStreet or Account.PersonTitle bring up the values from the underlying Contact record. Custom fields on a Contact are brought up to the Account record with fields ending in __pc. For example, Account.ContactTotalWorth__pc for a field called "Contact Total Worth" on the contact record.

Previous to Spring 13 you could not use Person Account fields in a formula field. For example, on an Opportunity record you could not do a Formula Field like "Account.PersonMailingStreet" to bring the field onto the Opportunity record. Now you can do this for all the standard and custom fields with the exception of PersonContactID.

For some reason PersonContactID is not available in Formula Fields. This also applies to any Formula Fields on the Contact that may use the Contact ID.

So when creating Formula Fields for Person Accounts just remember that the Contact ID is currently not accessible.


Sunday, February 3, 2013

Creating Mirror Relationship Records in SFDC with Apex Triggers

Reciprocal or Mirror Records Overview


In this blogpost I will be discussing creating reciprocal relationship records in Force.com using Apex Triggers. I'd first like to say that this use case is one that should be scrutinized before you implement it. Creating reciprocal or mirror relationships of records in Force.com can sometimes create duplicate records. To keep your managed data down, you would often like to avoid this process if possible. However there are times when you just have to keep record values in sync, so carefully evaluate your use case before implementing this. Now that I have the disclaimer out the way here we go!

When working with records in Force.com we sometimes have a need to create a reciprocal relationship between two records. For example, sometimes we may have a use case which involves keeping two records in sync. When you update Record A, you may need to update some corresponding fields on Record B. Below is a simple diagram illustrating this.


Force.com Data Model


The best way to implement this is to have a lookup field on the object which is a lookup to it self. In this case I have create a lookup field on contact called "Linked Contact" and related it to the Contact record as shown in the schema builder diagram below.



Apex Trigger Flow

Now that I have the data model configured for the reciprocal relationships, I need to keep the records in sync automatically. This means that whenever Contact record A is updated, make sure to update Contact record B. We can accomplish this via Apex Triggers when a record is inserted or updated to keep the lookup fields in sync, thereby allowing us to "link" the records. However, there is problem with this scenario.

In an Apex Trigger you only have acces to the records Id in the After context, which makes sense. There cannot be an Id until after the record has been created. So the flow would look like this:
The problem as you may see is that in a After context in an Apex Trigger you cannot update the original record! So in this scenario we cannot update Rec A in the Apex Trigger because we will get a read only error as shown below:

So you may be asking yourself how can we make this work in the Apex Trigger? The answer is quite simple actually but it is not clearly documented to my knowledge. If we create a new instance of an SObject in the Apex Trigger in memory using the Id of the newly created record as provided in the After Trigger context, we can perform an Update DML statement and not get a read only error! This is because in Apex, the SObject is seen as a new reference (even though the records have the same SFDC ID) and therefore is eligible for DML operations! The below snippet of code illustrated this working and not working.

List<Contact> originals = new List<Contact>();
if(mirrorResultMap.values().size() > 0)
{
for(Contact origContact : contactRecs.values())
{
Contact mirrorContact = mirrorResultMap.get(origContact.Id);
//origContact.Linked_Contact__c = mirrorContact.Id; //Link the Original Record tot he Mirror Record WILL FAIL
Contact origContactUpdate = new Contact(Id=origContact.Id, Linked_Contact__c = mirrorContact.Id); //This will WORK
originals.add(origContactUpdate);
}
//update contactRecs.values(); //Update the Records -> THIS WILL FAIL AS ITS ORIGINAL RECORDS IN MEMORY
update originals;
}


With this code in place we will no longer get an error message, and the two records will now be linked! You can see the lookup fields are now populated to reference each other.


This is useful as I said when you want to link two records together to keep them in sync on DML operations. They can be two records of the same type as I illustrated in this example, or you could keep two different objects in sync via lookups if you needed to. Its a handy skill set to know so go forth and code!

Full Code Dump:


Apex Trigger:

trigger ContactTrigger on Contact (after delete, after insert, after undelete
after update, before delete, before insert, before update
{
if(trigger.isAfter)
{
if(trigger.isInsert)
{
ContactTriggerHandler.processAfterInsert(trigger.newMap);
}
}
}


Apex Class (Trigger Handler)

public class ContactTriggerHandler 
{
public static void processAfterInsert(map<Id,Contact> contactRecs)
{
//Query for the existing mirror records
Map<Id,Contact> mirrorContacts = new Map<Id,Contact>([Select c.Id, c.Linked_Contact__c From Contact c where c.Linked_Contact__c in: contactRecs.keySet() ]);
List<Contact> mirrorInserts = new List<Contact>(); 
Map<Id,Contact> updateReciprical = new Map<Id,Contact>();
//Iterate over the Trigger New Records
for(Contact contactRec : contactRecs.values())
{
if(contactRec.Linked_Contact__c == null)
{
Contact mirrorContact = mirrorContacts.get(contactRec.Id);
if(mirrorContact == null) //If this record does not have a linked mirror record, create one and link it to this record
{
Contact c = new Contact(FirstName = contactRec.FirstName, LastName = contactRec.LastName, Description = 'Mirror Record', Linked_Contact__c = contactRec.Id);
mirrorInserts.add(c);
}
}
}
Map<Id,Contact> mirrorResultMap = new Map<Id,Contact>();
if(mirrorInserts.size() > 0)
{
insert mirrorInserts; //After Insert DML, the ID's will be populated
for(Contact mirrorInsert : mirrorInserts) //Store the results in a Map so we can now use these Id's to link the records
{
mirrorResultMap.put(mirrorInsert.Linked_Contact__c,mirrorInsert);
}
}
List<Contact> originals = new List<Contact>();
if(mirrorResultMap.values().size() > 0)
{
for(Contact origContact : contactRecs.values())
{
Contact mirrorContact = mirrorResultMap.get(origContact.Id);
//origContact.Linked_Contact__c = mirrorContact.Id; //Link the Original Record tot he Mirror Record WILL FAIL
Contact origContactUpdate = new Contact(Id=origContact.Id, Linked_Contact__c = mirrorContact.Id); //This will WORK
originals.add(origContactUpdate);
}
//update contactRecs.values(); //Update the Records -> THIS WILL FAIL AS ITS ORIGINAL RECORDS IN MEMORY
update originals;
}
}
}