function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Rajesh ShahRajesh Shah 

History related List using Visualforce

When developing a Visualforce page for overiding view page for any object, one problem that creeps up is to display the History details of a record. The standard related list Component doesn't works for History.

 

With the help of some code from Community ( I now can't find the link to it :( ), I wrote my own code  then to display the history of an object. It mimics the standard list as far as possible.  

 

Heres the code. It is for the Case object but it can be used for any other object.

 1.Component Code

 

<apex:component controller="CaseHistoriesComponentController">
<!-- Attribute Definition -->
<apex:attribute name="CaseId" description="Salesforce Id of the Case whose Case History needs to be rendered" type="Id" required="true" assignTo="{!caseId}" />

<!-- Case History Related List -->
<apex:pageBlock title="Case History">
<apex:pageBlockTable value="{!histories}" var="History" >
<apex:column headerValue="Date" value="{!History.thedate}"/>
<apex:column headerValue="User"> <apex:outputLink value="/{!History.userId}"> {!History.who} </apex:outputLink></apex:column>
<apex:column headerValue="Action"><apex:outputText escape="false" value="{!History.action}"/></apex:column>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:component>

 

 

 

 

2. Apex Code

 

public class CaseHistoriesComponentController {

public Id caseId {get; set;}
public cHistories[] histories;

// Variables
public Static final Map<String, Schema.SObjectField> CaseFieldmap = Schema.SObjectType.Case.fields.getMap();
public Static final List<Schema.PicklistEntry> fieldPicklistValues = CaseHistory.Field.getDescribe().getPicklistValues();

public List<cHistories> getHistories()
{
list<cHistories> histories = new list<cHistories>();
String prevDate = '';
for(CaseHistory cHistory : [Select CreatedDate, CreatedBy.Name, CreatedBy.Id, Field, NewValue, OldValue from CaseHistory where CaseId = :caseId order by CreatedDate desc])
{
if((cHistory.newValue == null && cHistory.oldValue == null)
|| (cHistory.newValue != null && !(string.valueOf(cHistory.newValue).startsWith('005') || string.valueOf(cHistory.newValue).startsWith('00G')))
|| (cHistory.oldValue != null && !(string.valueOf(cHistory.oldValue).startsWith('005') || string.valueOf(cHistory.oldValue).startsWith('00G'))))
{
cHistories tempHistory = new cHistories();
// Set the Date and who performed the action
if(String.valueOf(cHistory.CreatedDate) != prevDate)
{
tempHistory.theDate = String.valueOf(cHistory.CreatedDate);
tempHistory.who = cHistory.CreatedBy.Name;
tempHistory.userId = cHistory.CreatedBy.Id;
}
else
{
tempHistory.theDate = '';
tempHistory.who = '';
tempHistory.userId = cHistory.CreatedBy.Id;
}
prevDate = String.valueOf(cHistory.CreatedDate);

// Get the field label
String fieldLabel = CaseHistoriesComponentController.returnFieldLabel(String.valueOf(cHistory.Field));

// Set the Action value
if (String.valueOf(cHistory.Field) == 'created') { // on Creation
tempHistory.action = 'Created.';
}
else if(cHistory.OldValue != null && cHistory.NewValue == null){ // when deleting a value from a field
// Format the Date and if there's an error, catch it and re
try {
tempHistory.action = 'Deleted ' + Date.valueOf(cHistory.OldValue).format() + ' in <b>' + fieldLabel + '</b>.';
} catch (Exception e){
tempHistory.action = 'Deleted ' + String.valueOf(cHistory.OldValue) + ' in <b>' + fieldLabel + '</b>.';
}
}
else{ // all other scenarios
String fromText = '';
if (cHistory.OldValue != null) {
try {
fromText = ' from ' + Date.valueOf(cHistory.OldValue).format();
} catch (Exception e) {
fromText = ' from ' + String.valueOf(cHistory.OldValue);
}
}

String toText = '';
if (cHistory.OldValue != null) {
try {
toText = Date.valueOf(cHistory.NewValue).format();
} catch (Exception e) {
toText = String.valueOf(cHistory.NewValue);
}
}
if(toText != '')
tempHistory.action = 'Changed <b>' + fieldLabel + '</b>' + fromText + ' to <b>' + toText + '</b>.';
else
tempHistory.action = 'Changed <b>' + fieldLabel;
}

// Add to the list
histories.add(tempHistory);
}
}

return histories;
}

// Function to return Field Label of a Case field given a Field API name
public Static String returnFieldLabel(String fieldName)
{
if(CaseHistoriesComponentController.CaseFieldmap.containsKey(fieldName))
return CaseHistoriesComponentController.CaseFieldmap.get(fieldName).getDescribe().getLabel();
else
{
for(Schema.PicklistEntry pickList : fieldPicklistValues)
{
if(pickList.getValue() == fieldName)
{
if(pickList.getLabel() != null)
return pickList.getLabel();
else
return pickList.getValue();
}
}
}
return '';
}
// Inner Class to store the detail of the case histories
public class cHistories {

public String theDate {get; set;}
public String who {get; set;}
public Id userId {get; set;}
public String action {get; set;}
}
}

  Let me know your views on the code or if you have any questions

 

Best Answer chosen by Admin (Salesforce Developers) 
RyanGossinkRyanGossink

Thanks for that, I have used your original code as a basis for a generic history component, since you shared your original code I thought I should return the favor.

 

It is still a little rough around the edges, but you can use the component in a VF page using the following method:

 

 

<c:GenericHistoryComponent recordLimit="50" myObject="{!Software_License__c}"/>

 

The component code is as follows:

 

 

<apex:component controller="GenericHistoryComponentController">
<!-- Attribute Definition -->
<apex:attribute name="myObject" description="Object we wish to view the history of" type="SObject" required="true" assignTo="{!myObject}" />
<apex:attribute name="recordLimit" description="Number of lines of history to display" type="Integer" required="false" assignTo="{!recordLimit}" />

<!-- Object History Related List -->
<apex:pageBlock title="{!objectLabel} History">
<apex:pageBlockTable value="{!ObjectHistory}" var="History" >
<apex:column headerValue="Date" value="{!History.thedate}"/>
<apex:column headerValue="User">
<apex:outputLink value="/{!History.userId}"> {!History.who} </apex:outputLink>
</apex:column>
<apex:column headerValue="Action"><apex:outputText escape="false" value="{!History.action}"/></apex:column>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:component>

 

 And the controller class is as follows:

 

 

public class GenericHistoryComponentController {

// External variables
public SObject myObject {get; set;}
public Integer recordLimit {get; set;}
public static String objectLabel {get;}

// Internal Variables
public objectHistoryLine[] objectHistory;

public static final Map<String, Schema.SObjectType> mySObjectTypeMap = Schema.getGlobalDescribe();
public static Map<String, Schema.SObjectField> myObjectFieldMap;
public static List<Schema.PicklistEntry> historyFieldPicklistValues;

public List<objectHistoryLine> getObjectHistory(){

Id myObjectId = String.valueOf(myObject.get('Id'));
Schema.DescribeSObjectResult objectDescription = myObject.getSObjectType().getDescribe();

myObjectFieldMap = objectDescription.fields.getMap();
objectLabel = String.valueOf(objectDescription.getLabel());

//Get the name of thew history table
String objectHistoryTableName = objectDescription.getName();
//if we have a custom object we need to drop the 'c' off the end before adding 'History' to get the history tables name
if (objectDescription.isCustom()){
objectHistoryTableName = objectHistoryTableName.substring(0, objectHistoryTableName.length()-1);
}
objectHistoryTableName = objectHistoryTableName + 'History';

Schema.DescribeFieldResult objectHistoryFieldField = mySObjectTypeMap.get(objectHistoryTableName).getDescribe().fields.getMap().get('Field').getDescribe();
historyFieldPicklistValues = objectHistoryFieldField.getPickListValues();

list<objectHistoryLine> objectHistory = new list<objectHistoryLine>();

String prevDate = '';

if (recordLimit== null){
recordLimit = 100;
}

list<sObject> historyList = Database.query( 'SELECT CreatedDate,'+
'CreatedById,'+
'Field,'+
'NewValue,'+
'OldValue ' +
'FROM ' + objectHistoryTableName + ' ' +
'WHERE ParentId =\'' + myObjectId + '\' ' +
'ORDER BY CreatedDate DESC '+
'LIMIT ' + String.valueOf(recordLimit));

for(Integer i = 0; i < historyList.size(); i++){
sObject historyLine = historyList.get(i);
if ((historyLine.get('newValue') == null && historyLine.get('oldValue') == null)
|| (historyLine.get('newValue') != null && !(string.valueOf(historyLine.get('newValue')).startsWith('005') || string.valueOf(historyLine.get('newValue')).startsWith('00G')))
|| (historyLine.get('oldValue') != null && !(string.valueOf(historyLine.get('oldValue')).startsWith('005') || string.valueOf(historyLine.get('oldValue')).startsWith('00G')))){
objectHistoryLine tempHistory = new objectHistoryLine();
// Set the Date and who performed the action
if (String.valueOf(historyLine.get('CreatedDate')) != prevDate){
tempHistory.theDate = String.valueOf(historyLine.get('CreatedDate'));
tempHistory.userId = String.valueOf(historyLine.get('CreatedById'));
tempHistory.who = String.valueOf(historyLine.get('CreatedById'));
}
else{
tempHistory.theDate = '';
tempHistory.who = '';
tempHistory.userId = String.valueOf(historyLine.get('CreatedById'));
}
prevDate = String.valueOf(historyLine.get('CreatedDate'));

// Get the field label
String fieldLabel = GenericHistoryComponentController.returnFieldLabel(String.valueOf(historyLine.get('Field')));

// Set the Action value
if (String.valueOf(historyLine.get('Field')) == 'created') { // on Creation
tempHistory.action = 'Created.';
}
else if (historyLine.get('oldValue') != null && historyLine.get('newValue') == null){ // when deleting a value from a field
// Format the Date and if there's an error, catch it and re
try {
tempHistory.action = 'Deleted ' + Date.valueOf(historyLine.get('oldValue')).format() + ' in <b>' + fieldLabel + '</b>.';
} catch (Exception e){
tempHistory.action = 'Deleted ' + String.valueOf(historyLine.get('oldValue')) + ' in <b>' + fieldLabel + '</b>.';
}
}
else{ // all other scenarios
String fromText = '';
if (historyLine.get('oldValue') != null) {
try {
fromText = ' from ' + Date.valueOf(historyLine.get('oldValue')).format();
} catch (Exception e) {
fromText = ' from ' + String.valueOf(historyLine.get('oldValue'));
}
}

String toText = '';
if (historyLine.get('oldValue') != null) {
try {
toText = Date.valueOf(historyLine.get('newValue')).format();
} catch (Exception e) {
toText = String.valueOf(historyLine.get('newValue'));
}
}
if (toText != ''){
tempHistory.action = 'Changed <b>' + fieldLabel + '</b>' + fromText + ' to <b>' + toText + '</b>.';
}
else {
tempHistory.action = 'Changed <b>' + fieldLabel;
}
}

// Add to the list
objectHistory.add(tempHistory);
}
}

List<Id> userIdList = new List<Id>();
for (objectHistoryLine myHistory : objectHistory){
userIdList.add(myHistory.userId);
}
Map<Id, User> userIdMap = new Map<ID, User>([SELECT Name FROM User WHERE Id IN : userIdList]);

for (objectHistoryLine myHistory : objectHistory){
if (userIdMap.containsKey(myHistory.userId) & (myHistory.who != '') ){
myHistory.who = userIdMap.get(myHistory.who).Name;
}
}

return objectHistory;
}

// Function to return Field Label of a object field given a Field API name
public Static String returnFieldLabel(String fieldName){

if (GenericHistoryComponentController.myObjectFieldMap.containsKey(fieldName)){
return GenericHistoryComponentController.myObjectFieldMap.get(fieldName).getDescribe().getLabel();
}
else {
for(Schema.PicklistEntry pickList : historyFieldPicklistValues){
if (pickList.getValue() == fieldName){
if (pickList.getLabel() != null){
return pickList.getLabel();
}
else {
return pickList.getValue();
}
}
}
}
return '';
}

// Inner Class to store the detail of the object history lines
public class objectHistoryLine {

public String theDate {get; set;}
public String who {get; set;}
public Id userId {get; set;}
public String action {get; set;}
}
}

 

I've only tested this with the custom objects I have VF pages for at the moment, however, I've written it in a way such that it should work with standard objects.

 

 

 

 

All Answers

bob_buzzardbob_buzzard

I've done something similar to this for an existing project (although I'm using a popup window rather than related list). As you've gone this far (and kindly shared) you might wish to consider making it work across any object, rather than being tied to a single object.

 

