Wednesday, August 14, 2013

Custom Sort feature in Salesforce based on many fields level- Part III

In Salesforce we often face cases where we need to customize Sort feature. Sort means sorting of records in related list of any parent record. For example: Opportunity has its child records of Opportunity Line Items. In related list of OLIs at an Opportunity detail page, standard 'Sort' button is present. Clicking on this sort button navigates to a Salesforce standard page where we can sort based on only Product names.

But we can not override Sort button with custom Visualforce page for our customized features. So I took a workaround where I can sort records based on say 3 fields of OLI while click sort button .

This is sort by level. To clarify,
You have records of Students with fields FirstName, Surname, Score etc.
Now 1st sort based on First name:
A                Terry             20         
A               
Smith             55
B                Naidu            35
C                Raman            32

Then you want to sort based on Surname on above set of data.It becomes: 
A                Smith             55         
A               
Terry              20
B                Naidu            35
C                Raman            32
And so on...
I will share my code for achieving this kind of sort on Opportunity Line Items . 

1- At any Opportunity detail page, add custom related list of OLI . There place a button Sort. You can also add custom button Sort at standard related list, when you move to your page and sort there and come back, your sorted list of records will NOT be shown. Because in standard related list always sort order depends on a read-only field of SFDC in OLI named 'sortorder'.
    So, either you place a VF page in a section at Opportunity layout and there place your custom related list with button or, override detail page of Opportunity to display custom related list of OLI. For detail look at post:

Whatever you choose, page is
OLIInVFPageSectionII for me now.
2- Click 'Sort' . Move to your sort page 'DemoSortIII'
3- Sort selecting desired field and back to original page '
OLIInVFPageSectionII'.

4- We need to create 2 fields in Opportunity Line Item to store our sort order: Sorted_By_fieldnameI and Sorted_By_fieldnameII

Visualforce page  DemoSortIII

<apex:page standardController="Opportunity" extensions="SorterControllerLevelWise" >
  <apex:form >
      <apex:pageBlock title="Sort by field:" >
        <apex:outputPanel >
        <!-- dropdown box for levels with field names-->
        First field:
        <apex:selectList value="{!fieldnamechosenI}" size="1" >
                <apex:selectOptions value="{!levels}"/>
                <apex:actionSupport event="onchange" reRender="out"  action="{!fetchTable}" status="tablestatus"/>
        </apex:selectList>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        Then By:
        <apex:selectList value="{!fieldnamechosenII}" size="1" >
                <apex:selectOptions value="{!levels}"/>
                <apex:actionSupport event="onchange" reRender="out"  action="{!fetchTable}" status="tablestatus"/>
        </apex:selectList>
        </apex:outputPanel>
    </apex:pageBlock>
    
     <apex:pageBlock title="List of records:" >
        <apex:pageblockbuttons >
        <apex:commandButton action="{!Save}"  value="Save" />
        </apex:pageblockbuttons>
        
    <!-- table to display sorted data -->
        <apex:outputPanel id="out">
        <apex:actionstatus id="tablestatus" startText="loading...">
        <apex:facet name="stop">
        <apex:pageBlockSection columns="1" ID="pbtI">
            <apex:pageblockTable value="{!sortedListOfOLI }" var="pbt">
            <apex:column value="{!pbt.Quantity}"/>
            <apex:column value="{!pbt.TotalPrice}"/>
            
            </apex:pageblockTable>
            
        </apex:pageBlockSection>
        </apex:facet>
        </apex:actionstatus>
        </apex:outputPanel>
    </apex:pageBlock>
   </apex:form>
</apex:page>
Its controller:




public class SorterControllerLevelWise{
   

    private String sortedBy = null;
    private Boolean sortAscending = null;
    public String fieldnamechosenI { get; set; }
    public String fieldnamechosenII { get; set; }
    private AP_SortHelperLevelWise sorter = new AP_SortHelperLevelWise ();
    public List sortedListOfOLI {get;set;}
    public Opportunity opp;
    
    public SorterControllerLevelWise(ApexPages.StandardController controller) {
        this.opp = (Opportunity)controller.getRecord();
        Opportunity currOpp = [SELECT Id,Sorted_By_fieldnameI__c ,
                               Sorted_By_fieldnameII__c FROM Opportunity WHERE Id = :opp.Id];
        sorter.originalList = [SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId =:opp.id];
        /*if(currOpp.Sorted_By_fieldnameI__c != null &&  currOpp.Sorted_By_fieldnameII__c!= '')
            sortedList = (List)sorter.getSortedList(currOpp.Sorted_By_fieldnameI__c, 
                                                currOpp.Sorted_By_fieldnameII__c, opp.id);*/
        sortedListOfOLI = [SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId =:opp.id];
    }
    public PageReference sortthese(){
    
        return new PageReference ('/apex/DemoSortIIILevelBased?Id='+opp.Id);
    }
    
     public SorterControllerLevelWise() {
    }
    
