“New” action pre-processing override results in “Error: Invalid Data…” on “Save & New”

You can create a custom page and controller and then hook them up to the “New” action for an object – see e.g. Overriding a Standard Button. One situation where this is useful is where you want to do some of your own processing and then forward on to the default UI page.

Here is the sort of page you might use for this purpose:

<apex:page
    standardController="MyCustomObject__c"
    extensions="MyCustomObjectController"
    action="{!url}
    />

and controller:

public with sharing class MyCustomObjectController {

  // Controller extension
  public MyCustomObjectController(ApexPages.StandardController ignored) {
  }

  public PageReference url() {

    // Custom processing logic goes here

    // Standard URL pattern for "New"
    String keyPrefix = MyCustomObject__c.SObjectType.getDescribe().getKeyPrefix();
    PageReference newPr = new PageReference('/' + keyPrefix + '/e');

    // Preserve any query string parameters
    Map<String, String> oldParams = ApexPages.currentPage().getParameters();
    Map<String, String> newParams = newPr.getParameters();
    newParams.putAll(oldParams);

    // Avoid an infinite loop - go to default UI not back to this controller
    newParams.put('nooverride', '1');

    return newPr;
  }
}

This works well when “Save” is clicked in the resulting default UI page. But when “Save & New” is clicked, this (confusing) error message results:

Error: Invalid Data.
Review all error messages below to correct your data.
The page you submitted was invalid for your session. Please click Save again to confirm your change.

After much stripping back of the controller code and comparisons with the typical URL patterns, the problem turns out to be that in the “Save & New” case a save_new parameter is added for part of the cycle but that should not be present in the URL of the final page that is presented.

Adding this line of code (or otherwise making sure the parameter is not present in the final URL) fixes the problem:

    newParams.remove('save_new');

FullCalendar using a JavaScript remoting callback

FullCalendar quick start illustrates a simple way to get FullCalendar working in a Visualforce page. All the event data was just emitted as JSON text in the generated page. But FullCalendar is typically used where the date range being displayed can be changed making that approach impractical. Two alternate ways to supply the event data are supported:

  • via a json feed – a GET url is configured
  • via a function – an arbitrary piece of JavaScript is configured

Both of these are passed the date range that is required for the calendar so that only the relevant events can be returned. The second of these provides more opportunity to do processing on the client side and is the approach described below.

Here is a simplified version of the Apex controller. The Event class models the values required for each event by FullCalendar and Apex automatically serializes the returned list of these as JSON because of the RemoteAction annotation. The arguments to the events method are two Apex Date primitives and an SObject (whose fields are displayed in the page and provide additional control over the calendar content):

public with sharing class CalendarController {
    
    public class Event {
    
        public String title;
        public String tip;
        public DateTime starts;
        public DateTime ends;
        
        public Event(String title, String tip, DateTime starts, DateTime ends) {
            this.title = title;
            this.tip = tip;
            this.starts = starts;
            this.ends = ends;
        }
    }

    // Page calls back into this
    @RemoteAction
    public static Event[] events(Date starts, Date ends, Filter__c filter) {

        System.debug('>>> starts=' + starts + ' ends=' + ends + ' filter=' + filter);

        List<Event> events = new List<Event>();

        // Loop goes here here where query results are turned into Event objects

        return events;
    }
}

Here is the change to what is assigned to “events” in the Visualforce page. Each time FullCalendar requires event data it invokes this JavaScript function. The function obtains values from the SObject fields in the page and passes those together with the start and end dates to the controller via the standard remote action call that automatically serializes the request as JSON. The returned results are passed into FullCalendar by invoking the callback function supplied in the original function call:

events: function(start, end, callback) {
    
    // JQuery references to the hidden id fields for lookups
    // and also to checkbox fields assigned to fields whose
    // names match the Filter__c SObject
    var filter = {
        Booking__c: bookingLkid.val(),
        Class__c: clazzLkid.val(),
        ShowAvailable__c: showAvailable.prop('checked'),
        ShowBooked__c: showBooked.prop('checked')
    };
    
    // JavaScript dates need formatting to be accepted at the Apex side
    Visualforce.remoting.Manager.invokeAction(
        '{!$RemoteAction.CalendarController.events}',
        start.toUTCString(),
        end.toUTCString(),
        filter, 
        function(result, event) {
            if (event.status) {
                // Jobs done here:
                // 1) In Apex "end" is a reserved word so using "ends"; change back
                // 2) The Apex DateTime is serialized as milliseconds; change to Date
                // 3) The HTML < and > are replaced by entities; turn back
                for (var i = 0; i < result.length; i++) {
                    var r = result[i];
                    r.start = new Date(r.starts);
                    r.end = new Date(r.ends);
                    r.tip = r.tip.replace(/&lt\;/g, '<').replace(/&gt\;/g, '>');
                }
                // Invoke FullCalendar function
                callback(result);
            } else {
                alert('ERROR:\n' + event.message + '\n' + event.where);
            }
        });
}

Gotchas along the way were (API 25.0):

  • Although the remote class and method being invoked are explicit in the call, if the class isn’t the controller or an extension, a page build error of “No remoted actions found to resolve…” results.
  • The automatic serialization of Date appears broken in both directions: see the JavaScript above for work-arounds; there may be similar issues with other types.
  • As the JSON serialization can’t be configured its awkward to generate JSON that includes any Apex reserved words.
  • The “tip” string is HTML generated by the Apex code and so needed some manipulation on the client-side to remove the encoding introduced by the serialization.
  • The Force.com documentation is short on details and examples so expect to spend time debugging to figure out what is actually happening.

Finally I’d like to mention that adding a tooltip to FullCalendar took less than 5 minutes and worked first time: all that was needed was the qTip JavaScript and this small addition:

eventRender: function(event, element) {
    element.qtip({
        content: event.tip
    });
}

Only some types work as Apex map keys from Visualforce

The “Referencing Apex Maps and Lists” section of the Visualforce Developer’s Guide provides examples of how to reference data held in an Apex map in the controller from a Visualforce page. I haven’t found examples where the key values used have been other than strings or integers and the documentation is not explicit. But now that there is support for Non-primitive Types in Map Keys and Sets, I thought it would be worth trying a DateTime value as a map key in a page I have that heavily uses DateTime objects.

But DateTime as a map key type does not work from Visualforce:

Incorrect parameter type for subscript. Expected java.lang.Class, received DateTime

To get some sense of what does and what does not work I created the controller and page listed at the end of this post (both set to API 26.0). I found two further failing map key types.

Time as a map key type results in:

Unsupported type shared.xml.soap.Time encountered.

A custom Apex class as a map key type results in:

Map key Custom:[value=987] not found in map

i.e. value semantics are not supported (but the generally less useful reference semantics are).

I’ll create a case with Salesforce about the types that don’t work.

<apex:page controller="MapTestController">
  <h1>Map Test</h1>
  <br/><apex:outputText value="{!stringKeyMap[stringKey]}"/>
  <br/><apex:outputText value="{!integerKeyMap[integerKey]}"/>
  <br/><apex:outputText value="{!longKeyMap[longKey]}"/>
  <br/><apex:outputText value="{!decimalKeyMap[decimalKey]}"/>
  <br/><apex:outputText value="{!dateKeyMap[dateKey]}"/>
  <br/><apex:outputText value="{!customByReferenceKeyMap[customByReferenceKey]}"/>
  <!--
  <br/><apex:outputText value="{!timeKeyMap[timeKey]}"/>
  -->
  <!--
  <br/><apex:outputText value="{!dateTimeKeyMap[dateTimeKey]}"/>
  -->
  <!--
  <br/><apex:outputText value="{!customByValueKeyMap[customByValueKey]}"/>
  -->
</apex:page>
public class MapTestController {

    public String stringKey {
        get {
            return 'key';
        }
    }
    public Map<String, String> stringKeyMap {
        get {
            return new Map<String, String>{stringKey => 'string value'};
        }
    }
    
    public Integer integerKey {
        get {
            return 1234;
        }
    }
    public Map<Integer, String> integerKeyMap {
        get {
            return new Map<Integer, String>{integerKey => 'integer value'};
        }
    }
    
    public Long longKey {
        get {
            return 1234;
        }
    }
    public Map<Long, String> longKeyMap {
        get {
            return new Map<Long, String>{longKey => 'long value'};
        }
    }
    