There's not a lot of extra code required :

 

-> pass in the name of the history class and the underlying class (CaseHistory and Case, for this example),  

-> alter your select statement to something like the following:

 

 

String soql='select ParentId, OldValue, NewValue, IsDeleted, Id, Field, CreatedDate,' + 'CreatedById, CreatedBy.Name From ' + objectType + ' where ParentId = :objectId order by CreatedDate desc'; return Database.query(soql);

 where objectType is the history class.

->alter the field name lookup to figure out (and cache) the non-history object type field map and pull the fields out from that.

 

 

It certainly cut down on our duplicated code going this route :) 

 

 

 

 

Message Edited by bob_buzzard on 10-16-2009 07:54 AM
Rajesh ShahRajesh Shah

hmmm ... thts a good idea ... i will try implement the same when I get time.

 

 

RyanGossinkRyanGossink

Thanks for that, I have used your original code as a basis for a generic history component, since you shared your original code I thought I should return the favor.

 

It is still a little rough around the edges, but you can use the component in a VF page using the following method:

 

 

<c:GenericHistoryComponent recordLimit="50" myObject="{!Software_License__c}"/>

 

The component code is as follows:

 

 

<apex:component controller="GenericHistoryComponentController">
<!-- Attribute Definition -->
<apex:attribute name="myObject" description="Object we wish to view the history of" type="SObject" required="true" assignTo="{!myObject}" />
<apex:attribute name="recordLimit" description="Number of lines of history to display" type="Integer" required="false" assignTo="{!recordLimit}" />