    public List getLevels() {
        List options = new List();
        options.add(new SelectOption('-None-','-None-'));
        options.add(new SelectOption('Quantity','Quantity'));
        options.add(new SelectOption('TotalPrice','TotalPrice'));
        return options;
    }
            
    public void fetchTable(){
        //fetch table records when fields are differently properly chosen at dropdpwns
        
        if(fieldnamechosenI != fieldnamechosenII && fieldnamechosenI != '-None-'){
            sortedListOfOLI = (List) 
                              sorter.getSortedList(fieldnamechosenI,fieldnamechosenII, opp.id);
        }
        else {
            sortedListOfOLI = [SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId =:opp.id];
        }
        
    }
        
    public PageReference Save() {
        PageReference pr = new PageReference('/apex/OLIInVFPageSectionII') ;
        pr.setRedirect(false);
        sorter.saveOpp(opp.id);
        return pr;
    }
     
}

Class called from above class AP_SortHelperLevelWise


public class AP_SortHelperLevelWise {     // 
    public Map listPosition = null;    // >
    public Map> sortedFieldValuesPerFieldName = null;     // >>
    public Map>> sObjectIDsPerFieldNames = null;
    public String OpportuniId;
    public String field1Is;
    public String field2Is;
// Properties
    public List originalList {get; set;}
    
// Constructor
    public AP_SortHelperLevelWise() {
        originalList = null;
    }
    
    public List getSortedList(String fieldNameI, String fieldNameII, String oppId) {
        //make soql string for query records
        String soql = 'SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId = \''+oppId+
                      '\' ORDER BY ';
        //when field 1 is chosen others are none
        if(fieldNameI != null && fieldNameI != '-None-')
            soql = soql+fieldNameI;
        //when field 2 is not None
        if(fieldNameII != null && fieldNameII != '-None-' && fieldNameI != 'None')
            soql = soql+' ,'+fieldNameII;
        System.debug('\n\nthe string soql is ='+soql+'\n\n');
        List sortedList = Database.query(soql);
                
        
        return sortedList;
    }
 
    public void saveOpp(String OpportuniIdParam){
        Opportunity currOpp = [SELECT Id FROM Opportunity WHERE Id =:OpportuniIdParam];
        currOpp.Sorted_By_fieldnameI__c = field1Is;
        currOpp.Sorted_By_fieldnameII__c = field2Is;        
        upsert currOpp;
   }

    
}

Friday, August 9, 2013

Custom look and feel of standard Look Up Field feature in Salesforce

We all know well about 'look up ' field in Salesforce. When  we use any look-up field of an object in our Visualforce page, that automatically takes look and feel of standard look at any standard Salesforce  record detail page of that.
But say, you want to get look and feel of look-up feature in your Visualforce page for any requirement. I mean, say, you have an input field in you Visualforce page which is boned with a String at Alex class. Now you want to input in that field some value after clicking a glass icon and from a list of records in pop-up...Just same for any standard look-up feature. What I did is something I am going to share here.
Standard lookup feature:





Custom look and feel like above feature in VF page:




How it works:
1- A visualforce page where Student input field present and we click Icon there.
2- Then a new pop up window opens up with list of Students and also with search feature.
3- Click proper record and that comes as input to first page input field.

Visualforce page where Student input field resides:
<apex:page controller="RegistrationFormCreationController">
    <script>
   function callFunction(target, file_id, file_name){
        var column = document.getElementById(target);
        column.value = file_id;
        var viewLink = document.getElementById(target + '_link');
       
        viewLink.href = '/'+ file_id;
        viewLink.value = file_name;
        //viewLink.innerHTML = file_name;
       
        var wrap = document.getElementById(target + '_wrap');
        wrap.style.display = 'none';
    }
    </script>
  <apex:form >
<apex:pageMessages id="msgs" />
  <apex:pageblock >
        <apex:pageblockButtons >
          <apex:commandButton id="saveButton" value="Save" action="{!saveTheForm}"/>
        </apex:pageblockButtons>
  <apex:pageblockSection >
   <apex:pageblockSectionItem >
      <apex:outputLabel >Student</apex:outputLabel>
      <apex:outputLabel >
          <span id="document05_wrap">
          <apex:outputLabel >{!selectedStudent}</apex:outputLabel>
          </span>
          <input type="text" value="{!selectedStudent}" id="document05_link" name="document05a"/>
          <input type="hidden" value="{!selectedStudent}" id="document05" name="document05"/>
          <apex:commandLink id="brw" onclick="window.open('StudentBrowsing?column=document05','','width=500,height=500')">
              <img src="/s.gif" class="lookupIcon"/>
          </apex:commandLink>
       </apex:outputLabel>
      </apex:pageblockSectionItem>  
   </apex:pageblockSection>
 </apex:pageblock>
</apex:form>
</apex:page>

Its controller:

public class RegistrationFormCreationController{

    public String selectedStudent {get;set;}
    public RegistrationFormCreationController() {
    }//end of constructor
    