    public Decimal decimalKey {
        get {
            return 3.25;
        }
    }
    public Map<Decimal, String> decimalKeyMap {
        get {
            return new Map<Decimal, String>{decimalKey => 'decimal value'};
        }
    }
    
    public Date dateKey {
        get {
            return Date.newInstance(2012, 1, 1);
        }
    }
    public Map<Date, String> dateKeyMap {
        get {
            return new Map<Date, String>{dateKey => 'date value'};
        }
    }
    
    public Time timeKey {
        get {
            return Time.newInstance(8, 20, 30, 0);
        }
    }
    public Map<Time, String> timeKeyMap {
        get {
            return new Map<Time, String>{timeKey => 'time value'};
        }
    }
    
    public DateTime dateTimeKey {
        get {
            return DateTime.newInstance(2012, 1, 1, 0, 0, 0);
        }
    }
    public Map<DateTime, String> dateTimeKeyMap {
        get {
            return new Map<DateTime, String>{dateTimeKey => 'date time value'};
        }
    }
    
    public class Custom {
        private Integer value;
        public Custom(Integer value) {
            this.value = value;
        }
        public Boolean equals(Object other) {
            return this.value == ((Custom) other).value;
        }
        public Integer hashCode() {
            return value;
        }
    }
    
    private static final Custom customInstance = new Custom(321);
    public Custom customByReferenceKey {
        get {
            return customInstance;
        }
    }
    public Map<Custom, String> customByReferenceKeyMap {
        get {
            return new Map<Custom, String>{customByReferenceKey => 'custom (by reference) value'};
        }
    }
    
    public Custom customByValueKey {
        get {
            return new Custom(987);
        }
    }
    public Map<Custom, String> customByValueKeyMap {
        get {
            return new Map<Custom, String>{customByValueKey => 'custom (by value) value'};
        }
    }
}

FullCalendar quick start

I was pleasantly surprised at how painlessly Adam Shaw’s FullCalendar worked in a Visualforce page so am posting a basic controller and page here to help others get started. This is the output they produce (with of course all the hard work done by FullCalendar):

The controller emits hard-coded data: a real controller would build the data from various SObjects and provide actions to be invoked from the client-side JavaScript:

public with sharing class CalendarDemoController {
    public String events {
        get {
            JSONGenerator g = JSON.createGenerator(true);
            g.writeStartArray();
            for (Integer i = -2; i <= 2; i++) {
                g.writeStartObject();
                g.writeStringField('title', 'Event ' + i);
                g.writeDateTimeField('start', DateTime.now().addDays(i).addHours(i));
                g.writeDateTimeField('end', DateTime.now().addDays(i).addHours(i + 1));
                g.writeEndObject();
            }
            g.writeEndArray();
            return g.getAsString();
        }
    }
}

The fullcalendar-1.5.4.zip download is setup as a single static resource with the various pieces of content accessed via their paths in the Visualforce page. The calendar is rendered using a mixture of static configuration and the events JSON string provided by the controller:

<apex:page controller="CalendarDemoController" tabStyle="Contact">

<link href="{!URLFOR($Resource.fullcalendar,'fullcalendar-1.5.4/fullcalendar/fullcalendar.css')}" rel="stylesheet" type="text/css"/>
<link href="{!URLFOR($Resource.fullcalendar,'fullcalendar-1.5.4/fullcalendar/fullcalendar.print.css')}" rel="stylesheet" type="text/css" media="print"/>
<script src="{!URLFOR($Resource.fullcalendar, 'fullcalendar-1.5.4/jquery/jquery-1.8.1.min.js')}" type="text/javascript" language="javascript"></script>
<script src="{!URLFOR($Resource.fullcalendar, 'fullcalendar-1.5.4/fullcalendar/fullcalendar.min.js')}" type="text/javascript" language="javascript"></script>

<apex:sectionHeader title="Calendar Demo"/>

<!-- Calendar is rendered here by the fullcalendar Javascript -->
<apex:pageBlock >
    <div id='calendar'/>
</apex:pageBlock>

