4 Comments

You have probably come up with a scenario where you need to create a custom Visualforce page for a custom object and have that page available for both the browser and the Salesforce1 application. Right now there’s no easy way to tell Salesforce to use a specific page for the browser version and another different page for SF1. Let’s focus the problem from the UI/UX perspective: how to leverage the UI and best practices associated to each specific platform. So basically, if we use the Visualforce tags to build for the browser then the SF1 experience will not be the desired one (it will actually be very ugly!), and if we decide to not use Visualforce tags and instead use pure HTML5 (as recommended by the Salesforce1 practices) then the page will look good in SF1 but for browser users it will look different. We want to create a Visualforce page that will change its look depending on the client used to display it. Here’s an example

Same page for both browser and SF1

On the image you can see how the UI for each client is maintained. Also notice that in the case of SF1 we are using the standard publisher buttons to work with the page. Same page, same controller, but different UI and UX depending on the client. How we achieve this? Read on.

An Example

Let’s use an example to illustrate the problem. In our example we’re developing a questionnaire application: users are able to create questionnaires which will have many questions associated. This is an example of a questionnaire:

questionnaire

The questionnaire could be associated to any object, and we want to be able to answer a specific questionnaire by using both the browser client and the Salesforce1 app. Since the number of questions may vary, we need to build a Visualforce page that dynamically displays the questions associated to a specific questionnaire (we could not do this with a standard layout). Our Visualforce page will have a controller extension associated that will drive the logic of the page. We will associate a questionnaire to a Contact for this example.

Here is our controller extension:

public with sharing class ceContactQuestionnaire {
    private final ApexPages.StandardController controller;
    private final Contact contactRecord;
    
    public ceContactQuestionnaire(ApexPages.StandardController controller) {
        this.controller = controller;
        controller.addFields(new String[] {sObjectType.Contact.fields.Questionnaire__c.Name});        
        this.contactRecord = (Contact)controller.getRecord();
    }
    
    public String questionnaireName {
        get {
            if(questionnaireName == null && contactRecord.Questionnaire__c != null) {
                List<Questionnaire__c> q = [select Name from Questionnaire__c where Id = :contactRecord.Questionnaire__c];
                if(q.size() > 0) questionnaireName = q[0].Name;
            }
            return questionnaireName;
        }
        private set {
            questionnaireName = value;
        }
    }    

    public List<Contact_Question__c> questions {
        get {
            if(questions == null) {
                questions = new List<Contact_Question__c>();
                for(Contact_Question__c question : [select Id,Question__c,Answer__c from Contact_Question__c where Contact__c = :contactRecord.Id order by Order__c]) {
                    questions.add(question);
                }                    
            }
            return questions;
        }
        set {
            questions = value;
        }
    }
    
    public PageReference save() {
        upsert questions;
        return controller.save();
    }      
}

You can see is actually a pretty simple controller: the contact has a questionnaire associated and an auxiliary object with all the questions for the questionnaire. We get those questions and we make it available as a property. The extension also overrides the save method to save the answers given to those questions.

Let’s first design the page by using the Visualforce tags that we’ll use on a browser version of the page:

<apex:page standardController="Contact" extensions="ceContactQuestionnaire" >
    
    <apex:pageMessages />
    <apex:sectionHeader title="{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName}" subtitle="{!Contact.Name}" />
    <apex:form >
        <apex:pageBlock >
            <apex:pageBlockButtons >
                <apex:commandButton action="{!save}" value="Save" styleClass="btn btn-default" />
                <apex:commandButton action="{!cancel}" value="Cancel" styleClass="btn btn-default" />
            </apex:pageBlockButtons>
            <apex:pageBlockSection columns="1">
                <apex:repeat var="question" value="{!questions}">
                    <apex:pageBlockSectionItem >
                        <apex:outputLabel value="{!question.Question__c}" />
                        <apex:inputTextarea cols="80" rows="3" value="{!question.Answer__c}"/>
                    </apex:pageBlockSectionItem>
                </apex:repeat>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>