    /*
    Purpose: Method called in button click of save the form
    Parameter: None
    Return type: Page reference
    */
    public PageReference saveTheForm(){
     //do something   
    }    
}
Now , main Visualforce page for Pop Up window- 'StudentBrowsing'

<apex:page controller="StudentBrowsingcontroller" showheader="false">
<html>
    <head>
    <script type="text/javascript">
           function select (file_id, file_name)
            {
               if (! file_name)
                    file_name = document.getElementById (file_id).value;
                var target = '{!JSENCODE(columnName)}';
                //alert('Column'+target+'=='+file_id+'=='+file_name);
                window.opener.callFunction (target, file_id, file_name);
                window.close ();
            }
    </script>
    </head>
    <body>
        <div style="height: 500px; overflow-y: scroll">
        <apex:form >
        <!-- search feature -->
        <div style="margin: 10px;">
                <fieldset class="ui-corner-all">
                <legend class="Browsing">Browsing</legend>
                <Table caption="Browsing">
                    <tr>
                        <td> Student name </td>
                        <td>
                        <apex:inputText value="{!word}">
                            <apex:actionSupport event="onkeyup" action="{!reload}" 
reRender="studList" status="status" />
                        </Apex:inputText>
                        </td>
                    </tr>
                </table>
                </fieldset>
                <div style="text-align: center">
                    <apex:actionStatus id="status">
                        <apex:facet name="start">
                            <apex:outputPanel >Loading.....
                                <apex:image value="/img/loading32.gif" style="height: 15px;"/>
                                <!--<img src="/apexpages/devmode/img/saveStatus.gif" />-->
                            </apex:outputPanel>
                        </apex:facet>
                    </apex:actionStatus>
                </Div>           
            </Div>
        
      <!-- students list -->
            <apex:pageBlock Title="Students"> 
                <apex:pageBlockSection columns="1">
                    <apex:pageBlockTable value="{!students}" var="stud" id="studList">
                        <apex:column HeaderValue="Name"> 
                              <a href="javascript:select('{!stud.Id}');" > {! stud.Name} </a>
                              <input type="hidden" id="{!stud.Id}" value="{!stud.Name}" />
                          </apex:column> 
                   </apex:pageBlockTable>
                </apex:pageBlockSection>
           </Apex:pageBlock>
        </Apex:form>  
        </Div>
    </Body>
</html>

</Apex:page>
                                        
 Its controller:
 



public class StudentBrowsingcontroller{

    public List students { get; set; }
    
    public String columnName{set;get;}
    
    public String word{set;get;}
    
    //COnstructor
    
    public StudentBrowsingcontroller(){
        
        // get column name value from URL parameter, it is used in placing the selected value at base window exact space
        
        columnName = System.currentPageReference().getParameters().get('column');
        // fetching the Students from Contact object
        
        students = [SELECT Id, Name FROM Contact WHERE RecordType.Name = 'Student'];
    }
    
    /*
    Purpose: Action called on key press of search box. A list of related records based on search word will be fetched
    Parameters: None
    Return type: Page Reference    
    */
    
    public PageReference reload() {
    
        try{
            // based on typed word fetching the Students from Contact object
            
            if( word != null && word != ''){
                word = '%' + word + '%';
                students = [SELECT Id, Name FROM Contact WHERE RecordType.Name = 'Student' AND 
                            Name like :word ORDER BY LastModifiedDate];
            }
            return null;
         }
         catch(QueryException qe){
             ApexPages.addMessage( new ApexPages.Message(ApexPages.Severity.ERROR, qe.getMessage()));
             return NULL;
         }
         catch(Exception e){
             ApexPages.addMessage( new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage()));
             return NULL;
         }
   }

}


Custom search from list of records in Visualforce page: Look alike standard lookup feature

In Salesforce , we know well about look up field. At any standard record detail page, we can input look-up field value after clicking look-up icon and choosing correct data. Or at any Visualforce page also, if there is any look-up field of object bonded in page, then using <apex:input> tag automatically provide us facility to choose record from look-up window.

In look-up window, we can see list of records at bottom section and at top section, 'Search' among those records. Now, say, we have a situation where we need such a window which is totally our custom. I mean from our Visualforce page clicking some icon/link/button we have reached to a custom window where list of records are displayed in a table and there we need a 'Search' facility among those records . Following is detail of code how I did it.
My objective was to search records from list of Students record in below image:

