1

Our use case is quite simple. We need to display some list of OpportunityHistory records on visual force page. But client would like to be able to control what exact fields are displayed as well as their order. Sounds like ideal use case to use field set. But it looks like salesforce doesn't provide a way to use field set for this strange object, i.e. OpportunityHistory.

The best idea we've come up so far is to use field set on opportunity itself assuming that OpportunityHistory just holds subset of fields from opportunity (still would be a problem, cause client could include field from opp which is not in the opp history).

Any help would be appreciated. And thanks in advance.

3
  • Do they just want to see field history for fields in the field set? Or do they want to see some subset of NewValue, OldValue, Field, etc?
    – Adrian Larson
    Sep 22 '16 at 15:03
  • @AdrianLarson Just field history for fields in the field set.
    – wesaw
    Sep 22 '16 at 15:09
  • My mistake, I thought OpportunityHistory was the same thing as OpportunityFieldHistory.
    – Adrian Larson
    Sep 22 '16 at 15:48
2

One alternative to Field Set you might consider is a List Custom Setting. You may want to bake in some checks that the fields exist and are accessible.

public List<String> fields { get; private set; }

public MyController()
{
    this.fields = new List<SObjectField>();
    Map<String, SObjectField> schemaFields = SObjectType.OpportunityHistory.fields.getMap();
    for (MySetting__c setting : [SELECT Name FROM MySetting__c ORDER BY Order__c])
    {
        SObjectField field = schemaFields.get(setting.Name);
        if (field != null && field.getDescribe().isAccessible())
            fields.add(field);
    }
}

I assume you will come up with a better name than MySetting__c. Additionally, the above code assumes you will store the API Name of the field in the Name field of the Custom Setting (you only want to display each once anyway). It also assumes you will create a Number field named Order__c and make it required. Otherwise, you may want to add NULLS LAST to your query, as the default order is nulls first.

6
  • I was going to ask if storing this information in a custom setting would maintain the proper order, but then I saw ORDER BY Order__c. Probably worth noting (just to make it absolutely clear) that Order__c will need to be created as a field on the custom setting (the Number type seems like an intuitive fit). The default ordering direction in queries is ASC, so Order__c = 2 means that field would be the second group displayed (again, quite intuitive).
    – Derek F
    Sep 22 '16 at 15:48
  • Right, all your inferences are correct. :) I'll update it anyway just to be more explicit.
    – Adrian Larson
    Sep 22 '16 at 15:48
  • 1
    About the only real disadvantage that I can see is that the user would need to spell the api name of the field correctly (or does the keyset of sObject.fields.getMap() consist of the field labels?).
    – Derek F
    Sep 22 '16 at 15:51
  • 1
    No, the keys are the API Names. Yes, that is an important consideration. At least it is case-insensitive.
    – Adrian Larson
    Sep 22 '16 at 15:52
  • @AdrianLarson I was hoping to avoid using custom settings because of: 1) manual typing of api field names (I bet our client doesn't event know what api field name is); 2) it's rather inconvenient to insert some new field in the middle, i.e. you should update order__c for other records to make a space in advance.
    – wesaw
    Sep 22 '16 at 15:54
2

I believe that your idea of using a fieldset on Opportunity is the way that you want to go here.

The idea is that you'd have a controller extension to go along with your visualforce page, and in the constructor, you'd use the fieldset to determine which records in OpportunityHistory to display.

public class MyControllerExtension{
    public MyControllerExtension(ApexPages.StandardController con){
        // Retrieve the fieldset
        Schema.Fieldset oppFieldSet = sObjectType.Opportunity.Fieldsets.get('myFieldset');

        // Extract the api names of the fields in the fieldset
        List<String> fields = new List<String>();
        for(Schema.FieldSetMember fsm :oppFieldSet.getFields()){
            fields.add(fsm.getFieldPath());
        }

        // Use the field names to filter your query
        List<OpportunityHistory> oppHistory = [SELECT OldValue, NewValue, Field FROM OpportunityHistory WHERE Field IN :fields];
    }
}

Note that while my example code is mostly complete, it will need some tweaking to be useful with a Visualforce page.

This method won't run into any code issues if someone happens to include a field in the fieldset that isn't being tracked in history. That field will simply just never appear as a result in your Visualforce page.

+edit:

Whoops, looks like I forgot about the requirement of ordering, and I was assuming work on OpportunityFieldHistory instead of OpportunityHistory.

While the fields on OpportunityHistory are indeed simply a subset of the fields on Opportunity, the Field field doesn't exist on this object (so we can't filter on that).

What you're trying to accomplish is to replace the standard Stage History related list (which can't be edited, hence, your question).

This makes matters just a little more complicated. We can still use a fieldset, but we now need to ensure the fields in the fieldset are actually in OpportunityHistory.

Sets will help us out here.

public class MyControllerExtension{

    public List<String> fieldsToDisplay {get; private set;}
    public List<OpportunityHistory> historyResults {get; private set;}

    public MyControllerExtension(ApexPages.StandardController con){

        // Retrieve the fieldset

        Schema.Fieldset oppFieldSet = sObjectType.Opportunity.Fieldsets.get('myFieldset');

        // Because we're working with OpportunityHistory, we need to ensure that 
        //   our given fields are actually on the object.
        // Normally, I'd suggest using the retainAll() method of the set class
        //   but that would destroy the ordering.
        // We'll just need to use contains() as we iterate over the fieldset.

        Set<String> oppHistoryFields = sObjectType.OpportunityHistory.fields.getMap().keySet();

        // Extract the api names of the fields in the fieldset

        for(Schema.FieldSetMember fsm :oppFieldSet.getFields()){
            if(oppHistoryFields.contains(fsm.getFieldPath())){
                fieldsToDisplay.add(fsm.getFieldPath());
            }
        }

        // This SOQL query doesn't need to be dynamic, but it might save some typing.

        historyResults = database.query('SELECT ' + String.join(new List<String>(sObjectType.OpportunityHistory.fields.getMap().keySet()), ', ') + FROM OpportunityHistory WHERE OpportunityId = <id from standard controller>);
    } 
}

In the Visualforce page, an <apex:repeat> inside of an <apex:pageBlockTable> should be able to take care of ordering the fields

<apex:PageBlockTable value="{!historyResults}" var="History">
    <apex:repeat value="{!fieldsToDisplay}" var="field">
        <apex:column hearderValue="{!field}" value="{!History[field]}" />
    </apex:repeat>
</apex:PageBlockTable>
5
  • I think I confused OpportunityHistory with other field history tracking objects. It seems to be different based on the SOAP API, otherwise this is almost verbatim what I would have recommended. Great minds! :)
    – Adrian Larson
    Sep 22 '16 at 15:42
  • @AdrianLarson Yep, there are two different objects. I need OpportunityHistory, but Derek's elegant solution relies on OpportunityFieldHistory unfortunately.
    – wesaw
    Sep 22 '16 at 15:46
  • @wesaw I also realized that my answer doesn't order results by the order defined in the fieldset. I'll see if I can't tweak this for working on OpportunityHistory instead of OpportunityFieldHistory
    – Derek F
    Sep 22 '16 at 15:53
  • @wesaw Updated my answer
    – Derek F
    Sep 22 '16 at 17:04
  • 1
    @DerekF Thanks, looks promising. The only use case when this wouldn't work, if field set contained fields from related object, e.g. CreatedBy.Name
    – wesaw
    Sep 22 '16 at 17:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.