<!-- Object History Related List -->
<apex:pageBlock title="{!objectLabel} History">
<apex:pageBlockTable value="{!ObjectHistory}" var="History" >
<apex:column headerValue="Date" value="{!History.thedate}"/>
<apex:column headerValue="User">
<apex:outputLink value="/{!History.userId}"> {!History.who} </apex:outputLink>
</apex:column>
<apex:column headerValue="Action"><apex:outputText escape="false" value="{!History.action}"/></apex:column>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:component>

 

 And the controller class is as follows:

 

 

public class GenericHistoryComponentController {

// External variables
public SObject myObject {get; set;}
public Integer recordLimit {get; set;}
public static String objectLabel {get;}

// Internal Variables
public objectHistoryLine[] objectHistory;

public static final Map<String, Schema.SObjectType> mySObjectTypeMap = Schema.getGlobalDescribe();
public static Map<String, Schema.SObjectField> myObjectFieldMap;
public static List<Schema.PicklistEntry> historyFieldPicklistValues;

public List<objectHistoryLine> getObjectHistory(){

Id myObjectId = String.valueOf(myObject.get('Id'));
Schema.DescribeSObjectResult objectDescription = myObject.getSObjectType().getDescribe();

myObjectFieldMap = objectDescription.fields.getMap();
objectLabel = String.valueOf(objectDescription.getLabel());

//Get the name of thew history table
String objectHistoryTableName = objectDescription.getName();
//if we have a custom object we need to drop the 'c' off the end before adding 'History' to get the history tables name
if (objectDescription.isCustom()){
objectHistoryTableName = objectHistoryTableName.substring(0, objectHistoryTableName.length()-1);
}
objectHistoryTableName = objectHistoryTableName + 'History';

Schema.DescribeFieldResult objectHistoryFieldField = mySObjectTypeMap.get(objectHistoryTableName).getDescribe().fields.getMap().get('Field').getDescribe();
historyFieldPicklistValues = objectHistoryFieldField.getPickListValues();

list<objectHistoryLine> objectHistory = new list<objectHistoryLine>();

String prevDate = '';

if (recordLimit== null){
recordLimit = 100;
}

list<sObject> historyList = Database.query( 'SELECT CreatedDate,'+
'CreatedById,'+
'Field,'+
'NewValue,'+
'OldValue ' +
'FROM ' + objectHistoryTableName + ' ' +
'WHERE ParentId =\'' + myObjectId + '\' ' +
'ORDER BY CreatedDate DESC '+
'LIMIT ' + String.valueOf(recordLimit));

for(Integer i = 0; i < historyList.size(); i++){
sObject historyLine = historyList.get(i);
if ((historyLine.get('newValue') == null && historyLine.get('oldValue') == null)
|| (historyLine.get('newValue') != null && !(string.valueOf(historyLine.get('newValue')).startsWith('005') || string.valueOf(historyLine.get('newValue')).startsWith('00G')))
|| (historyLine.get('oldValue') != null && !(string.valueOf(historyLine.get('oldValue')).startsWith('005') || string.valueOf(historyLine.get('oldValue')).startsWith('00G')))){
objectHistoryLine tempHistory = new objectHistoryLine();
// Set the Date and who performed the action
if (String.valueOf(historyLine.get('CreatedDate')) != prevDate){
tempHistory.theDate = String.valueOf(historyLine.get('CreatedDate'));
tempHistory.userId = String.valueOf(historyLine.get('CreatedById'));
tempHistory.who = String.valueOf(historyLine.get('CreatedById'));
}
else{
tempHistory.theDate = '';
tempHistory.who = '';
tempHistory.userId = String.valueOf(historyLine.get('CreatedById'));
}
prevDate = String.valueOf(historyLine.get('CreatedDate'));

// Get the field label
String fieldLabel = GenericHistoryComponentController.returnFieldLabel(String.valueOf(historyLine.get('Field')));

// Set the Action value
if (String.valueOf(historyLine.get('Field')) == 'created') { // on Creation
tempHistory.action = 'Created.';
}
else if (historyLine.get('oldValue') != null && historyLine.get('newValue') == null){ // when deleting a value from a field
// Format the Date and if there's an error, catch it and re
try {
tempHistory.action = 'Deleted ' + Date.valueOf(historyLine.get('oldValue')).format() + ' in <b>' + fieldLabel + '</b>.';
} catch (Exception e){
tempHistory.action = 'Deleted ' + String.valueOf(historyLine.get('oldValue')) + ' in <b>' + fieldLabel + '</b>.';
}
}
else{ // all other scenarios
String fromText = '';
if (historyLine.get('oldValue') != null) {
try {
fromText = ' from ' + Date.valueOf(historyLine.get('oldValue')).format();
} catch (Exception e) {
fromText = ' from ' + String.valueOf(historyLine.get('oldValue'));
}
}

String toText = '';
if (historyLine.get('oldValue') != null) {
try {
toText = Date.valueOf(historyLine.get('newValue')).format();
} catch (Exception e) {
toText = String.valueOf(historyLine.get('newValue'));
}
}
if (toText != ''){
tempHistory.action = 'Changed <b>' + fieldLabel + '</b>' + fromText + ' to <b>' + toText + '</b>.';
}
else {
tempHistory.action = 'Changed <b>' + fieldLabel;
}
}

// Add to the list
objectHistory.add(tempHistory);
}
}

List<Id> userIdList = new List<Id>();
for (objectHistoryLine myHistory : objectHistory){
userIdList.add(myHistory.userId);
}
Map<Id, User> userIdMap = new Map<ID, User>([SELECT Name FROM User WHERE Id IN : userIdList]);

for (objectHistoryLine myHistory : objectHistory){
if (userIdMap.containsKey(myHistory.userId) & (myHistory.who != '') ){
myHistory.who = userIdMap.get(myHistory.who).Name;
}
}

return objectHistory;
}

// Function to return Field Label of a object field given a Field API name
public Static String returnFieldLabel(String fieldName){

if (GenericHistoryComponentController.myObjectFieldMap.containsKey(fieldName)){
return GenericHistoryComponentController.myObjectFieldMap.get(fieldName).getDescribe().getLabel();
}
else {
for(Schema.PicklistEntry pickList : historyFieldPicklistValues){
if (pickList.getValue() == fieldName){
if (pickList.getLabel() != null){
return pickList.getLabel();
}
else {
return pickList.getValue();
}
}
}
}
return '';
}

// Inner Class to store the detail of the object history lines
public class objectHistoryLine {

public String theDate {get; set;}
public String who {get; set;}
public Id userId {get; set;}
public String action {get; set;}
}
}

 