How search works:
1- Type your search key in top section input box
2- when you type, based on your string input below list is filtered.
I am posting modified code from my original code, because that includes lots more features.
Visualforce Page:
<apex:page controller="StudentBrowsingcontroller" showheader="false">
<html>
    <head>
    </head>
    <body>
        <div style="height: 500px; overflow-y: scroll">
        <apex:form >
        <!-- search feature -->
        <div style="margin: 10px;">
                <fieldset class="ui-corner-all">
                <legend class="Browsing">Browsing</legend>
                <Table caption="Browsing">
                    <tr>
                        <td> Student name </td>
                        <td>
                        <apex:inputText value="{!word}">
                            <apex:actionSupport event="onkeyup" action="{!reload}" reRender="studList" status="status" />
                        </Apex:inputText>
                        </td>
                    </tr>
                </table>
                </fieldset>
                <div style="text-align: center">
                    <apex:actionStatus id="status">
                        <apex:facet name="start">
                            <apex:outputPanel >Loading.....
                                <apex:image value="/img/loading32.gif" style="height: 15px;"/>
                            </apex:outputPanel>
                        </apex:facet>
                    </apex:actionStatus>
                </Div>           
            </Div>
       <!-- students list -->
            <apex:pageBlock Title="Students"> 
                <apex:pageBlockSection columns="1">
                    <apex:pageBlockTable value="{!students}" var="stud" id="studList">
                        <apex:column HeaderValue="Name"> 
                              <apex:outputText value="{!stud.Name}"></apex:outputText>
                          </apex:column> 
                   </apex:pageBlockTable>
                </apex:pageBlockSection>
           </Apex:pageBlock>
        </Apex:form>  
        </Div>
    </Body>
</html>
Apex code:

                                                                



public class StudentBrowsingcontroller{

    public List students { get; set; }
    
    public String word{set;get;}
    
    //COnstructor
    
    public StudentBrowsingcontroller(){
        
       // fetching the Students from Contact object
        
        students = [SELECT Id, Name FROM Contact WHERE RecordType.Name = 'Student'];
    }
    
    /*
    Purpose: Action called on key press of search box. A list of related records based on search word will be fetched
    Parameters: None
    Return type: Page Reference    
    */
    
    public PageReference reload() {
    
        try{
            // based on typed word fetching the Students from Contact object
            
            if( word != null && word != ''){
                word = '%' + word + '%';
                students = [SELECT Id, Name FROM Contact WHERE
                            Name like :word ORDER BY LastModifiedDate];
            }
            return null;
         }
         catch(QueryException qe){
             ApexPages.addMessage( new ApexPages.Message(ApexPages.Severity.ERROR, qe.getMessage()));
             return NULL;
         }
         catch(Exception e){
             ApexPages.addMessage( new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage()));
             return NULL;
         }
   }

}

Tuesday, August 6, 2013

Custom Sort feature at Salesforce - Part II


Read post: Custom Sort feature at Salesforce - Part I

In Salesforce we often face cases where we need to customize Sort feature. Sort means sorting of records in related list of any parent record. For example: Opportunity has its child records of Opportunity Line Items. In related list of OLIs at an Opportunity detail page, standard 'Sort' button is present. Clicking on this sort button navigates to a Salesforce standard page where we can sort based on only Product names.


But we can not override Sort button with custom Visualforce page for our customized features. So I took a workaround where I can sort records based on say 3 fields of OLI while click sort button .
First, create 2 custom fields at Opportunity.Sorted_By_field__c and Sorted_By_Asc__c

1- Add a section from page layout of Opportunity, then add a VF page at that section 
which shows custom Related list of OLI. And there is button 'Sort Oli'.
2- Click on custom button 'Sort OLI' at that related list. Go to your page.
N.B. You may add custom Sort button at standard related list of OLI also 
which will lead to your page for custom sort. But when you sort there and come back then 
sorted order will not be shown in standard related list.
3- Sort then. click Save Sort there.
4- back to Opportunity detail page.


1- Add a VF page at a section from Edit Layout of Opportunity, where my custom button ' Sort OLI' will be present.





VF page 'NewSectionPageFOrOLI':
<apex:page standardController="Opportunity" extensions="customOLIController">
<apex:form >
    <apex:pageblock id="pageBlock" >
        <apex:pageblockButtons >
        <apex:commandButton value="Sort These" action="{!sortthese}" dir=""/>
        </apex:pageblockButtons>

        <apex:pageBlockTable id="pbt" value="{!sortedList}" var="wOn" >
          
            <!-- Displaying the Field Values -->
            <apex:column value="{!wOn.Quantity}" />
            <apex:column value="{!wOn.TotalPrice}"/>
          
        </apex:pageBlockTable>
    </apex:pageblock>
</apex:form>
</apex:page>


And the controller of this page where code for button lies is as follow:

public class customOLIController {
   

    private String sortedBy = null;
    private Boolean sortAscending = null;
    private AP_SortHelperCopied sorter = new AP_SortHelperCopied ();
    public List<OpportunityLineItem> sortedList {get;set;}
    public Opportunity opp;
    
    public customOLIController (ApexPages.StandardController controller) {
        this.opp = (Opportunity)controller.getRecord();
        Opportunity currOpp = [SELECT Id,Sorted_By_field__c, Sorted_By_Asc__c FROM Opportunity WHERE Id = :opp.Id];
        sorter.originalList = [SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId =:opp.id];
        if(currOpp.Sorted_By_field__c != null &&  currOpp.Sorted_By_field__c != '')
            sortedList = (List<OpportunityLineItem>)sorter.getSortedList(currOpp.Sorted_By_field__c, currOpp.Sorted_By_Asc__c, opp.id);
     
    }
    public PageReference sortthese(){
    
    return new PageReference ('/apex/DemoSortII?Id='+opp.Id);
    }
    
