Work-around for missing “Lookup Relationship” data type custom fields on Task (Activity)

Unlike other standard objects such as Contact and Account, and custom objects, Task (Activity) custom fields cannot be of data type “Lookup Relationship”. (This type allows a relationship to be created with some other object.) So while you can relate a Task to say a Contact through the WhoId field and some other object through the WhatId field, you are stuck if you want to relate the Task to a third object.

Here is a work-around. It uses three custom fields added to Task (Activity):

  • ThirdPartyId – 18 character text field not shown in layout
  • ThirdPartyName – 255 character text field not shown in layout
  • ThirdParty – text formula field shown in layout

The formula produces a basic hyperlink in the Task page:

IF (ThirdPartyId__c != null && ThirdPartyName__c != null, HYPERLINK('/' + ThirdPartyId__c, ThirdPartyName__c, "_self"), null)

(see Hyperlink formula fields for a bit more background) that looks like this:

But there then needs to be a way to choose the related object and all that can be done to allow this is to add a custom button or link to the page. I used a custom button that opens a Visualforce page that in turn allows the related object to be looked up. In my case I wanted to allow either a Contact or an Account to be selected and so needed to use fields of the right lookup type; I re-used an existing managed package custom object to supply those fields but could have created a specific custom object instead. Note that the object instance is never persisted: it just supplies the right meta data for the <apex:inputField>s and the selected id value and name are set back on the Task.

Here is the page:

produced by this Visualforce:

<apex:page standardController="Task" extensions="ChooseThirdPartyController">
    <apex:sectionHeader title="Choose Third Party"/>
    <apex:form >
        <apex:pageBlock mode="edit">
            <apex:pageMessages />
            <apex:pageBlockButtons >
                <apex:commandButton value="Save" action="{!save}" />
                <apex:commandButton value="Cancel" action="{!cancel}" immediate="true" />
            </apex:pageBlockButtons>
            <apex:pageBlockSection title="Task" columns="1">
                <apex:outputField value="{!Task.Subject}"/>
            </apex:pageBlockSection>
            <apex:pageBlockSection title="Third Party" columns="1">
                <apex:inputField value="{!cr.cve__Contact__c}"/>
                <apex:inputField value="{!cr.cve__Account__c}"/>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>

with the logic implemented in this controller extension:

public with sharing class ChooseThirdPartyController {

    private ApexPages.StandardController standardController;

    public ChooseThirdPartyController(ApexPages.StandardController standardController) {
        this.standardController = standardController;
        if (!Test.isRunningTest()) {
            this.standardController.addFields(new String[] {String.valueOf(Task.ThirdPartyId__c)});
        }
    }
    
    private Task t {
        get {
            return (Task) standardController.getRecord();
        }
    }

    // Could use any object here that has the right type of lookup fields on it   
    public cve__ClaimRelationship__c cr {
        get {
            if (cr == null) {
                cr = new cve__ClaimRelationship__c();
                // Default the initial value
                if (t.ThirdPartyId__c != null) {
                    if (t.ThirdPartyId__c.startsWith(Contact.SObjectType.getDescribe().getKeyPrefix())) {
                        cr.cve__Contact__c = t.ThirdPartyId__c;
                    } else if (t.ThirdPartyId__c.startsWith(Account.SObjectType.getDescribe().getKeyPrefix())) {
                        cr.cve__Account__c = t.ThirdPartyId__c;
                    }
                }
            }
            return cr;
        }
        private set;
    }
    
    public PageReference save() {
        if (cr.cve__Contact__c != null && cr.cve__Account__c != null) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'Pick a Contact or an Account but not both'));
            return null;
        } else {
            if (cr.cve__Contact__c != null) {
                Contact c = [select Id, Name from Contact where Id = :cr.cve__Contact__c];
                t.ThirdPartyId__c = c.Id;
                t.ThirdPartyName__c = c.Name;
            } else if (cr.cve__Account__c != null) {
                Account a = [select Id, Name from Account where Id = :cr.cve__Account__c];
                t.ThirdPartyId__c = a.Id;
                t.ThirdPartyName__c = a.Name;
            } else {
                t.ThirdPartyId__c = null;
                t.ThirdPartyName__c = null;
            }
            return standardController.save();
        }
    }
}

Page headers and footers in renderAs=”PDF” Visualforce pages

This page Creating Professional PDF Documents with CSS and Visualforce describes how @page and @bottom-right CSS can be used to include headers and footers in paginated PDF output. But the default styling for the footer text is a largish serifed font. So this:

@page {
    @bottom-right {
        content: "{!documentName} {!claimantInsuredNumbers}/{!claim.Name} - Page " counter(page) " of " counter(pages);
    }
}

results in this in the output bottom right of each PDF page:

which if CSS is being used to style the page content can leave the footer presented in a mis-matching style.

Googling for @bottom-right results in a hit on this page Prince Page Headers and Footers that includes examples of setting styles. Borrowing from that on the assumption that the technique is generic, this change:

@page {
    @bottom-right {
        content: "{!documentName} {!claimantInsuredNumbers}/{!claim.Name} - Page " counter(page) " of " counter(pages);
        font-family: sans-serif;
        font-size: 80%;
    }
}

results in the desired styling:

PS

The :first page CSS pseudo-class is supported allowing e.g. the information to be skipped on the first page:

@page :first {
    @bottom-right {
        /* Nothing */
    }
}

Add “Version Settings” to the check-list…

I have some global code in a managed package that is used by some non-packaged (source) code in a customer’s org. An upgrade of the managed package broke the non-packaged code in a rather confusing way. The runtime error reported was:

System.TypeException: Invalid conversion from runtime type DefaultContactHistorySource to ContactHistorySource

where ContactHistorySource was a recently introduced managed package global interface and DefaultContactHistorySource was a recently introduced non-global implementation of that interface. Neither of these was used directly by the non-packaged code.

The work-around (discovered after many hours of failure) was to simply change the version number of the managed package that the non-packaged code depended on via the “Version Settings” tab. It appears that although ContactHistorySource and DefaultContactHistorySource were not being used directly, some version filtering was nevertheless being applied – perhaps because ContactHistorySource was global. Unfortunately the error that was reported didn’t offer much help in identifying the problem. This is also a rather nasty case of a managed package upgrade breaking existing code.

Other things to watch out for with managed packages are Add “Deployment Status” to the check-list… and Always double check that managed packages are deployed.