This is a pretty simple Visualforce page, we just render each question we get from the controller as a label for the question and a text area for the answer. This is how it looks on the browser:

rendering on browser

No surprises here: we’re using the familiar UI and look&feel of a Salesforce layout. Let’s make our Visualforce page available for SF1 (edit the page and select the option “Available for Salesforce mobile apps”) so we can open the page in SF1. You’ll notice that the UI and look&feel are pretty ugly:

rendering on SF1

If we present mobile users with this layout they will probably complain about it and not feel happy. So how do we fix this?

Determine what Client is Being Used

We need a way to know which client is using our Visualforce page so we can present the appropriate content and user experience. There is no official way to know this, but there are some tricks. Here I will use the parameters of the page to know whether I’m in a browser or in the SF1 app. We need a property in the controller extension:

public Boolean isSF1 {
    get {                    
        if(String.isNotBlank(ApexPages.currentPage().getParameters().get('sfdcIFrameHost')) ||
            String.isNotBlank(ApexPages.currentPage().getParameters().get('sfdcIFrameOrigin')) ||
            ApexPages.currentPage().getParameters().get('isdtp') == 'p1' ||
            (ApexPages.currentPage().getParameters().get('retURL') != null && ApexPages.currentPage().getParameters().get('retURL').contains('projectone') )
        ) {
            return true;
        }else{
            return false;
        }
    }
}

I will use this property to dynamically adjust the UI of the Visualforce page.

One Page for All

For the UI I will use a special version of Bootstrap for Salesforce1, which I have added as a static resource. This is how the page looks now:

<apex:page standardController="Contact" extensions="ceContactQuestionnaire" docType="html-5.0" tabStyle="Questionnaire__c">
    <apex:stylesheet value="{!URLFOR($Resource.Bootstrap_SF1,'/css/bootstrap-namespaced.min.css')}"/>
    
    <apex:outputPanel rendered="{!!isSF1}">
        <apex:pageMessages />
        <apex:sectionHeader title="{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName}" subtitle="{!Contact.Name}" />
        <apex:form >
            <apex:pageBlock >
                <apex:pageBlockButtons >
                    <apex:commandButton action="{!save}" value="Save" styleClass="btn btn-default" />
                    <apex:commandButton action="{!cancel}" value="Cancel" styleClass="btn btn-default" />
                </apex:pageBlockButtons>
                <apex:pageBlockSection columns="1">
                    <apex:repeat var="question" value="{!questions}">
                        <apex:pageBlockSectionItem >
                            <apex:outputLabel value="{!question.Question__c}" />
                            <apex:inputTextarea cols="80" rows="3" value="{!question.Answer__c}"/>
                        </apex:pageBlockSectionItem>
                    </apex:repeat>
                </apex:pageBlockSection>
            </apex:pageBlock>
        </apex:form>
    </apex:outputPanel>
    
    <apex:outputPanel rendered="{!isSF1}">
        <div class="bootstrap" style="padding: 10px;">
            <h1>{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName} <small>{!Contact.Name}</small></h1>
            <apex:form >
                <apex:repeat var="question" value="{!questions}">
                    <div class="form-group">
                        <label>{!question.Question__c}</label>
                        <apex:inputTextarea value="{!question.Answer__c}" rows="3" cols="80" styleClass="form-control"/>
                    </div>
                </apex:repeat>
                <span id="submit"><apex:commandButton action="{!save}" value="Save" styleClass="btn btn-default" /></span>
                <span id="cancel"><apex:commandButton action="{!cancel}" value="Cancel" styleClass="btn btn-default" /></span>                            
            </apex:form>
        </div>
        
    </apex:outputPanel>
</apex:page>