I've only tested this with the custom objects I have VF pages for at the moment, however, I've written it in a way such that it should work with standard objects.

 

 

 

 

This was selected as the best answer
DBAdminDBAdmin

Was able to leverage code for a Custom Object, however, when trying to create a new record for the Custom Object, receiving an error from the Controller Class at:

 

list<sObject> historyList = Database.query( 'SELECT CreatedDate,'+
'CreatedById,'+
                                                    'Field,'+
                                                    'NewValue,'+
                                                    'OldValue ' +
                                                    'FROM ' + objectHistoryTableName + ' ' +
                                                    'WHERE ParentId =\'' + myObjectId + '\' ' +
                                                    'ORDER BY CreatedDate DESC '+
                                                    'LIMIT ' + String.valueOf(recordLimit));

 

stipulating that the Id is NULL. Appears that everything is fine is the Custom Object record already exists.

 

Additionally, is there a way to hide the Related History in edit mode? Currently it is defaulted to 'true'. 

 

JustinMccJustinMcc

There is a bug in the code for the following case:

If the old value is null and the new value is not null.  Instead of showing "Changed to <value>" it would just show "Changed".  The error is on line 97:

 

 

if (historyLine.get('oldValue') != null) {
try {
toText = Date.valueOf(historyLine.get('newValue')).format();
} catch (Exception e) {
toText = String.valueOf(historyLine.get('newValue'));
}
}

 