<!-- Fullcalendar configuration plus event data supplied by controller -->
<script type="text/javascript" language="javascript">
$(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
            left: 'title',
            center: 'month,agendaWeek,agendaDay',
            right: 'prevYear prev,today,next nextYear'
        },
        defaultView: 'agendaWeek',
        allDayDefault: false,
        minTime: 8,
        maxTime: 21,
        events:
{!events}
    });
});
</script>
</apex:page>

“Combobox” input in Visualforce

To quote the Force.com documentation:

A combobox is a picklist that also allows users to type a value that is not already specified in the list.

A picklist field itself stores a string value and does not enforce that the value stored is one of the picklist entry values. The “combobox” presentation just makes this flexibility visible in the UI. Here is an example in the Task editing page:

For some reason this presentation type hasn’t been included in the field types that can be specified for custom fields so it isn’t possible to get this sort of editing capability via default (layout-based) UI. But it can be created using Visualforce, and here is one way to do that.

My requirement was a bit more complicated than a basic combobox in that the list of values to be offered also depended on another field on the page. Also to save development work and to make the interface a little cleaner I didn’t use a pop-up window. Here is an example of the resulting page:

When the “Type” field is changed alternate candidate “Document Name” links are presented and when a link is clicked the text is transferred to the “Document Name” field. The square brackets around each candidate is copied from how the default date is presented next to a date field. The user can of course enter any other “Document Name” value they like by just typing into the field directly.

Here is the Visualforce with the Javascript that uses JSON data provided by the controller to determine what links to write into the HTML to the right of the “Document Name” field:

<apex:page id="p" standardController="cve__Document__c" extensions="DocumentController">
    <apex:sectionHeader title="Document Edit" subtitle="New Document" />
    <apex:pageMessages />
    <apex:form id="f">
        <apex:pageBlock id="pb" title="Document Edit" mode="edit">
            <apex:pageBlockButtons >
                <apex:commandButton value="Save" action="{!save}"/>
                <apex:commandButton value="Cancel" action="{!cancel}"/>
            </apex:pageBlockButtons>
            <apex:pageBlockSection id="pbs" title="Information" columns="1">
                <apex:inputField value="{!cve__Document__c.cve__Claim__c}"/>
                <apex:inputField id="type" value="{!cve__Document__c.cve__Type__c}" onchange="cvOfferOptions();"/>
                <apex:pageBlockSectionItem id="pbsi">
                    <apex:outputLabel value="{!$ObjectType.cve__Document__c.fields.Name.label}"/>
                    <apex:panelGrid columns="2">
                        <apex:inputField id="name" value="{!cve__Document__c.Name}" style="width: 20em" required="true"/>
                        <apex:outputPanel id="panel">
                        </apex:outputPanel>
                    </apex:panelGrid>
                </apex:pageBlockSectionItem>
                <apex:inputField value="{!cve__Document__c.cve__Required__c}"/>
                <apex:inputField value="{!cve__Document__c.cve__Received__c}"/>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
<script type="text/javascript">
function cvDocuments() {
    var documentNames = {!documentNamesJson};
    var type = document.getElementById('p:f:pb:pbs:type');
    var selectedType = type.options[type.selectedIndex].text;
    return documentNames[selectedType];
}
function cvOfferOptions() {
    var documents = cvDocuments();
    var html = '';
    if (documents != null) {
        for (var i = 0; i < documents.length; i++) {
            html += ''
                    + '<div>[ <a href="javascript:cvPickValue('
                    + i
                    + ');" title="Click to set as \'Document Name\'">'
                    + documents[i]
                    + '</a> ]</div>'
                    ;
        }
    }
    var panel = document.getElementById('p:f:pb:pbs:pbsi:panel');
    panel.innerHTML = html;
}
function cvPickValue(index) {
    var name = document.getElementById('p:f:pb:pbs:pbsi:name');
    name.value = cvDocuments()[index];
}
function cvMakeDoubleDelegate(function1, function2) {
    return function() {
        if (function1) {
            function1();
        }
        if (function2) {
            function2();
        }
    }
}
window.onload = cvMakeDoubleDelegate(window.onload, cvOfferOptions);
</script>
</apex:page>

The controller extension is just responsible for providing the JSON string, with the data access and “save” and “cancel” actions left to the standard controller:

public with sharing class DocumentController {

    private ApexPages.StandardController standardController;

    public DocumentController(ApexPages.StandardController standardController) {
        this.standardController = standardController;
    }

    // This controller extension just supplies this data with all other functionality using the standard controller
    public String documentNamesJson {
        get {
            Map<String, List<String>> m = new Map<String, List<String>>();
            m.put('Legal', new String[] {
                    'Appeal',
                    'Complaints',
                    'Other'
                    });
            m.put('Medical', new String[] {
                    'Attending Physician\'s Statement',
                    'Medical Records',
                    'Medical Reviews',
                    'Pharmacy',
                    'Toxicology',
                    'Autopsy/Coroner\'s Report',
                    'Other'
                    });
            m.put('Financial', new String[] {
                    'Income Records',
                    'Interest Calculation',
                    'Collection Referral',
                    'Refund',
                    'Other'
                    });
            return json(m);
        }
    }
    
    private String json(Object o) {
        JSONGenerator generator = JSON.createGenerator(false);
        generator.writeObject(o);
        return generator.getAsString();
    }
}

Note that while in this example the candidate values are expressed in Apex code, DescribeFieldResult.getPicklistValues can be used to obtain the entry values defined for a picklist field.

Keeping track of the selected tab of an apex:tabPanel in the controller – using Javascript remoting

While this Keeping track of the selected tab of an apex:tabPanel in the controller implementation works, taking a look at the “Network” view of Chrome’s “Developer Tools” brings the bad news that the call to the server made when a tab is clicked on involves (in my case) a 700k byte data transfer and an elapsed time of around 3 seconds. This is a lot of overhead to just transmit the name of the selected tab to the server. This cost and time delay is not immediately apparent because the browser remains responsive. Looking at the payload sent to the server, the problem is that the full view state is being sent as part of the request.

The “writing the name of that tab back to the controller” technique I should have used in the first place is JavaScript remoting. Changing to that reduces the data transfer size to a few hundred bytes and the elapsed time to a few hundred milliseconds.

Here is the changed Visualforce and Apex:

<apex:page id="p" standardController="Xyz__c" extensions="XyzController">
    <apex:form id="f">
        <apex:tabPanel id="tp" switchType="client" value="{!selectedTab}" onclick="setSelectedTabOnController()">
            <apex:tab name="Tab1" label="Tab1" id="Tab1">
                <!-- Tab content -->
            </apex:tab>
            <apex:tab name="Tab2" label="Tab2" id="Tab2">
                <!-- Tab content -->
            </apex:tab>
        </apex:tabPanel>
    </apex:form>
<script type="text/javascript">
function getSelectedTabName() {
    if (RichFaces) {
        var tabs = RichFaces.panelTabs['p:f:tp'];
        for (var i = 0; i < tabs.length; i++) {
            var tab = tabs[i];
            if (RichFaces.isTabActive(tab.id + '_lbl')) {
                return tab.name;
            }
        }
    }
    return null;
}
function setSelectedTabOnController() {
    // Invoke server remote action method
    XyzController.updateLastViewedTab('{!Xyz__c.Id}', getSelectedTabName(), callbackHandler)
}
function callbackHandler(result, event) {
    if (event.type == 'exception') {
        alert(event.message);
    }
}
</script>
</apex:page>
global with sharing class XyzController  {
    private Xyz__c xyz;
    @RemoteAction
    global static void updateLastViewedTab(Id xyzId, String tabName) {
    	upsert new Xyz__c(Id = xyzId, LastViewedTab__c = tabName);
    }
    public String selectedTab {
        get {
            if (selectedTab == null) {
                if (xyz.LastViewedTab__c != null) {
                    selectedTab = xyz.LastViewedTab__c;
                } else {
                    selectedTab = 'Tab1';
                }
            }
            return selectedTab;
        }
        // Setter has to be public as some (but not all) actions do result in an update
        set;
    }
    // Other code...
}

Keeping track of the selected tab of an apex:tabPanel in the controller

PS Read the next few paragraphs for some context but then move on to the better implementation described in Keeping track of the selected tab of an apex:tabPanel in the controller – using Javascript remoting.