Notice on line 2 how I use the static resource for the Bootstrap for SF1 CSS library. Lines 4 and 25 create two outputPanels that will be rendered based on the property we created in the controller extension to determine if the page is being called from SF1. The first output panel will be rendered for the browser client, and the code and layout is the same we had before. The new part is the second output panel, which will be rendered for the SF1 app. Notice that the code for the SF1 layout doesn’t use page blocks or any of the standard layout tags, but instead uses standard HTML div tags to layout the content. Now the page in the browser still looks as before, but on SF1 it now looks like this:

rendering on FS1 using Bootstrap

Definitely better isn’t? But there’s still something that doesn’t fit right into the SF1 experience: the publisher bar. Notice that we still have two buttons for saving and canceling, and that save button on the publisher bar appears disabled. We need to take advantage of the publisher bar and provide a seamless user experience to our SF1 users.

Taking Care of the Publisher Bar

Using the publisher bar is pretty easy once you know what to do, of course. Here is the modified section of the page for the SF1 part:

<apex:outputPanel rendered="{!isSF1}">
    <div class="bootstrap" style="padding: 10px;">
        <h1>{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName} <small>{!Contact.Name}</small></h1>
        <apex:form >
            <apex:repeat var="question" value="{!questions}">
                <div class="form-group">
                    <label>{!question.Question__c}</label>
                    <apex:inputTextarea value="{!question.Answer__c}" rows="3" cols="80" styleClass="form-control"/>
                </div>
            </apex:repeat>
            <apex:actionFunction action="{!save}" name="saveForm" />                
        </apex:form>
    </div>
    
    <script src='/canvas/sdk/js/publisher.js'></script>

    <script>
        if(sforce.one) {                       
            Sfdc.canvas.publisher.subscribe({name: "publisher.showPanel", onData:function(e) { 
                Sfdc.canvas.publisher.publish({name: "publisher.setValidForSubmit", payload: "true"});                                       
            }});
            Sfdc.canvas.publisher.subscribe({name: "publisher.post", onData:function(e) {
                saveForm();
                Sfdc.canvas.publisher.publish( { name: "publisher.close", payload:{refresh:"false"}}); 
            }});            
        }
    </script>
    
</apex:outputPanel>

To work with the publisher bar we need to reference the publisher API (this is provided by the Salesforce platform). You can see we add a reference to the library in line 15. In line 18 we use the sforce.one property to know whether we are in SF1 or not (although this section will not be rendered if we’re not in SF1, it is a good practice to put this check). Notice that we removed the two buttons we had in the previous version for the save and cancel, as we don’t need them anymore since we will use the buttons on the publisher bar instead.

The publisher API works with a publisher/subscriber pattern: it exposes some events that we could subscribe to, or publish, as needed. In line 19 we subscribe to the publisher.showPanel event, which will get fired when the page is opened and finished rendering, in which case we need to enable the save button on the bar. We can tell the publisher API to enable the save button by firing the publisher.setValidForSubmit event.

In line 22 we subscribe to the publisher.post event, which gets fired when the user taps on the save button on the publisher bar. In this case we need to save the form, by calling the action function defined in line 11, which will call the save action on the controller extension. And finally we publish the publisher.close event to notify the API to dismiss the current page.

This is how the SF1 page looks now with the publisher bar enabled:

rendering on FS1 using the publisher bar

Users can now answer questions and when done tap on the save button to save their changes, and this will all work with the UI/UX of a SF1 application.

0 Comments

In a previous post (Showing System Fields in a Custom Visualforce Page) I talked about how to show system information (Created By, Last Modified By, and Owner) in a custom visualforce page. The approach I presented works but it has some shortcomings:

  • Doesn’t work as well when the object is a detail object in a Master-Detail relationship (there is no owner here for the detail object, as the owner is the one from the master object)
  • It requires to put some fields (as hidden) in the visualforce page.

I present here a better approach that takes into consideration the above points.

Let’s start by the component controller:

public class ComponentControllerSystemInformation {
    public sObject recordValue {        
           get {
               return recordValue;
           }
           set {
               recordValue = value;
               
               Schema.DescribeSObjectResult d = recordValue.getSObjectType().getDescribe();
            String soql = 'select CreatedById,CreatedDate,LastModifiedById,LastModifiedDate' + (isMaster?',OwnerId':'') + ' from ' + d.Name + ' where Id = \'' + recordValue.Id + '\'';
            recordValue = Database.query(soql);
           }
    }
     
    public Id recordId {
        get {
            return recordValue.Id;
        }
    } 
        
    public Id createdById {
        get {
            return (Id)recordValue.get('CreatedById');
        }
    } 
     
    public String createdByName {
        get {
            User createdByUser = [select name from user where id = :createdById limit 1];
            return createdByUser == null ? null : createdByUser.Name;
        }
    }
         
    public String convertedCreatedDate {
        get {
            DateTime createdDate = (DateTime)recordValue.get('CreatedDate');
            return createdDate.format();
        }
    }
     
    public Id lastModifiedById {
        get {
            return (Id)recordValue.get('LastModifiedById');
        }
    } 
     
    public String lastModifiedByName {
        get {
            User lastModifiedByUser = [select name from user where id = :lastModifiedById limit 1];
            return lastModifiedByUser == null ? null : lastModifiedByUser.Name;
        }
    }
         
    public String convertedLastModifiedDate {
        get {
            DateTime lastModifiedDate = (DateTime)recordValue.get('LastModifiedDate');
            return lastModifiedDate.format();
        }
    }
         
    public String ownerName {
        get {
            User ownerUser = [select name from user where id = :ownerId limit 1];
            return ownerUser == null ? null : ownerUser.Name;
        }
    }
     
    public String ownerPhoto {
        get {
            try {
                Id ownerId = (Id)recordValue.get('ownerId');
                User owner = [select smallphotourl from user where id = :ownerId limit 1];
                return owner == null ? null : owner.SmallPhotoUrl;
            } catch(System.RequiredFeatureMissingException e) {
                System.debug('Chatter not enabled in organization:' + e.getMessage());
                return null;
            }
        }
    }
    
    public Boolean isMaster {
      get {
        Schema.DescribeSObjectResult d = recordValue.getSObjectType().getDescribe();
        Map<String, Schema.SObjectField> fields = d.fields.getMap();
        Schema.SObjectField field = fields.get('OwnerId');
        return field != null;
      }
    }
    
    public Id ownerId {
      get {
        if(!isMaster) return null;
        
        return (Id)recordValue.get('OwnerId');
      }
    }             
}

Notice lines 9-11 where I query the fields that are required by this component to work, removing the requirement I had in the previous solution to include those fields in the visualforce markup. Notice also in line 81 a new property isMaster which we will use to show or hide the owner information in the visualforce page.

The component can now be written as follows:

<apex:component controller="ComponentControllerSystemInformation">
    <apex:attribute name="record" assignTo="{!recordValue}"
        type="sObject" description="The object for which to display system information" required="true"/>
     
    <apex:pageBlockSection title="{!$Label.SystemInformation}" columns="2">
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="{!$Label.LastModifiedBy}" />
            <apex:outputPanel >
                <apex:outputLink id="lastModifiedBy"
                    onblur="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}').hide();"
                    onfocus="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}', '/{!lastModifiedById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"
                    onmouseout="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}').hide();"
                    onmouseover="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}', '/{!lastModifiedById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"                      
                 value="{!URLFOR('/' + lastModifiedById)}">{!lastModifiedByName}</apex:outputLink>&nbsp;
                <apex:outputText value="{!convertedLastModifiedDate}" />                                                                        
            </apex:outputPanel>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem rendered="{!NOT(isMaster)}" />    
        <apex:pageBlockSectionItem rendered="{!isMaster}">
            <apex:outputLabel for="owner" value="{!$Label.Owner}" />
            <apex:outputPanel >
                <apex:image value="{!ownerPhoto}" width="16" height="16"/>&nbsp;
                <apex:outputLink id="owner"
                    onblur="LookupHoverDetail.getHover('{!$Component.owner}').hide();"
                    onfocus="LookupHoverDetail.getHover('{!$Component.owner}', '/{!ownerId}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"
                    onmouseout="LookupHoverDetail.getHover('{!$Component.owner}').hide();"
                    onmouseover="LookupHoverDetail.getHover('{!$Component.owner}', '/{!ownerId}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"
                 value="{!URLFOR('/' + ownerId)}">{!ownerName}</apex:outputLink>&nbsp;
                <apex:outputLink value="{!URLFOR('/' + recordId + '/a?retURL=' + URLENCODE($CurrentPage.Url))}">[Change]</apex:outputLink>
            </apex:outputPanel>
        </apex:pageBlockSectionItem>       
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="{!$Label.CreatedBy}" />
            <apex:outputPanel >
                <apex:outputLink id="createdBy"
                    onblur="LookupHoverDetail.getHover('{!$Component.createdBy}').hide();"
                    onfocus="LookupHoverDetail.getHover('{!$Component.createdBy}', '/{!createdById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"
                    onmouseout="LookupHoverDetail.getHover('{!$Component.createdBy}').hide();"
                    onmouseover="LookupHoverDetail.getHover('{!$Component.createdBy}', '/{!createdById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"                      
                 value="{!URLFOR('/' + createdById)}">{!createdByName}</apex:outputLink>&nbsp;
                <apex:outputText value="{!convertedCreatedDate}" />                                                                        
            </apex:outputPanel>
        </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
</apex:component>

Notice lines 18 and 19, where we use the new property isMaster to show or hide the owner information based on whether this is a detail object in a Master-Detail relationship or not.

With this, the visualforce page is simplified as follows:

<apex:page standardController="Contact">
  <apex:pageBlock mode="mainDetail" >
    <c:ComponentControllerSystemInformation record="{!record}" />
  </apex:pageBlock>
</apex:page>

We include the component in line 3 inside a pageBlock tag. Our page will show the system information just as in a standard page:

systemInformation

And finally, for completeness, here is the test for the component:

@isTest
public class SystemInformationComponentTest{
  
    @isTest public static void TestComponent() {
         
        Account record = new Account(Name = 'Test');
        insert record;
        record = [select ownerId,createdById,lastModifiedById,createdDate,lastModifiedDate from account where id = :record.id];
        User owner = [select name,smallPhotoUrl from user where id = :record.ownerId];
             
        ComponentControllerSystemInformation controller = new SystemInformationComponentController();
        controller.recordValue = record;
         
        System.assertEquals(record.Id, controller.recordId);
        System.assertEquals(record.CreatedById, controller.createdById);
        System.assertEquals(record.LastModifiedById, controller.lastModifiedById);
        System.assertEquals(record.OwnerId, controller.ownerId);
        System.assertEquals(record.CreatedDate.format(), controller.convertedCreatedDate);
        System.assertEquals(record.LastModifiedDate.format(), controller.convertedLastMOdifiedDate);
        System.assertEquals(owner.Name, controller.ownerName);
        System.assertEquals(owner.SmallPhotoUrl, controller.ownerPhoto); 
        System.assertEquals(owner.Name, controller.lastModifiedByName);
        System.assertEquals(owner.Name, controller.createdByName);
        System.assertEquals(true, controller.isMaster);
    }
}

Now, our custom pages have the same functionality as a standard one.

1 Comments

Recently I had to do a little project where users wanted to display a list of photos associated with a custom object. I decided to take advantage of what Salesforce already provides: attachments. I designed a custom visualforce page to show the attachment as photos. In this post I will show a little example of how to accomplish this using an extension controller and a visualforce page.

Let’s work with a simple example: suppose we want to implement a database of movies in Salesforce and, besides the typical movie attributes, we want to show photos of the movie (movie poster, scenes, etc.) The end result is something like this:

Movie with Photos

As you can see from the image, we are displaying a list of photos below the movie fields. The first thing we need to do is enable attachments for our custom object.