The first line above should read if(historyLine.get('newValue') != null) {

 

Justin

ron_reedron_reed
Is there any chance that someone would be able to provide a test class for this code?
msreekmmsreekm

thanks for the  code RyanGossink ..

  

the above code needs minor changes for standard objects.. parentid column is only in custom object history table.  for standard objects the column name is objectname+id (eg. caseId)..

 

 

//getObjectHistory method        
	public List<objectHistoryLine> getObjectHistory(){        
		Id myObjectId = String.valueOf(myObject.get('Id'));        
		Schema.DescribeSObjectResult objectDescription = myObject.getSObjectType().getDescribe();        
		myObjectFieldMap = objectDescription.fields.getMap();        
		objectLabel = String.valueOf(objectDescription.getLabel());                
		
		//Get the name of thew history table        
		String objectHistoryTableName = objectDescription.getName();
		
		//ID field name         
		string ObjectIdName ;
		
		//if we have a custom object we need to drop the 'c' off the end before adding 'History' to get the history tables name        
		if (objectDescription.isCustom()){            
			objectHistoryTableName = objectHistoryTableName.substring(0, objectHistoryTableName.length()-1);
			ObjectIdName = 'ParentId';        
		}        
		else{
		    ObjectIdName = objectHistoryTableName+ 'Id';
		}	
			objectHistoryTableName = objectHistoryTableName + 'History';                
			Schema.DescribeFieldResult objectHistoryFieldField = mySObjectTypeMap.get(objectHistoryTableName).getDescribe().fields.getMap().get('Field').getDescribe();			        
			historyFieldPicklistValues = objectHistoryFieldField.getPickListValues();                            
			list<objectHistoryLine> objectHistory = new list<objectHistoryLine>();
			               
		    String prevDate = '';             
			if (recordLimit== null){ 	recordLimit = 100;	 }
			                
			list<sObject> historyList = Database.query( 'SELECT CreatedDate,'+
 	                                                     'CreatedById,'+                                                    
	                                                     'Field,'+                                                    
	                                                     'NewValue,'+                                                    
	                                                     'OldValue ' +                                                    
	                                                     'FROM ' + objectHistoryTableName + ' ' +                                                   	                                                     
	                                                     'WHERE ' + ObjectIdName + '=\'' + myObjectId + '\' ' +                                                    
	                                                     'ORDER BY CreatedDate DESC '+                                                    
	                                                     'LIMIT ' + String.valueOf(recordLimit));    

 rest of the code remains the same..

 

 

guest1231231guest1231231

Great code, how can this be applied to an Account's Related Activity History?

 

-Thanks

vikram_n3233vikram_n3233

@Rajesh; Thanks man - ur code really helped me in doing my task today

Rajesh ShahRajesh Shah

Account related activity history related list can be directly created. Eg code is given below:

 

<apex:relatedList list="ActivityHistories" />

 

 

MattreyMattrey

One minor issue I noticed when implementing this is that the history Date is displayed as GMT, not in the user's local time zone. To fix this, in the controller (~line 60) where you set tempHistory.theDate, don't use String.valueOf(). Instead, cast to a DateTime object and use the format() method like this:

 

tempHistory.theDate = ((Datetime)historyLine.get('CreatedDate')).format('yyyy-MM-dd HH:mm:ss'); 

 

This should show the local time. The format string here matches that currently displayed  by the history component but you can always change it to day-month-year, change to am/pm time or whatever else works better for you. The format string is the same as that used by Java so just Google that for the details.

JPClark3JPClark3

Has anyone notice this failing since the spring release?

It appears that the 'Field'  now have the namespace on them, and don't lookup in the objectfieldmap like they used to.

md1md1

I just used the code for custom objects and it seems to be working for me.

rani 2rani 2

Hi I am trying to implement this component but getting error as

Error: Compile Error: Method does not exist or incorrect signature: [Schema.SObjectType].getDe​scribe() at line 30 column 99Error: Compile Error: Method does not exist or incorrect signature: [Schema.SObjectType].getDe​scribe() at line 30 column 99

 

Plz help !

RajivRajiv

Hi everyone,

 

Can anyone provide test case for GenericHistoryComponentController class. Please I need urgently.

 

Thanks for your help in advance.

Rajiv

ColinKenworthyColinKenworthy

I just had one issue using this component in my VF page and I think it was because I was using an action="myActionMethod" attribute in my apex:page tag.

My action method is sometimes returning a PageReference that navigates away from the VF page, but when it does it seems to be passing a null instead of myObject into the component and this was giving me a 'de-reference a null object' error and stopping the page redirect. It worked fine when there was no redirect.

 

 

I just made the beginning of the method look like this and all was well. (new code in red)

 

    public List<objectHistoryLine> getObjectHistory(){

// if no object passed in, return empty list
if (myObject == null) {
return new List<objectHistoryLine>();
}

Id myObjectId = String.valueOf(myObject.get('Id'));
Schema.DescribeSObjectResult objectDescription = myObject.getSObjectType().getDescribe();

myObjectFieldMap = objectDescription.fields.getMap();
objectLabel = String.valueOf(objectDescription.getLabel());

//Get the name of thew history table
.....

 

 

 

 

Farhat MirzaFarhat Mirza

Excellent work .

I am getting one issue when custom object field is of type picklist  it is returning the id as new entry in list.

Is it something to do with the below code

string.valueOf(historyLine.get('newValue')).startsWith('005') || string.valueOf(historyLine.get('newValue')).startsWith('00G')).

 

How can i get rid of that records.

 

 

Vishal_ThoriyaVishal_Thoriya

hi JustinMcc

have you solved that bug using the (newValue != null)?

 

but i am still facing the same error. can u please help me....?

 

any kind of help will be appriciated......

 

thanks in advance.

Mike.KatulkaMike.Katulka

=========================================

=========================================

Here is the latest up to date working Component,

Controller, and my Unit Test (tricky) for

the GenericHistoryComponent

=========================================

=========================================

 

The bug fixes here are: (see previous posts for specifics.)

1. ParentID vs ObjectNameID  (fixed so this works for standard objects.)

2. OldValue was supposed to read NewValue.

3. If no object passed in, return empty list.

4. Opportinuty history object has the suffix FieldHistory.

5. v24 of the Salesforce API test classes cannot see existing org data by default.  Added (SeeAllData=true) to the test class.

6. Fixed the display of the history record created date to show as the users local time instead of GMT.

 

Component:

<apex:component controller="GenericHistoryComponentController">
    <!-- Attribute Definition -->
    <apex:attribute name="myObject" description="Object we wish to view the history of" type="SObject" required="true" assignTo="{!myObject}" />
    <apex:attribute name="recordLimit" description="Number of lines of history to display" type="Integer" required="false" assignTo="{!recordLimit}" />
    
    <!-- Object History Related List -->
    <apex:pageBlock title="{!objectLabel} History">
        <apex:pageBlockTable value="{!ObjectHistory}" var="History" >
            <apex:column headerValue="Date"  value="{!History.thedate}"/>
            <apex:column headerValue="User">
                <apex:outputLink value="/{!History.userId}"> {!History.who} </apex:outputLink>
            </apex:column>
            <apex:column headerValue="Action"><apex:outputText escape="false" value="{!History.action}"/></apex:column>
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:component>

Controller:

public class GenericHistoryComponentController {
    
    // External variables
    public SObject myObject {get; set;}
    public Integer recordLimit {get; set;}
    public static String objectLabel {get;}
    
    // Internal Variables
    public objectHistoryLine[] objectHistory; 

    public static final Map<String, Schema.SObjectType> mySObjectTypeMap = Schema.getGlobalDescribe();
    public static Map<String, Schema.SObjectField> myObjectFieldMap;
    public static List<Schema.PicklistEntry> historyFieldPicklistValues;
    
    public List<objectHistoryLine> getObjectHistory(){
	// if no object passed in, return empty list
        if (myObject == null) {
            return new List<objectHistoryLine>();
        }

        Id myObjectId = String.valueOf(myObject.get('Id'));
        Schema.DescribeSObjectResult objectDescription = myObject.getSObjectType().getDescribe();

        myObjectFieldMap = objectDescription.fields.getMap();
        objectLabel = String.valueOf(objectDescription.getLabel());
        
        //Get the name of the history table
        String objectHistoryTableName = objectDescription.getName();
        
        //ID field name         
		string ObjectIdName;
		
		//if we have a custom object we need to drop the 'c' off the end before adding 'History' to get the history tables name        
		if (objectDescription.isCustom()){            
			objectHistoryTableName = objectHistoryTableName.substring(0, objectHistoryTableName.length()-1);
			ObjectIdName = 'ParentId';        
		}        
		else{
		    ObjectIdName = objectHistoryTableName+ 'Id';
		}
		
	if(objectHistoryTableName == 'Opportunity') {objectHistoryTableName = objectHistoryTableName + 'FieldHistory';}
	else {objectHistoryTableName = objectHistoryTableName + 'History';}
        
        Schema.DescribeFieldResult objectHistoryFieldField = mySObjectTypeMap.get(objectHistoryTableName).getDescribe().fields.getMap().get('Field').getDescribe();
        historyFieldPicklistValues = objectHistoryFieldField.getPickListValues();
                    
        list<objectHistoryLine> objectHistory = new list<objectHistoryLine>();
        
        String prevDate = '';
                
        if (recordLimit== null){
            recordLimit = 100;
        }
                
        list<sObject> historyList = Database.query( 'SELECT CreatedDate,'+
                                                    'CreatedById,'+
                                                    'Field,'+
                                                    'NewValue,'+
                                                    'OldValue ' +
                                                    'FROM ' + objectHistoryTableName + ' ' +
                                                    'WHERE ' + ObjectIdName + ' =\'' + myObjectId + '\' ' +
                                                    'ORDER BY CreatedDate DESC '+
                                                    'LIMIT ' + String.valueOf(recordLimit));
        
        for(Integer i = 0; i < historyList.size(); i++){
            sObject historyLine = historyList.get(i);
            if ((historyLine.get('newValue') == null && historyLine.get('oldValue') == null) 
                    || (historyLine.get('newValue') != null && !(string.valueOf(historyLine.get('newValue')).startsWith('005') || string.valueOf(historyLine.get('newValue')).startsWith('00G')))
                    || (historyLine.get('oldValue') != null && !(string.valueOf(historyLine.get('oldValue')).startsWith('005') || string.valueOf(historyLine.get('oldValue')).startsWith('00G')))){
                objectHistoryLine tempHistory = new objectHistoryLine();
                // Set the Date and who performed the action
                if (String.valueOf(historyLine.get('CreatedDate')) != prevDate){
                    tempHistory.theDate = datetime.valueof(historyLine.get('CreatedDate')).format();
                    tempHistory.userId = String.valueOf(historyLine.get('CreatedById'));
                    tempHistory.who = String.valueOf(historyLine.get('CreatedById'));
                }
                else{
                    tempHistory.theDate = '';
                    tempHistory.who = '';
                    tempHistory.userId = String.valueOf(historyLine.get('CreatedById'));
                }
                prevDate = String.valueOf(historyLine.get('CreatedDate'));
                
                // Get the field label
                String fieldLabel = GenericHistoryComponentController.returnFieldLabel(String.valueOf(historyLine.get('Field')));
                
                // Set the Action value
                   if (String.valueOf(historyLine.get('Field')) == 'created') {    // on Creation
                       tempHistory.action = 'Created.';
                  }
                  else if (historyLine.get('oldValue') != null && historyLine.get('newValue') == null){ // when deleting a value from a field
                      // Format the Date and if there's an error, catch it and re
                      try {
                         tempHistory.action = 'Deleted ' + Date.valueOf(historyLine.get('oldValue')).format() + ' in <b>' + fieldLabel + '</b>.';
                    } catch (Exception e){
                         tempHistory.action = 'Deleted ' + String.valueOf(historyLine.get('oldValue')) + ' in <b>' + fieldLabel + '</b>.';
                    }
                  }
                  else{  // all other scenarios
                    String fromText = '';
                    if (historyLine.get('oldValue') != null) {
                         try {
                              fromText = ' from ' + Date.valueOf(historyLine.get('oldValue')).format();
                         } catch (Exception e) {
                              fromText = ' from ' + String.valueOf(historyLine.get('oldValue'));
                         }
                    }
                    
                    String toText = '';
                    if (historyLine.get('newValue') != null) {
                        try {
                             toText = Date.valueOf(historyLine.get('newValue')).format();
                        } catch (Exception e) {
                             toText = String.valueOf(historyLine.get('newValue'));
                        }
                    }
                    if (toText != ''){
                        tempHistory.action = 'Changed <b>' + fieldLabel + '</b>' + fromText + ' to <b>' + toText + '</b>.';
                    }
                    else {
                        tempHistory.action = 'Changed <b>' + fieldLabel;
                    }
                   }
                   
                   // Add to the list
                   objectHistory.add(tempHistory);
            }
         }
         
         List<Id> userIdList = new List<Id>();
         for (objectHistoryLine myHistory : objectHistory){
             userIdList.add(myHistory.userId);
         }
         Map<Id, User> userIdMap = new Map<ID, User>([SELECT Name FROM User WHERE Id IN : userIdList]);
         
         for (objectHistoryLine myHistory : objectHistory){
             if (userIdMap.containsKey(myHistory.userId) & (myHistory.who != '') ){
                 myHistory.who = userIdMap.get(myHistory.who).Name;
             }
         }
         
         return objectHistory;
    }    
    
    // Function to return Field Label of a object field given a Field API name
    public Static String returnFieldLabel(String fieldName){

        if (GenericHistoryComponentController.myObjectFieldMap.containsKey(fieldName)){
            return GenericHistoryComponentController.myObjectFieldMap.get(fieldName).getDescribe().getLabel();
        }
        else {
            for(Schema.PicklistEntry pickList : historyFieldPicklistValues){
                if (pickList.getValue() == fieldName){
                    if (pickList.getLabel() != null){
                        return pickList.getLabel();
                    }
                    else {
                        return pickList.getValue();
                    }
                }
            }
        }
        return '';
    }
    
    // Inner Class to store the detail of the object history lines    
    public class objectHistoryLine {

        public String theDate {get; set;}
        public String who {get; set;}
        public Id userId {get; set;} 
        public String action {get; set;}
    }
}

 

Unit Test:

@isTest(SeeAllData=true)
public class GenericHistoryControllerTester {

  static testMethod void myTestGenericHistoryComponentController() {
        
        //Unit test by: Mike Katulka @ www.itedgecrm.com - mike@itedgecrm.com
        //Component by: http://boards.developerforce.com/t5/Visualforce-Development/History-related-List-using-Visualforce/td-p/151785
        //83% coverage
        
        //***Instructions MUST READ***
        //We cannot generate History from a unit test, and unable to manually insert records into the History tables from code.  This seems to be a limitation of Salesforce unit testing.
        //Created for Winter 12 org testing, updated to work with v24 API (SeeAllData=true)
        //Steps:
        // 1. Enable History Tracking for Account.
        // 2. You will need to declaratively make changes to an existing Account.  
        // 3. Create history in Sandbox and Production.
        // 4. Then run this test/deploy.
        // This test will look at the existing history in your org, and operate off of it.  
        
        //create controller
		GenericHistoryComponentController controller = new GenericHistoryComponentController();
		//null myObject
		system.assert(controller.getObjectHistory().size() == 0);

        //Account allows for history in related lists in visualforce, this is simply a standard object being used to get code coverage so you can use the component for other custom and standard objects.
        system.assert([select Accountid from accounthistory where isdeleted=false limit 1].size() == 1); //limit in case of 50,000 record governor limit
        string accid = [select Accountid from accounthistory where isdeleted=false limit 1].accountid;
        
		//Set myObject
		controller.myObject = [select id from Account where id=:accid];				
        system.assert(controller.getObjectHistory().size() > 0);        
  }

}

 

 

Mike Katulka

www.itedgecrm.com 

 

ColinKenworthy2ColinKenworthy2

Mike, you still need to test for myObject being equal to null (I described a few posts earlier) for it to work everywhere.

 

cheers

Colin.

Mike.KatulkaMike.Katulka

Hey Colin,  I just updated the controller, and the unit test in my last post.  Thanks!

 

Mike Katulka

www.itedgecrm.com

all2humanall2human

Opportunity field history is stored in the OpportunityFieldHistory object, so I had to change the code this way:

 

   if(objectHistoryTableName == 'Opportunity'){
        objectHistoryTableName = objectHistoryTableName + 'FieldHistory';
        }
    else{
        objectHistoryTableName = objectHistoryTableName + 'History';
    }

 

dfpittdfpitt

Thanks a lot for sharing.

 

How can I use this on a VF email?

 

I already:

- Changed the controller access to global

- Changed the apex:pageBlock to a apex:outputPanel

- Changed the apex:pageBlockTable to a apex:dataTable

 

But I can't reference the SObject directly in the VF Email template and If a reference the related list name, it doesn't take it

 

With this line:

<c:GenericHistoryComponent recordLimit="50" myObject="{!relatedTo.Related_List__r}"/>

 

I get this error:

Error: Wrong type for attribute <c:generichistorycomponent myObject="{!relatedTo.Related_List__r}">. Expected SObject, found Custom_Object__c[]

 

Any ideas?

ColinKenworthy2ColinKenworthy2

You would have to explain what kind of object "relatedTo" was in your email. Also you would have to expand on what the lookup / master-detail field Related_List__c was.

dfpittdfpitt

I was able to figure it out.

 

My recipientType is a User and my relatedToType is a custom object (CO1), which is related to another custom_object (CO2) with a master-detail relationship. I want to show the history table for each of the records of type CO2 that are related to my current CO1 record.

 

This is what I did, and its working:

            <apex:repeat var="cx" value="{!relatedTo.Related_List__r}">
                   <c:GenericHistoryComponent recordLimit="50" myObject="{!cx}"/>
            </apex:repeat>

carlocarlo

This is very impressive.

 

Thanks

Aliraza AliAliraza Ali

I can't get the test class to run. I am getting the following error on this line.. I tried executing the same line in the developer console and eclipse and it returns me 1 row.  I have had to remark all the system.assert statements also.

 

Please help.

 

 string accid = [select Accountid from accounthistory where isdeleted=false limit 1].accountid;

 

Class.GenericHistoryControllerTester.myTestGenericHistoryComponentController: line 28, column 1

 

System.QueryException: List has no rows for assignment to SObject

bob_buzzardbob_buzzard

Have you created this in version 24 of the API - that hides the data in the system from you.  If there is a record in there, you can use the @IsTest(SeeAllData=true) annotation on your test class/method.

Aliraza AliAliraza Ali

Thanks a million... your recommendation worked.

 

 

Mike.KatulkaMike.Katulka

I updated the test class and revision notes in my previous post.   See the latest component, class, and unit test on page 2, at the bottom.

 

Thanks Bob for the input!

reema_agarwalreema_agarwal

Hello

 

I want to track the history of assets with fields Asset name,Asset status and contact name. I am not getting how to do it can someone please hepl me with it i am new to apex.

 

Thanks!

Mike.KatulkaMike.Katulka

Assets don't look like they have History functionality.  See here http://success.salesforce.com/questionDetail?qId=a1X30000000JNiUEAW

 

Also, this post is for a more specific need, to be able to display history on a "Custom Visualforce Page".

Mike.KatulkaMike.Katulka

I just found a bug related to displaying the correct time to the user in the "Date" column of the component.  It was displaying the GMT time pulled from the history table.  Now it will display the proper time for the user logged in to view the component.

 

I updated the component controller and revision notes in my previous post.   See the latest component, class, and unit test on page 2, at the bottom.

clouduserclouduser

I have implemented this code. When I view in salesforce standard view, username is displaying properly in history. When I view in customer portal, it is not displaying the username instead it displays the userID. Do you have any thoughts to use it for both ways?

pcjones10pcjones10

Thanks Rajesh Shah,

I expanded on your code to ignore object Ids in the history list and added the "Show more »  |  Go to list » links.

 

You can get the code here:

 

https://github.com/pcjones10/Genteric-History-Component

adflintadflint

pcjones - for some reason your code is not filtering out the line items with Ids.  Cannot figure out why...I am using it for Case History and I am still seeing Ids in the history related list.  I have tried to change this.

 

Also, all - the code filters out any lines where the "value" of the field starts with "005" or "00G", this works fine unless you are referencing an auto numbered item like Case Number, once your Cases start with "005" then they won't show up...  Anyone know how to filter out Ids without using "StartsWith"?

cloud-developmentcloud-development

I'm amazed that Salesforce haven't provided this as standard! 

 

But equally amazed by the contributions of others to solve the problem - saved me days of work.

 

Thanks guys :)

 

ZathZath

Hi

 

I am getting the following error:

 

Error: <apex:attribute assignTo> cannot be same as the <apex:attribute name> ( myObject )

 

any ideas?

Sandeep123Sandeep123

Hi All,

 

This helped a lot for me, just having one issue for the same. if someone can help me ?

 

I am using GenericHistoryComponent for showing Approval history rather then using Standerd approval history, hence i am using approval process on that object, so in history section, it is displaying "Changed Record Locked"

 

So any one can help me to get rid of this, i don't want to display Record locked/Unlocked history. can i get rid of from histroy section ?

 

Thanks in advance. Appriciating your help for the same.

 

 

AndrCAndrC

Thanks for the help!

 

I wonder if it is also possible to filter the results by created date, for example only display fields that where changed after 01/01/2012?

 

Thanks again!

crop1645crop1645

Mike -- very helpful. Thanks. I made a small modification to your test method to:

  1. Put out a useful message to the test executor in case the sandbox or prod org dosn't have an Account with AccountHistory rows
  2. Ensure that the sandbox/prod org has an Account where fields were actually changed from old value to new value and avoid picking up Accounts that were only just 'created'

Code is here (V27.0) - 85% test coverage

	  @isTest(SeeAllData=true) static void myTestGenericHistoryComponentController() {
	        
	        //Unit test by: Mike Katulka @ www.itedgecrm.com - mike@itedgecrm.com
	        //Component by: http://boards.developerforce.com/t5/Visualforce-Development/History-related-List-using-Visualforce/t...
	        //83% coverage
	        
	        //***Instructions MUST READ***
	        //We cannot generate History from a unit test, and unable to manually insert records into the History tables from code.
// This seems to be a limitation of Salesforce unit testing. //Created for Winter 12 org testing, updated to work with v24 API (SeeAllData=true) //Steps: // 1. Enable History Tracking for Account. // 2. You will need to declaratively make changes to an existing Account. // 3. Create history in Sandbox and Production. // 4. Then run this test/deploy. // This test will look at the existing history in your org, and operate off of it. //create controller GenericHistoryComponentController controller = new GenericHistoryComponentController(); //null myObject system.assert(controller.getObjectHistory().size() == 0); //Account allows for history in related lists in visualforce,
// this is simply a standard object being used to get code coverage so you can use the component for other custom and standard objects. AggregateResult[] aggResList = [select AccountId, Count(ID) from AccountHistory Group By AccountId Having Count(ID) > 1 Limit 1]; System.assert(aggResList.size() > 0,'Test requires at least 1 Account with 2+ Account Field History rows'); //Set myObject controller.myObject = [select id from Account where id=: (ID)aggResList[0].get('accountId')]; system.assert(controller.getObjectHistory().size() > 0); }

 

 

staceyeileenstaceyeileen

This is fantastic!  

 

I am still getting history rows with IDs in them when a lookup is changed from null to a value.  If it's changed from one value to another the ID row is not appearing.  Any ideas on how to modify the code to eliminate these ID rows?  

Jessica ZurikJessica Zurik
Thanks for the answer!
Victor19Victor19
I am trying to set up test data for the controller without using SeeAllData=true. Can anyone suggest an approach that I could use?

Thanks!
Poornima HRPoornima HR
If you dont want to use SeeAllData=true , you need to create an instance of custom object history record and insert it.
Like this: 
Account a = new Account();
a.Status__c = 'New';
insert a;

AccountHistory ah = new AccountHistory();
ah.Field = 'Status__c'; // API name of the field you want to track
ah.parentId = a.Id
insert ah;

Then call the Controller methods

 
Samata DoshiSamata Doshi
Does anyone know how to support field level scecurity with this component? If a field is not accessible to x user, and y user who has access to this field updates this field. The x user shouldn't see the history of updates done by y user. In short the component needs to behave same as standard history related list. Appriciate your help.
Constance HUAConstance HUA
Will this be implemented in Lightning? Solution and code is here, I guess it's now less complicated to implement!

I don't have the possibility to write Apex apparently with my version (professionnal) so please make it accessible for all!

Thank you :)
sgkmillssgkmills
FYI, you can now display the History details of a custom object by using the following
<apex:relatedList id="HistoryList" list="Histories" />
 This wasn't possible with API versions 38.0 and below. My visualforce page kept on failing with the error 'Histories' is not a valid child relationship....
My visualforce page was 38.0. I changed it to 39.0 and now the above code doesn't cause the error.

See screenshot below:
Custom Object Related list