     public customOLIController() {
    }
    
    public PageReference Save() {
        PageReference pr = new PageReference('/apex/NewSectionPageFOrOLI') ;
        pr.setRedirect(false);
        sorter.saveOpp(opp.id);
        return pr;
    }
    public PageReference SortByQuantity() {
        setSortedBy('Quantity');

        sortedList = (List<OpportunityLineItem>) sorter.getSortedList('Quantity', sortAscending, opp.id);
        return null;
    }
    public PageReference SortByTotalPrice() {
        setSortedBy('TotalPrice');

        sortedList = (List<OpportunityLineItem>) sorter.getSortedList('TotalPrice', sortAscending,opp.id);
        return null;
    }
    
    public List<OpportunityLineItem> getList() {
        if (sortedList == null) {
            SortByQuantity();
        }
        return sortedList;
    }
    private void setSortedBy(String value) {
        if (sortedBy == value) {
             sortAscending = !sortAscending;
        } else {
            sortAscending = true;
        }
        sortedBy = value;
    }
}
The class called from this class is:

public class AP_SortHelperCopied {     // 
    public Map listPosition = null;    // >
    public Map> sortedFieldValuesPerFieldName = null;     // >>
    public Map>> sObjectIDsPerFieldNames = null;
    public String OpportuniId;
    public Boolean ascIs;
    public String fieldIs;
// Properties
    public List originalList {get; set;}
    
// Constructor
    public AP_SortHelperCopied () {
        originalList = null;
    }
    // Public Method
    public List getSortedList(String fieldName, Boolean ascending, String oppId) {
        if (originalList == null) {
            // Assume that originalList has a not NULL value.
            // If the class who uses this method has not assigned a value it will get an Exception which
            //    needs to be handled by the calling class.            // Force the exception...
            originalList.clear();
        }        // Make field name uppercase
        fieldName = fieldName.toUpperCase();        // Get sorted list
        OpportuniId = oppId;
        return makeSortedList(fieldName, ascending);
    }
    public List getSortedList(List originalList, String fieldName, Boolean ascending, String oppId) {
        this.originalList = originalList;
        sortedFieldValuesPerFieldName = null;
        OpportuniId = oppId;
        return getSortedList(fieldName, ascending,OpportuniId);
    }
    
// Private Methods
    public void InitializeFieldName(String fieldName) {
        String sObjectID;
        Integer position;
        String fieldValue;
        List sObjectIDs = null;
        Set valuesForFieldSet = null;    // Sets automatically omit duplicate values 
        List valuesForFieldList = null;
        Map> sObjectIDsPerFieldValues = null;
        
        // Make sortedFieldValuesPerFieldName
        if (sortedFieldValuesPerFieldName == null) {
            listPosition = new Map();
            sortedFieldValuesPerFieldName = new Map>();
            sObjectIDsPerFieldNames = new Map>>();
        }
        
        // Get (or create) map of sObjectIDsPerFieldValues
        sObjectIDsPerFieldValues = sObjectIDsPerFieldNames.get(fieldName);
        if (sObjectIDsPerFieldValues == null) {
            sObjectIDsPerFieldValues = new Map>();
            sObjectIDsPerFieldNames.put(fieldName, sObjectIDsPerFieldValues);
        }
        if (!sortedFieldValuesPerFieldName.keySet().contains(fieldName)) {
            // Objects need to be initialized
            position = 0;
            valuesForFieldSet = new Set();
            listPosition = new Map();
            
            for (sObject sObj : originalList) {
                sObjectID = sObj.ID;
                fieldValue = getValue(sObj, fieldName);
                
                // Add position to list
                listPosition.put(sObjectID, position++);
                
                // Add the value to the set (sets rather than lists to prevent duplicates)
                valuesForFieldSet.add(fieldValue);
                
                // Get (or create) map of sObjectIDs
                sObjectIDs = sObjectIDsPerFieldValues.get(fieldValue);
                if (sObjectIDs == null) {
                    sObjectIDs = new List();
                    sObjectIDsPerFieldValues.put(fieldValue, sObjectIDs);
                }
                
                // Add ID to sObjectIDs
                sObjectIDs.add(sObjectID);
            }
            
            // Sort set items (Need to convert to list)
            valuesForFieldList = new List();
            valuesForFieldList.addAll(valuesForFieldSet);
            valuesForFieldList.sort();
            
            // Now add it to the map.
            sortedFieldValuesPerFieldName.put(fieldName, valuesForFieldList);
        }
    }
    public List makeSortedList(String fieldName, Boolean ascending) {
        Integer position;
        List sObjectIDs = null;
        List valuesForFieldList = null;        // Initialize objects
        InitializeFieldName(fieldName);        // Get a list of the same type as the "originalList"
        List outputList = originalList.clone();
        outputList.clear();        // Get a list of sorted values
        valuesForFieldList = sortedFieldValuesPerFieldName.get(fieldName);
        
        // for each sorted value
        for (String fieldValue : valuesForFieldList) {
            // Get lisft of IDs
            sObjectIDs = sObjectIDsPerFieldNames.get(fieldName).get(fieldValue);
            
            // for each ID
            for (String ID : sObjectIDs) {
                // Get position in originalList
                position = listPosition.get(ID);                // Add each sObject to the list.
                if ((ascending) || (outputList.size()==0)) {
                    outputList.add(originalList[position]);
                } else {
                    outputList.add(0, originalList[position]);
                }
            }
        }
        System.debug('\n\nI am returning the output = '+fieldName+'and = \n'+ascending+'\n');
       
        fieldIs = fieldName;
        ascIs = ascending;
        

        return outputList;
    }
    public void saveOpp(String OpportuniIdp){
        Opportunity currOpp = [SELECT Id FROM Opportunity WHERE Id =:OpportuniIdp];
        currOpp.Sorted_By_field__c = fieldIs;
        currOpp.Sorted_By_Asc__c = ascIs;
        
        upsert currOpp;
        
    
    }
    public String getValue(sObject sObj, String fieldName) {
        // This returns the sObject desired in case the fieldName refers to a linked object.
        Integer pieceCount;
        String[] fieldNamePieces;
        
        fieldNamePieces = fieldName.split('\\.');
        pieceCount = fieldNamePieces.size();
        for (Integer i = 0; i < (pieceCount-1); i++) {
            sObj = sObj.getSObject(fieldNamePieces[i]);
        }
        return String.valueOf(sObj.get(fieldNamePieces[pieceCount-1]));
    }
 
    
}