Object settingsWe will select the “Add Notes and Attachments related list to default page layout” on object creation. This will enable the object to have attachments. We can add as many attachments as we want (or as the Salesforce space limitations allow us). Once the custom object is created, we will be able to add attachments using the standard page layout that is created as part of the object.


Let’s us create a movie and add some attachments to it. In the example, I’ve set up some basic fields for a movie: name, genre, storyline and release date (this is enough for this example). I’ve created a sample movie (I’m a big fan of the Alien franchise) and added some attachments to it:

The movie layout with attachments

Now, for the fun part: we will create a custom visualforce page to show those attachments as photos. Let’s start by creating a new visualforce page by typing https://<salesforceurl>/apex/Movie?id=<someMovieId>. This will create a new Apex page called Movie, from scratch. Add the following markup to the visualforce page:

<apex:page standardController="Movie__c" extensions="MovieControllerExtension">
    <apex:detail relatedList="false"/>
    <apex:pageBlock >
        <apex:pageBlockSection title="Photos ({!totalPhotos})" collapsible="false">
            <apex:repeat value="{!photos}" var="photo">
                <apex:image url="{!URLFOR($Action.Attachment.Download, photo)}" width="50px" height="50px" />                    
            </apex:repeat>
        </apex:pageBlockSection>
    </apex:pageBlock>
</apex:page>

This is a very simple page: we’re leveraging on the apex:detail tag to have all the movie details and then we create a page block to list the photos (the attachments). The magic happens in line 6, where we use the special action Download to actually download the attachment at run time and display it as the source of the apex:image tag. We do all this inside the apex:repeat tag to cycle through all the attachments for the current movie object. Notice how we are using a controller extension called MovieControllerExtension. This extension provides two properties: totalPhotos and photos. The photos property is just a list of Ids for each of the attachments associated with the object. This is the code for the controller extension:

public with sharing class MovieControllerExtension {

    private ApexPages.standardController controller;
    
    private Movie__c movie;

    private List<Id> photoIds; 

    public MovieControllerExtension(ApexPages.StandardController controller) {
        this.controller = controller;
        
        this.movie = (Movie__c)controller.getRecord();
    }

    public List<Id> photos {
        get {
            if(photoIds == null) {
                photoIds = new List<Id>();
                for(Attachment att : [select Id from Attachment where ParentId = :movie.Id]) {
                    photoIds.Add(att.Id);
                }
            }
                            
            return photoIds;
        }
    }
    
    public Integer totalPhotos {
        get {
            return photos.size();
        }
    }
}

Lines 19 and 20 show how the attachment ids are retrieved from the database and added to the photos property.

And this is it! we get a nice list of all the photos for the movie by leveraging the attachment records associated to our custom object.

0 Comments

When creating a custom Visualforce page there are times when we would like to have a section showing system fields. In a standard visualforce page (the ones automatically generated by the platform) we see the following fields:

  • Created By: showing the name of the user who created the record and the creation date and time
  • Last Modified By: showing the name of the last user who modified the record and the date and time of this modification
  • Owner ID: The name of the user who owns the record, its picture, and a link that allows to change the owner.

Most of this information can be showed by using apex:outputField commands. For example:

<apex:pageBlock mode="mainDetail" >
  <apex:pageBlockSection title="{!$Label.SystemInformation}" columns="2">
    <apex:outputField value="{!Contact.CreatedById}" />
    <apex:outputField value="{!Contact.CreatedDate}" />
    <apex:outputField value="{!Contact.LastModifiedById}"/>
    <apex:outputField value="{!Contact.LastModifiedDate}"/>
    <apex:outputField value="{!Contact.OwnerId}"/>
  </apex:pageBlockSection>
</apex:pageBlock>

This will produce the following output:

standardSystemInformation

Notice that we get all the necessary information, but we lose some cool features:

  • There is no “Change” link to change the owner
  • There is no photo of the owner
  • The format is not like the one in the standard page.

We could get the format we desire by tweaking the visualforce a little bit (e.g. use apex:outputPanel to group fields together in the same line) but having the “change” and the photo is not trivial thing. Besides, by using apex:outputPanel and apex:outputText we lose the cool popup that shows summary information when we hover the mouse over the links.

The following example contains a Visualforce component that can be added to any custom page to display the system fields in a format that mimics the format produced by a standard page.

<apex:component controller="SystemInformationComponentController"> 
    <apex:attribute name="record" assignTo="{!recordValue}" 
        type="sObject" description="The object for which to display system information" required="true"/>
    
    <apex:pageBlockSection title="{!$Label.SystemInformation}" columns="2">
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="{!$Label.CreatedBy}" />
            <apex:outputPanel >
                <apex:outputLink id="createdBy"
                    onblur="LookupHoverDetail.getHover('{!$Component.createdBy}').hide();" 
                    onfocus="LookupHoverDetail.getHover('{!$Component.createdBy}', '/{!createdById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();" 
                    onmouseout="LookupHoverDetail.getHover('{!$Component.createdBy}').hide();" 
                    onmouseover="LookupSHoverDetail.getHover('{!$Component.createdBy}', '/{!createdById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"                       
                 value="{!URLFOR('/' + createdById)}">{!createdByName}</apex:outputLink>&nbsp;
                <apex:outputText value="{!convertedCreatedDate}" />                                                                         
            </apex:outputPanel>
        </apex:pageBlockSectionItem> 
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="{!$Label.LastModifiedBy}" />
            <apex:outputPanel >
                <apex:outputLink id="lastModifiedBy"
                    onblur="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}').hide();" 
                    onfocus="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}', '/{!lastModifiedById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();" 
                    onmouseout="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}').hide();" 
                    onmouseover="LookupHoverDetail.getHover('{!$Component.lastModifiedBy}', '/{!lastModifiedById}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"                       
                 value="{!URLFOR('/' + lastModifiedById)}">{!lastModifiedByName}</apex:outputLink>&nbsp;
                <apex:outputText value="{!convertedLastModifiedDate}" />                                                                         
            </apex:outputPanel>
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel for="owner" value="{!$Label.Owner}" />
            <apex:outputPanel >
                <apex:image value="{!ownerPhoto}" width="16" height="16"/>&nbsp;
                <apex:outputLink id="owner"
                    onblur="LookupHoverDetail.getHover('{!$Component.owner}').hide();" 
                    onfocus="LookupHoverDetail.getHover('{!$Component.owner}', '/{!ownerId}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();" 
                    onmouseout="LookupHoverDetail.getHover('{!$Component.owner}').hide();" 
                    onmouseover="LookupHoverDetail.getHover('{!$Component.owner}', '/{!ownerId}/m?retURL={!URLENCODE($CurrentPage.Url)}&isAjaxRequest=1').show();"
                 value="{!URLFOR('/' + ownerId)}">{!ownerName}</apex:outputLink>&nbsp;
                <apex:outputLink value="{!URLFOR('/' + recordId + '/a?retURL=' + URLENCODE($CurrentPage.Url))}">[Change]</apex:outputLink>
            </apex:outputPanel>
        </apex:pageBlockSectionItem>        
    </apex:pageBlockSection>
</apex:component>

This component requires the following controller:

public class SystemInformationComponentController{
    public sObject recordValue { get; set;}
    
    public Id recordId {
        get {
            return recordValue.Id;
        }
    }  
       
    public Id createdById {
        get {
            return (Id)recordValue.get('CreatedById');
        }
    }  
    
    public String createdByName {
        get {
            User createdByUser = [select name from user where id = :createdById limit 1];
            return createdByUser == null ? null : createdByUser.Name;
        }
    }
        
    public String convertedCreatedDate {
        get { 
            DateTime createdDate = (DateTime)recordValue.get('CreatedDate');
            return createdDate.format(); 
        }
    }
    
    public Id lastModifiedById {
        get {
            return (Id)recordValue.get('LastModifiedById');
        }
    }  
    