I have a heavily used page that presents a dozen or so fairly complex panels using apex:tab/apex:tabPanel. The controller can set the tab that is initially displayed by providing a property that is used for the “value” (not the “selectedTab”) of the apex:tabPanel. But I also want tab selections made by the user to be recorded back on the controller (and then persisted). This is to improve usability so that the next time the user returns to the page they can continue where they left off. This approach also avoids the need to encode the current tab selection in saveURL and retURL parameters that are part of links to allow child objects to be added or edited and then the same tab returned to when a Save or Cancel is done.

One approach I found suggested is to use a “switchType” of “server” or “ajax” instead of “client”. For my page using either of these settings changed the instantaneous tab switch of the “client” setting to a glacial and feedback-less several second delay in the tab switch. I also found postings that implied that “client” would write the current tab name back to the controller but in my tests it did not.

If you know of a recently tested platform-supported way to do this please comment. Meanwhile here is what I ended up doing.

The problem has two parts:

  • finding which tab is currently selected
  • writing the name of that tab back to the controller

Solving the first problem means inspecting the platform’s implementation classes and so will break if they change. Solving the second problem makes use of published platform functionality and Matt Lacey’s apex:actionFunction and apex:param – Or How to Pass Values from Javascript to an Apex Controller.

Here is an outline of the Visualforce:

<apex:page id="p" standardController="Xyz__c" extensions="XyzController">
    <apex:form id="f">
        <apex:tabPanel id="tp" switchType="client" value="{!selectedTab}" onclick="setSelectedTabOnController()">
            <apex:tab name="Tab1" label="Tab1" id="Tab1">
                <!-- Tab content -->
            </apex:tab>
            <apex:tab name="Tab2" label="Tab2" id="Tab2">
                <!-- Tab content -->
            </apex:tab>
        </apex:tabPanel>
        <apex:actionFunction id="af" name="selectTabActionFunction" action="{!selectTab}" reRender="">
            <apex:param name="selectedTab" assignTo="{!selectedTab}" value="{!selectedTab}"/>
        </apex:actionFunction>
    </apex:form>
<script type="text/javascript">
function getSelectedTabName() {
    if (RichFaces) {
        var tabs = RichFaces.panelTabs['p:f:tp'];
        for (var i = 0; i < tabs.length; i++) {
            var tab = tabs[i];
            if (RichFaces.isTabActive(tab.id + '_lbl')) {
                return tab.name;
            }
        }
    }
    return null;
}
function setSelectedTabOnController() {
    selectTabActionFunction(getSelectedTabName());
}
</script>
</apex:page>

and an outline of the corresponding controller Apex:

public with sharing class XyzController  {
    private Xyz__c xyz;
    // "Get" called when page rendered; "set" called by client-side Javascript
    public String selectedTab {
        get {
            if (selectedTab == null) {
                if (xyz.LastViewedTab__c != null) {
                    // Select the last saved tab
                    selectedTab = xyz.LastViewedTab__c;
                } else {
                    // Select a default tab
                    selectedTab = 'Tab1';
                }
            }
            return selectedTab;
        }
        set {
            if (value != selectedTab) {
                // Persist just the tab change
                upsert new Xyz__c(Id = xyz.Id, LastViewedTab__c = value);
            }
            selectedTab = value;
        }
    }
    // Method for client-side Javascript to invoke; does nothing
    public PageReference selectTab() {
        return null;
    }
    // Other code...
}

Note that the “onclick” event on apex:tabPanel happens after the selected tab has already switched ensuring it is the new tab name that is written to the controller not the old tab name. And the performance of the write to the controller is good enough that the tab switch remains pretty much instantaneous.

Visualforce dynamic binding works for table columns

I was pleasantly surprised this week to find out that some customization that I needed to implement – allowing the set of columns in an apex:pageBlockTable to be specified in a custom setting – could be implemented in Visualforce using the most obvious solution:

<apex:pageBlockTable var="c" value="{!contacts}">
    <apex:repeat var="f" value="{!fields}">
        <apex:column value="{!c[f]}"/>
    </apex:repeat>
</apex:pageBlockTable>

where “contacts” is a list of Contact objects and “fields” is a list of Contact field names obtained from a custom setting holding comma separated values. (The query that populates “contacts” must also use the field names to avoid the “SObject row was retrieved via SOQL without querying the requested field” error.)

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: