Leveling Up for the New Year

One of my biggest goals for 2017 was to pass the Salesforce Platform Developer II certification. I’m thrilled to say that I completed the brand-new certification path over the holidays. Platform Developer II was a very challenging (and fun!) exam that made me stretch myself in deep corners of the Salesforce platform.

Platform Developer II Certification

It’s very exciting to hit this threshold on Salesforce, and I’m eager to start work on the next big challenge.

Distinguishing Custom Settings in Apex

In many respects, the Salesforce API treats Hierarchy and List Custom Settings the same way: their schemata are the same, and most, though not all, static methods apply to both types of settings. This works well in the typical use case of calling MyCustomSetting__c.getInstance(). But suppose you’d like to build generic code that operates on settings. How can you tell list from hierarchy settings to handle them appropriately?

Custom Settings are easily identifiable with Schema.DescribeSObjectResult.isCustomSetting(), but there’s no analogue method for custom setting type. Luckily, there’s two critical differences in field behavior on these objects that yield methods of determining which object is which.

  • For a Hierarchy setting, Name is nillable, while it is required for List settings.
  • Inserting a List Custom Setting with a non-null SetupOwnerId results in a FieldIntegrityException.

Hence, a simple way to tell the two apart, given a DescribeSObjectResult:

 public Boolean isHierarchyCustomSetting(Schema.DescribeSObjectResult s) {
    return s.isCustomSetting() && s.fields.getMap().get('Name').getDescribe().isNillable();
 }

 public Boolean isListCustomSetting(Schema.DescribeSObjectResult s) {
    return s.isCustomSetting() && !s.fields.getMap().get('Name').getDescribe().isNillable();
 }

An alternate, but slower, route is to construct an instance with newSobject() and populate the SetupOwnerId field. A (catchable) exception upon insert identifies the class as a List Custom Setting.

Visualforce Traps for Experienced Programmers

Visualforce has a way of punishing those who make assumptions about its behavior based on other development environments (including Apex and even HTML). I’ve been accumulating a list of Visualforce behaviors and features that seem inexplicable to me at first blush, or that I’ve tripped up on multiple times. Whenever I’m debugging, I check this list of mistakes first. Suggested additions are welcome!

Parameters Not Being Set

The <apex:param> component silently does not set its parameter unless the name attribute is populated, although omitting name is not an error, and the attribute doesn’t make sense for command button parameters.

Race Condition / Incorrect Serialization of Actions

It’s common to need to set a value in the controller and subsequently trigger a partial page re-render. In some cases, there’s no controller action required at all; in others, the business logic may be embedded in a controller setter method rather than a Visualforce action.

The naïve way to handle this is an <apex:commandButton> with one or more <apex:param> elements and a reRender, but no action set. Unfortunately, this causes a difficult-to-debug race condition. The server calls that set the parameters and perform the re-render are not serialized, meaning that the re-render will be working with stale data some, but not all, of the time.

Workaround: an empty action method in the controller. This forces a complete server round-trip prior to beginning the re-render.

Required Non-sObject Fields

It seems obvious to use the required attribute on, for example, <apex:inputText> components to obtain the standard Salesforce UI presentation of a red bar beside the component, when working with non-sObject fields. Unfortunately, this doesn’t work. Instead, you can use a workaround with a styled empty <div>.

Misplaced Labels

<apex:outputLabel> is position-dependent. In order to be rendered with the correct style as part of an <apex:pageBlockSectionItem>, it must precede its for element, and not be nested within that element.

<apex:pageBlockSectionItem>
  <apex:outputLabel for="outValue" value="A Label" />
  <apex:outputText value="{! someControllerValue }" id="outValue" />
</apex:pageBlockSectionItem>

The above shows the correct ordering to receive standard Salesforce styling.

reRender Targets and the rendered Attribute

Showing and hiding page elements based on changes to controller values is a very common workflow. It’s easy to forget that you cannot do this by setting the rendered attribute on a target and then performing a reRender on that very same target. Since the elements with rendered="false" are never even sent to the client, they can’t be re-rendered in the normal way.

Instead, wrap the element with the rendered attribute in an <apex:outputPanel> that you reRender to show and hide its content.

Accessing Visualforce Components in JavaScript.

Accessing Visualforce components in JavaScript using document.getElementById() with $Component is highly unintuitive. id values assigned to Visualforce components aren’t global like they are in HTML, so in many cases you need to qualify them in order to obtain a reference to the component. More confusingly, the manner in which you must so qualify the component reference is partially dependent upon where in the page the JavaScript is located.

Salesforce has several pages that attempt to explain this, but by far the most useful is this set of examples.

Where else is Visualforce likely to trip up coders trying to apply experience from other environments?

Free Events in Click & Pledge

Click & Pledge nicely supports online registration for both free and paid/ticketed events. However, one point of confusion for both users and staff is that a “$0.00” line item is displayed on the registration page even for free events, and for free events using the named event model, a “Payment Information” section is displayed on the second page of the registration UI.

Fortunately, C&P offers the ability to include custom CSS in the template designer. Some simple CSS suppresses these payment-related elements, granting a more streamlined free registration experience for both named and anonymous events. The effect isn’t perfect, but until Click & Pledge permits deeper modification of its registration component, I’m satisfied with the results. Enhancements are welcome!

Bridging Click & Pledge and Salesforce Campaigns with Process and Flow

This post has been extensively revised to broaden the applicability of the solution, covering anonymous and named Click & Pledge events.

Click & Pledge provides functionality to add contacts who register for an event to a Salesforce campaign. However, contacts are added with the first available status (often ‘Sent’), rather than a value showing them as registered or attended. Because campaigns provide a standard, deduplicated, reportable data architecture that’s independent of registration package, it’s valuable to propagate C&P status updates into the associated campaigns. Further, integrating registration information into campaigns increases the value of the Campaign History related list provided on every contact, offering an at-a-glance summary of recent invitations and responses thereto.

At the same time, we can add value to the Campaign Member object and support common reporting needs by marking who’s attending each event as a guest of whom, using a custom lookup field Registered by to the Contact on the Campaign Member object.

Because both of the registration objects used by Click and Pledge (C&P Event Registered Attendee and C&P Event Registrant) may be associated with C&P Temporary Contacts that can be linked to Contacts in any order, at different times, and via asynchronous process, we use an adaptable structure with two processes and two autolaunched flows. The processes and flows create and update Campaign Members incrementally as information becomes available, and re-run on each modification to the registration objects to propagate updates.

It’s also important to take into account anonymous events. Click & Pledge creates C&P Registered Attendee records for anonymous registrations, but they’re never linked to contacts — only the C&P Event Registrant is. Our flows take this variation into account.

Process 1: Registered Attendees

Triggered on the C&P Event Registered Attendee object upon creation or modification.

Because we cannot rely on a Contact ever being linked to the registered attendee (in the case of anonymous registrations), the process’s single action group is run without criteria. An update to a registered attendee without a contact attached may stem from a check-in event.

The process simply invokes Flow 1 with the Registered Attendee as its parameter. The flow is responsible for extracting the other required information from the object hierarchy.

Process 2: Registrants

Triggered on the C&P Event Registrant object upon creation or modification.

Even in the case of anonymous events, a Contact must ultimately be assigned to the registrant. This process therefore has a single action group criterion, [CnP_PaaS_EVT__Event_registrant_session__c].CnP_PaaS_EVT__ContactId__c is not null.

The only action is Run Flow, triggering Flow 2 with the Registrant ID as a parameter. This process is necessary not only to update the Registered by field, but also to allow our flows to source a Contact value from the registrant object for anonymous events.

Flow 1: Update Campaign Members

Called by Process 1 and Flow 2

This flow contains the meat of the functionality. It accepts one input variable, holding the ID of the C&P Event Registered Attendee. The flow has the following structure.

Update Campaign Members flow

Four Fast Lookup elements assign the C&P Event Registered Attendee, C&P Event, Campaign Member, and C&P Event Registrant objects to sObject variables. Note that the registered attendee is guaranteed not to be null by the calling process, but the other objects are nullable, and the registrant will often be null dependent upon the user’s processing of temporary contacts.