    public String lastModifiedByName {
        get {
            User lastModifiedByUser = [select name from user where id = :lastModifiedById limit 1];
            return lastModifiedByUser == null ? null : lastModifiedByUser.Name;
        }
    }
        
    public String convertedLastModifiedDate {
        get {
            DateTime lastModifiedDate = (DateTime)recordValue.get('LastModifiedDate');
            return lastModifiedDate.format(); 
        }
    } 
    
    public Id ownerId {
        get {
            return (Id)recordValue.get('OwnerId');
        }
    }  
    
    public String ownerName {
        get {
            User ownerUser = [select name from user where id = :ownerId limit 1];
            return ownerUser == null ? null : ownerUser.Name;
        }
    }
    
    public String ownerPhoto {
        get {
            Id ownerId = (Id)recordValue.get('ownerId');
            User owner = [select smallphotourl from user where id = :ownerId limit 1];
            return owner == null ? null : owner.SmallPhotoUrl;

        }
    }           
}

To use the component, simply call it from your custom page passing the record for the page:

<apex:page standardController="Contact">
  <apex:pageBlock mode="mainDetail" >
    <apex:outputField value="{!Contact.CreatedById}" rendered="false" />
    <apex:outputField value="{!Contact.CreatedDate}" rendered="false" />
    <apex:outputField value="{!Contact.LastModifiedById}" rendered="false" />
    <apex:outputField value="{!Contact.LastModifiedDate}" rendered="false" />
    <apex:outputField value="{!Contact.OwnerId}" rendered="false" />
    <c:SystemInformationComponent record="{!record}" />
  </apex:pageBlock>
</apex:page>

This will produce the following:

customizedSystemInformation

Notice how we get all the features we need and they look as a standard page output.

There are some things to consider when using this component:

  • Custom labels: for the component to be used in multiple languages, notice that the field labels use the following custom labels:
    • $Label.SystemInformation
    • $Label.CreatedBy
    • $Label.LastModifiedBy
    • $Label.Owner
  • The controller is based in the generic sObject type, allowing the component to be used in any standard or custom object
  • Datetimes are formatted using the format() method to show the converted datetime in the user’s profile timezone
  • In order to display the owner photo, the system must have Chatter enabled.
  • When using the component, the calling page must include the following fields (using an <apex:outputField value=”…” rendered=”false” />):
    • CreatedById
    • CreatedDate
    • LastModifiedById
    • LastModifiedDate
    • OwnerId

This is to make the fields available to the standard controller (and thus, the underlying record property) so it can be used on the component’s controller (see the above example to see how)

For completeness, here is the test for the controller:

@isTest
public class SystemInformationComponentTest{
 
    @isTest public static void TestComponent() {
        
        Account record = new Account(Name = 'Test');
        insert record;
        record = [select ownerId,createdById,lastModifiedById,createdDate,lastModifiedDate from account where id = :record.id];
        User owner = [select name,smallPhotoUrl from user where id = :record.ownerId];
            
        SystemInformationComponentController controller = new SystemInformationComponentController();
        controller.recordValue = record;
        
        System.assertEquals(record.Id, controller.recordId);
        System.assertEquals(record.CreatedById, controller.createdById);
        System.assertEquals(record.LastModifiedById, controller.lastModifiedById);
        System.assertEquals(record.OwnerId, controller.ownerId);
        System.assertEquals(record.CreatedDate.format(), controller.convertedCreatedDate);
        System.assertEquals(record.LastModifiedDate.format(), controller.convertedLastMOdifiedDate);
        System.assertEquals(owner.Name, controller.ownerName);
        System.assertEquals(owner.SmallPhotoUrl, controller.ownerPhoto); 
        System.assertEquals(owner.Name, controller.lastModifiedByName);
        System.assertEquals(owner.Name, controller.createdByName);
    }
}

I have written a better approach to this, please have a look at Showing System Fields in a Custom Visualforce Page (an improved approach) for more information