2- Notice that Sort button click will navigate to DemoSortII, a VF page where customization for sorting based on 2/3 columns present. In below image, you can see table at DemoSortII page where OLIs are there and each column has a button which gives facility to sort by that field.Finally clicking on save returns to original Opportunity detail page and related list shows sorted list by our choice which we have made at DemoSortII page.

VF page DemoSortII:
<apex:page standardController="Opportunity" extensions="customOLIController" >
  <apex:form >
    <apex:pageBlock >
    <apex:commandButton action="{!Save}"  value="Save" />
      <apex:pageBlockSection columns="1" ID="AjaxTable">
        <apex:datatable value="{!List}" var="acc" Border="1" cellspacing="1" cellpadding="5">
          <apex:column >
            <apex:facet name="header">
              <apex:commandButton action="{!SortByQuantity}" value="Sort By Quantity" rerender="AjaxTable" />
            </apex:facet>
            <apex:outputText value="{!acc.Quantity}" />
            </apex:column>
          <apex:column >
            <apex:facet name="header">
            <apex:commandButton action="{!SortByTotalPrice}" value="Sort By Price" rerender="AjaxTable" />
            </apex:facet>
            <apex:outputText value="{!acc.TotalPrice}" />
          </apex:column>
        </apex:datatable>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>


Controller of this class is same as above class customOLIController.


Custom Sort feature at Salesforce - Part I

In Salesforce we often face cases where we need to customize Sort feature. Sort means sorting of records in related list of any parent record. For example: Opportunity has its child records of Opportunity Line Items. In related list of OLIs at an Opportunity detail page, standard 'Sort' button is present. Clicking on this sort button navigates to a Salesforce standard page where we can sort based on only Product names.


But we can not override Sort button with custom Visualforce page for our customized features. So I took a workaround where I can sort records based on say 3 fields of OLI while click sort button .
First, create 2 custom fields at Opportunity.Sorted_By_field__c and Sorted_By_Asc__c


1- Override detail page of Opportunity with custom to display custom Related list
2- Click on custom button 'Sort OLI' at that related list. Go to your page.
N.B. You may add custom Sort button at standard related list of OLI also which will lead to your page for custom sort. But when you sort there and come back then sorted order will not be shown in standard related list.
3- Sort then. click Save Sort there.
4- back to Opportunity detail page
[Also, See bottom most section of this post]


1- Override detail page of Opportunity with custom VF page, where my custom button ' Sort OLI' will be present.






VF page 'CustomRelatedListForOLI':


<!-- VisualForce Page Responsible for Displaying Related Lists of OLI along with Action Buttons for Page -->
<apex:page standardController="Opportunity" extensions="SorterController">   
  <apex:form >   
    <!-- To display the Detail Page Record -->
    <apex:detail relatedList="true" inlineEdit="true" showChatter="true" />
   
    <!-- To Display Page Block -->
    <apex:pageblock id="pageBlock" >
        <apex:pageblockButtons>
            <apex:commandButton value="Sort OLI" action="{!gotoCustomSortPage}"/>
        </apex:pageblockButtons>
        <!-- To Display Page Block Table -->
        <apex:pageBlockTable id="pbtable" value="{!sortedList}" var="s" >
           
            <!-- Displaying the Field Values -->
            <apex:column value="{!s.Quantity}"/>
            <apex:column value="{!s.TotalPrice}"/>
        </apex:pageBlockTable>
    </apex:pageblock>
  </apex:form>
</apex:page>


And the controller of this page where code for button lies is as follow:

public class SorterController{    

    private String sortedBy = null;
    private Boolean sortAscending = null;
    private AP_SortHelper sorter = new AP_SortHelper();
    public List sortedList {get;set;}

    private String oppid;
    
    /*standard controller*/
    public SorterController(ApexPages.StandardController controller) {
     oppid = ApexPages.CurrentPage().getParameters().get('Id');
     Opportunity currOpp = [SELECT Id,Sorted_By_field__c, Sorted_By_Asc__c FROM Opportunity WHERE Id = :oppid ];
     sorter.originalList = [SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId =:oppid];
     if(currOpp.Sorted_By_field__c != null &&  currOpp.Sorted_By_field__c != '')
         sortedList = (List)sorter.getSortedList(currOpp.Sorted_By_field__c, currOpp.Sorted_By_Asc__c, oppid);
    
    }
    /*controller . This class is as controller of page 'DemoSort'*/
    public SorterController() {
        oppid = ApexPages.CurrentPage().getParameters().get('Id');
        sorter.originalList = [SELECT Quantity, TotalPrice FROM OpportunityLineItem WHERE OpportunityId =:oppid];
    }
    /*Method for Sort button at VF 'CustomRelatedListForOLI'*/
    public PageReference gotoCustomSortPage() {
       return new PageReference('/apex/DemoSort?Id='+oppid);
    }
    /*Method for save button at VF page 'DemoSort'*/
    public PageReference Save() {
        PageReference pr = new PageReference('/apex/CustomRelatedListForOLI?id='+oppid+'&sfdc.override=1') ;
        pr.setRedirect(false);
        sorter.saveOpp(oppid);
        return pr;
    }
    public PageReference SortByQuantity() {
        setSortedBy('Quantity');
        sortedList = (List) sorter.getSortedList('Quantity', sortAscending, oppid);
        return null;
    }
    public PageReference SortByTotalPrice() {
        setSortedBy('TotalPrice');
        sortedList = (List) sorter.getSortedList('TotalPrice', sortAscending,oppid);
        return null;
    }
    public List getList() {
        if (sortedList == null) {
            SortByQuantity();
        }
        return sortedList;
    }
    private void setSortedBy(String value) {
        if (sortedBy == value) {
             sortAscending = !sortAscending;
        } else {
            sortAscending = true;
        }
        sortedBy = value;
    }
}


The class called from this class is:

public class AP_SortHelper {     // 
    public Map listPosition = null;    // >
    public Map> sortedFieldValuesPerFieldName = null;     // >>
    public Map>> sObjectIDsPerFieldNames = null;
    public String OpportuniId;
    public Boolean ascIs;
    public String fieldIs;
// Properties
    public List originalList {get; set;}
    
// Constructor
    public AP_SortHelper() {
        originalList = null;
    }
    // Public Method
    public List getSortedList(String fieldName, Boolean ascending, String oppId) {
        if (originalList == null) {
            // Assume that originalList has a not NULL value.
            // If the class who uses this method has not assigned a value it will get an Exception which
            //    needs to be handled by the calling class.            // Force the exception...
            originalList.clear();
        }        // Make field name uppercase
        fieldName = fieldName.toUpperCase();        // Get sorted list
        OpportuniId = oppId;
        return makeSortedList(fieldName, ascending);
    }
    public List getSortedList(List originalList, String fieldName, Boolean ascending, String oppId) {
        this.originalList = originalList;
        sortedFieldValuesPerFieldName = null;
        OpportuniId = oppId;
        return getSortedList(fieldName, ascending,OpportuniId);
    }
    
// Private Methods
    public void InitializeFieldName(String fieldName) {
        String sObjectID;
        Integer position;
        String fieldValue;
        List sObjectIDs = null;
        Set valuesForFieldSet = null;    // Sets automatically omit duplicate values 
        List valuesForFieldList = null;
        Map> sObjectIDsPerFieldValues = null;
        
        // Make sortedFieldValuesPerFieldName
        if (sortedFieldValuesPerFieldName == null) {
            listPosition = new Map();
            sortedFieldValuesPerFieldName = new Map>();
            sObjectIDsPerFieldNames = new Map>>();
        }
        
        // Get (or create) map of sObjectIDsPerFieldValues
        sObjectIDsPerFieldValues = sObjectIDsPerFieldNames.get(fieldName);
        if (sObjectIDsPerFieldValues == null) {
            sObjectIDsPerFieldValues = new Map>();
            sObjectIDsPerFieldNames.put(fieldName, sObjectIDsPerFieldValues);
        }
        if (!sortedFieldValuesPerFieldName.keySet().contains(fieldName)) {
            // Objects need to be initialized
            position = 0;
            valuesForFieldSet = new Set();
            listPosition = new Map();
            
            for (sObject sObj : originalList) {
                sObjectID = sObj.ID;
                fieldValue = getValue(sObj, fieldName);
                
                // Add position to list
                listPosition.put(sObjectID, position++);
                
                // Add the value to the set (sets rather than lists to prevent duplicates)
                valuesForFieldSet.add(fieldValue);
                
                // Get (or create) map of sObjectIDs
                sObjectIDs = sObjectIDsPerFieldValues.get(fieldValue);
                if (sObjectIDs == null) {
                    sObjectIDs = new List();
                    sObjectIDsPerFieldValues.put(fieldValue, sObjectIDs);
                }
                
                // Add ID to sObjectIDs
                sObjectIDs.add(sObjectID);
            }
            
            // Sort set items (Need to convert to list)
            valuesForFieldList = new List();
            valuesForFieldList.addAll(valuesForFieldSet);
            valuesForFieldList.sort();
            
            // Now add it to the map.
            sortedFieldValuesPerFieldName.put(fieldName, valuesForFieldList);
        }
    }
    public List makeSortedList(String fieldName, Boolean ascending) {
        Integer position;
        List sObjectIDs = null;
        List valuesForFieldList = null;        // Initialize objects
        InitializeFieldName(fieldName);        // Get a list of the same type as the "originalList"
        List outputList = originalList.clone();
        outputList.clear();        // Get a list of sorted values
        valuesForFieldList = sortedFieldValuesPerFieldName.get(fieldName);
        
        // for each sorted value
        for (String fieldValue : valuesForFieldList) {
            // Get lisft of IDs
            sObjectIDs = sObjectIDsPerFieldNames.get(fieldName).get(fieldValue);
            
            // for each ID
            for (String ID : sObjectIDs) {
                // Get position in originalList
                position = listPosition.get(ID);                // Add each sObject to the list.
                if ((ascending) || (outputList.size()==0)) {
                    outputList.add(originalList[position]);
                } else {
                    outputList.add(0, originalList[position]);
                }
            }
        }
        System.debug('\n\nI am returning the output = '+fieldName+'and = \n'+ascending+'\n');
       
        fieldIs = fieldName;
        ascIs = ascending;
        

        return outputList;
    }
    public void saveOpp(String OpportuniIdp){
        Opportunity currOpp = [SELECT Id FROM Opportunity WHERE Id =:OpportuniIdp];
        currOpp.Sorted_By_field__c = fieldIs;
        currOpp.Sorted_By_Asc__c = ascIs;
        
        upsert currOpp;
        
    
    }
    public String getValue(sObject sObj, String fieldName) {
        // This returns the sObject desired in case the fieldName refers to a linked object.
        Integer pieceCount;
        String[] fieldNamePieces;
        
        fieldNamePieces = fieldName.split('\\.');
        pieceCount = fieldNamePieces.size();
        for (Integer i = 0; i < (pieceCount-1); i++) {
            sObj = sObj.getSObject(fieldNamePieces[i]);
        }
        return String.valueOf(sObj.get(fieldNamePieces[pieceCount-1]));
    }
 
    
}

2- Notice that Sort button click will navigate to DemoSort, a VF page where customization for sorting based on 2/3 columns present. In below image, you can see table at DemoSort page where OLIs care there and each column has a button which gives facility to sort by that field.Finally clicking on save returns to original Opportunity detail page and related list shows sorted list by our choice which we have made at DemoSort page.
VF page DemoSort:
<apex:page  controller="SorterController"  >

 <apex:form >
    <apex:pageBlock >
    <apex:commandButton action="{!Save}"  value="Save Sort" />
      <apex:pageBlockSection columns="1" ID="AjaxTable">
        <apex:datatable value="{!List}" var="acc" Border="1" cellspacing="1" cellpadding="5">
          <apex:column >
            <apex:facet name="header">
              <apex:commandButton action="{!SortByQuantity}" value="Sort By Quantity" rerender="AjaxTable" />
            </apex:facet>
            <apex:outputText value="{!acc.Quantity}" />
            </apex:column>
          <apex:column >
            <apex:facet name="header">
            <apex:commandButton action="{!SortByTotalPrice}" value="Sort By Price" rerender="AjaxTable" />
            </apex:facet>
            <apex:outputText value="{!acc.TotalPrice}" />
          </apex:column>
        </apex:datatable>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>


Controller of this class is same as above class 'SorterController' .
Drawback of this method:
1- as you override , so inline edit of Opportunity detail page is gone
2- Edit Layout of Opportunity detail page link is gone.
So, another way may be:
1- Add a section from page layout of Opportunity, then add a VF page at that section ,which shows custom Related list of OLI. And there is button 'Sort Oli'.
2- Click on custom button 'Sort OLI' at that related list. Go to your page.
N.B. You may add custom Sort button at standard related list of OLI also which will lead to your page for custom sort. But when you sort there and come back then sorted order will not be shown in standard related list.
3- Sort then. click Save Sort there.
4- back to Opportunity detail page.
Read post: Custom Sort feature at Salesforce - Part II