The flow uses five formulas to draw data from these four objects. Note that because the object hierarchy involves many nullable lookup fields and connections to contacts that may be populated in any order, we guard cross-object field references with null checks.

CampaignId

Evaluates to the Campaign assigned to the registration level, if any, or to that assigned to the event, if any, or null.

IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__Registration_level__c}) && !ISBLANK({!RegistrationLevelSobject.CnP_PaaS_EVT__Campaign__c}), {!RegistrationLevelSobject.CnP_PaaS_EVT__Campaign__c},
IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__EventId__c}) && !ISBLANK({!EventSobject.CnP_PaaS_EVT__Campaign__c}), {!EventSobject.CnP_PaaS_EVT__Campaign__c}, null))

ContactId

Evaluates to the Contact assigned to the registered attendee, unless the event is marked as anonymous, in which case it evaluates to the contact of the registrant. May be null if neither contact has been assigned.

IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__EventId__c}) && {!EventSobject.CnP_PaaS_EVT__Anonymous__c} && !ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__Registrant_session_Id__c}) && !ISBLANK({!Registrant.CnP_PaaS_EVT__ContactId__c}), {!Registrant.CnP_PaaS_EVT__ContactId__c}, {!RegisteredAttendeeSobject.CnP_PaaS_EVT__ContactId__c})

RegistrantId

Evaluates to the Contact assigned to the Registrant, unless it is the same as the Contact of the attendee. Used to populate the Registered by field.

IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__Registrant_session_Id__c}) && !ISBLANK({!Registrant.CnP_PaaS_EVT__ContactId__c}) && {!Registrant.CnP_PaaS_EVT__ContactId__c} != {!ContactId}, {!Registrant.CnP_PaaS_EVT__ContactId__c}, null)

CMStatusExisting

Evaluates to the campaign member status value to use if an existing Campaign Member is located. It will not overwrite an ‘Attended’ value; this copes with check-in events performed on the multiple registered attendees that may be linked to a single contact and registrant for anonymous events.

IF({!RegisteredAttendeeSobject.CnP_PaaS_EVT__CheckIn_Status__c} = 'Checked-In', 'Attended', IF(!ISPICKVAL({!CM.Status}, 'Attended'), 'Registered', TEXT({!CM.Status})))

CMStatusNew

Evaluates to the campaign member status value to use if a new Campaign Member is being created.

IF({!RegisteredAttendeeSobject.CnP_PaaS_EVT__CheckIn_Status__c} = 'Checked-In', 'Attended', 'Registered')

Structure

Because the calling processes cannot guarantee that appropriate information will be available in the object hierarchy in all circumstances, the flow uses a decision element to decide whether to proceed; it continues if both ContactId and CampaignId are non-null.

These conditions being met, if the Campaign Member is null, a new record is created and inserted, using CMStatusNew, ContactId, CampaignId, and RegistrantId. If not, the Campaign Member is updated, and CMStatusExisting is used.

If the registrant has not yet been populated (for a named event), the Registered by field will be populated when Process 2 and Flow 2 fire.

Flow 2: Update Registered Attendees

Called by Process 2, calls Flow 1

This flow is required for handling anonymous registrations (where a contact is assigned only to the registrant object) and for populating the “Registered by” field on the Campaign Member in the circumstance that the C&P Temporary Contact corresponding to the registrant is processed after one or more of those corresponding to the associated attendees. It accepts as input parameter the ID of the modified C&P Event Registrant object. The flow has the following structure.

Update Registered Attendees flow

A Fast Lookup assigns all C&P Registered Attendees linked to the registrant to an sObject collection variable. A loop iterates over the collection and invokes Flow 1 with the required parameter. No criteria are applied to the registered attendees; all evaluation is handled in Flow 1.

Summary

Processes and Flows offer an easy way to migrate Click and Pledge registration information to campaigns in real time. Unifying information about event registrations with campaigns enhances reportability, particularly in a context where multiple registration packages or types of event are in use (Click and Pledge, Google Forms, small events without online registrations), and allows staff to easily inspect event registrations on the Contact record, even without access to Click & Pledge.