Thursday, July 27, 2017

"Who knows who" relations in Salesforce Contact and Accounts - part 2: Solution description

"Who Knows Who" - insert, update and delete reverse records

"Who Knows Who" custom object has Master-Detail relation with "reverse" record which inserts after insertion first record. When "Who Knows Who" record inserted, fires trigger which inserts "reverse" record:
 if(trigger.IsInsert){        
      for(Linked_Relationship__c l: trigger.new){
          if(l.Master_Relationship__c == null && l.Firm__c != l.Related_To_Firm__c){
               Linked_Relationship__c lc = new Linked_Relationship__c();
               lc.Firm__c =  l.Related_To_Firm__c;
               lc.Contact__c =    l.Related_To_Contact__c;
               lc.Related_To_Contact__c = l.Contact__c;
               lc.Related_To_Firm__c = l.Firm__c;
               lc.Reverse_Type__c =   l.Type__c;
               lc.Type__c = l.Reverse_Type__c;
               lc.Comments__c = l.Comments__c;
               lc.Master_Relationship__c = l.id;
               lc.Reverse_Contact_Relationship__c = l.Contact_Relationship__c; 
               lc.Contact_Relationship__c = l.Reverse_Contact_Relationship__c;
               toInsert.Add(lc); 
           }            
       }                                                    
   }
After "Who Knows Who" record update "reverse" record also will be updated (no difference - master or detail):
if(trigger.IsUpdate){        
     for(Linked_Relationship__c lr : trigger.new){
         if(lr.Master_Relationship__c == null){
              lrIds.Add(lr.Id);
         }
         else{
              masterIds.Add(lr.Master_Relationship__c);
         }
     }
     List updateList = [SELECT Id, Firm__c, Contact__c, Related_To_Contact__c, Related_To_Firm__c,
                                                  Reverse_Type__c, Type__c, Comments__c, Master_Relationship__c, 
                                                  Reverse_Contact_Relationship__c, Contact_Relationship__c
                                                  FROM Linked_Relationship__c WHERE
                                                  Master_Relationship__c IN: lrIds OR Id IN: masterIds];
    for(Linked_Relationship__c l : updateList ){
       if(l.Master_Relationship__c != null){
            lrMap.put(l.Master_Relationship__c, l);
       }
       else{
            lrMap.put(l.Id, l);  
       }
   }

   for(Linked_Relationship__c lr : trigger.new){
        if(lrMap.containskey(lr.Id)) {
            if (!(lrMap.get(lr.Id).Firm__c==lr.Related_To_Firm__c&&lrMap.get(lr.Id).Related_To_Firm__c==lr.Firm__c&&lrMap.get(lr.Id).Related_To_Contact__c==lr.Contact__c&&lrMap.get(lr.Id).Contact__c==lr.Related_To_Contact__c&&lrMap.get(lr.Id).Type__c==lr.Reverse_Type__c&&lrMap.get(lr.Id).Reverse_Type__c==lr.Type__c&&lrMap.get(lr.Id).Comments__c==lr.Comments__c&&lrMap.get(lr.Id).Reverse_Contact_Relationship__c == lr.Contact_Relationship__c&&lrMap.get(lr.Id).Contact_Relationship__c == lr.Reverse_Contact_Relationship__c )){
                lrMap.get(lr.Id).Firm__c =  lr.Related_To_Firm__c;
                lrMap.get(lr.Id).Contact__c = lr.Related_To_Contact__c;
                lrMap.get(lr.Id).Related_To_Contact__c = lr.Contact__c;
                lrMap.get(lr.Id).Related_To_Firm__c = lr.Firm__c;
                lrMap.get(lr.Id).Reverse_Type__c =   lr.Type__c;
                lrMap.get(lr.Id).Type__c = lr.Reverse_Type__c;
                lrMap.get(lr.Id).Comments__c = lr.Comments__c;
                lrMap.get(lr.Id).Reverse_Contact_Relationship__c = lr.Contact_Relationship__c; 
                lrMap.get(lr.Id).Contact_Relationship__c = lr.Reverse_Contact_Relationship__c;
                toInsert.Add(lrMap.get(lr.Id));
            }   
        }
       if(lrMap.containskey(lr.Master_Relationship__c)){ 
           if(!(lrMap.get(lr.Master_Relationship__c).Firm__c==lr.Related_To_Firm__c&&lrMap.get(lr.Master_Relationship__c).Related_To_Firm__c==lr.Firm__c&&lrMap.get(lr.Master_Relationship__c).Related_To_Contact__c==lr.Contact__c&&lrMap.get(lr.Master_Relationship__c).Contact__c==lr.Related_To_Contact__c&&lrMap.get(lr.Master_Relationship__c).Type__c==lr.Reverse_Type__c&&lrMap.get(lr.Master_Relationship__c).Reverse_Type__c==lr.Type__c&&lrMap.get(lr.Master_Relationship__c).Comments__c==lr.Comments__c&&lrMap.get(lr.Master_Relationship__c).Reverse_Contact_Relationship__c == lr.Contact_Relationship__c&&lrMap.get(lr.Master_Relationship__c).Contact_Relationship__c == lr.Reverse_Contact_Relationship__c )){
                lrMap.get(lr.Master_Relationship__c).Firm__c =  lr.Related_To_Firm__c;
                lrMap.get(lr.Master_Relationship__c).Contact__c = lr.Related_To_Contact__c;
                lrMap.get(lr.Master_Relationship__c).Related_To_Contact__c = lr.Contact__c;
                lrMap.get(lr.Master_Relationship__c).Related_To_Firm__c = lr.Firm__c;
                lrMap.get(lr.Master_Relationship__c).Reverse_Type__c =   lr.Type__c;
                lrMap.get(lr.Master_Relationship__c).Type__c = lr.Reverse_Type__c;
                lrMap.get(lr.Master_Relationship__c).Comments__c = lr.Comments__c;
                lrMap.get(lr.Master_Relationship__c).Reverse_Contact_Relationship__c = lr.Contact_Relationship__c; 
                lrMap.get(lr.Master_Relationship__c).Contact_Relationship__c = lr.Reverse_Contact_Relationship__c;
                toInsert.Add(lrMap.get(lr.Master_Relationship__c));
           }       
       }                             
   }
}
If "Who Knows Who" record deleted, "reverse" record also will be deleted:
for(Linked_Relationship__c lr: trigger.old){                          
              lrMap.put(lr.Firm__c, lr);
        }
List lrList = [SELECT Id, Firm__c, Contact__c, Related_To_Contact__c, Related_To_Firm__c,
                                               Reverse_Type__c, Type__c, Comments__c,  Master_Relationship__c,
                                               Reverse_Contact_Relationship__c, Contact_Relationship__c
                                               FROM Linked_Relationship__c WHERE
                                               Related_To_Firm__c IN: lrMap.keySet() ];
for(Linked_Relationship__c l : lrList )  {
    if(lrMap.get(l.Related_To_Firm__c).Firm__c==l.Related_To_Firm__c&&lrMap.get(l.Related_To_Firm__c).Related_To_Firm__c==l.Firm__c&&lrMap.get(l.Related_To_Firm__c).Related_To_Contact__c==l.Contact__c && lrMap.get(l.Related_To_Firm__c).Contact__c==l.Related_To_Contact__c&&lrMap.get(l.Related_To_Firm__c).Reverse_Type__c==l.Type__c&&lrMap.get(l.Related_To_Firm__c).Type__c==l.Reverse_Type__c&&lrMap.get(l.Related_To_Firm__c).Contact_Relationship__c==l.Reverse_Contact_Relationship__c&&lrMap.get(l.Related_To_Firm__c).Reverse_Contact_Relationship__c==l.Contact_Relationship__c){
          toDelete.Add(l);
    }
}                                                                                                              
delete toDelete;

Trigger on Contact

When Contact record inserted, trigger insert "Who Knows Who" records("User knows Contact" scenario).
for(Contact c: trigger.new){                
     Linked_Relationship__c l = new Linked_Relationship__c();
     l.Firm__c = knowContList[0].AccountId;
     l.Contact__c = knowContList[0].Id;
     l.Related_To_Firm__c = c.AccountId;
     l.Related_To_Contact__c = c.Id;
     toInsert.Add(l);                       
   }                       
}
After insertion of the first record, trigger on "Who Knows Who" inserts second - reverse record.
After update of Contact - change Contact's Account, triggers updates related "Who Knows Who" records.
for(Contact c: trigger.new){
     if(c.AccountId != trigger.oldMap.get(c.Id).AccountId ){
        contIds.Add(c.id);
        Linked_Relationship__c l = new Linked_Relationship__c();
        l.Firm__c = c.AccountId;
        l.Related_To_Firm__c = c.AccountId;
        l.Related_To_Contact__c = c.Id;
        l.Reverse_Contact_Relationship__c = 'Employee';
        l.Type__c = 'Employer';
        toInsert.Add(l); 
    }
}
List l_oldList = [SELECT Id, Firm__c, Type__c, Comments__c, Contact_Relationship__c,
                                                      Related_To_Firm__c, Related_To_Contact__c, Reverse_Type__c, 
                                                      Reverse_Contact_Relationship__c, Contact__c FROM Linked_Relationship__c 
                                                      WHERE Related_To_Contact__c IN: contIds ];                                                                                                 

Map> lrMap = new Map>();
List otherList = new List();

for(Linked_Relationship__c lr : l_oldList ){
    if(lr.Reverse_Contact_Relationship__c != null && lr.Reverse_Contact_Relationship__c.contains('Employee')){
        if(lrMap.containskey(lr.Related_To_Contact__c)){
            lrMap.get(lr.Related_To_Contact__c).Add(lr);
        }
        else{
            lrMap.put(lr.Related_To_Contact__c, new List{lr});
        } 
     }
     else{
        otherList.Add(lr);
     }                       
}
for(Contact c: [SELECT Id, AccountId, Account.Name FROM Contact WHERE Id IN:  trigger.new]){
                 Linked_Relationship__c l_new = new Linked_Relationship__c();
                 if(!lrMap.containskey(c.Id)){
                     l_new.Firm__c = c.AccountId;
                     l_new.Reverse_Type__c = 'Former Employer';
                     l_new.Contact__c = c.Id;
                     l_new.Contact_Relationship__c = 'Former Employee';
                     l_new.Related_To_Firm__c = trigger.oldMap.get(c.Id).AccountId;
                     l_new.Comments__c = string.valueof(date.today()) + ' - ' + c.Account.Name ;
                     toInsert.Add(l_new);
                 }
                 else{
                     for(Linked_Relationship__c l_old : lrMap.get(c.Id)){
                         l_old.Type__c = 'Former Employer';                        
                         l_old.Reverse_Contact_Relationship__c = 'Former Employee';                     
                         l_old.Comments__c = string.valueof(date.today())+ ' - ' + c.Account.Name;                          
                         l_old.Related_To_Firm__c = c.AccountId;
                         l_old.Related_To_Contact__c = c.Id;                                                                                                                                                          
                         toInsert.Add(l_old);                         
                     }
                }
               for(Linked_Relationship__c l_other : otherList ){
                    if(l_other.Contact__c == c.Id){
                        l_other.Firm__c = c.AccountId;
                        l_other.Contact__c = c.Id;
                        toInsert.Add(l_other);
                    }
               }

If Contact deleted, all related "Who Knows Who" records also will be deleted.
for(Contact c: trigger.old){
     contIds.Add(c.Id);
}
List lrList = [SELECT Id, Related_To_Contact__c, Contact__c 
        FROM Linked_Relationship__c
        WHERE Contact__c IN: contIds OR Related_To_Contact__c IN: contIds];

if(lrList.size()>0){        
     delete lrList;
}

"Who knows who" relations for Salesforce Contact and Accounts - part 1: How this works

Sometimes we need to store information about Contact-Contact, Account-Contact, Account-Account relations:
  • User knows Contact and Account
  • Contact is employee of a Company;
  • Account is Employer of a Contact;
  • Contact is former employee of a Company;
  • Account is former Employer of a Contact, etc.
For this purpose we used Custom object "Who Knows Who":

The records can be created manually and automatic. After insert "Who Knows Who" record fires trigger which inserts "reverse" record.
Automatic scenarios
User "knows" Account and created Contact
When new Contact created, new "Who Knows Who" record is inserted:
Contact related lists:
This Contact and Company related to User:
The user who creates this Contact "knows" it and Contact's Company (reverse record).
"Known" Contact("User" field - lookup to User):

Employer - Former Employer
When Contact reassigned to other Account, insert 2 "Who Know Who" records - "Employer" and "Former Employer": Account is Former Employer of Contact:
In left column information about Contact: current Contact's Company(Account field), and date of termination in previous company. Reverse record: In this way, information about Account's former employees stores in "Who Knows Who" related list. The account is the employer of Contact: Current Account stores info about former employer of Contact in "Who Knows Who" related list:
Manually scenarios
If you want to create a relation between some Contacts or Account, you can manually to insert "Who Knows Who" record and select kind of relationship in "Account relationship" and "Contact relationship" picklists.

Monday, January 16, 2017

Pre-poulate the "Name" field when creating a new record and update after saving

Sometimes there is a need to not use Auto-number as a record name. Instead, you want to pre-populate a Name field when creating a new record with a default value and update the Name based on some rules.
Unfortunately, it's impossible to create your own New custom button. The only option to achieve the requirement is to override the New button with custom VF page. Further please find solution description.

#Edit a new button - override with Visualforce page


Visualforce page was created which redirects to a hack URL with parameter "Name=Partner+Plan".

<apex:page standardController="Partner_Plan__c">
  <script>
      window.top.location.href = '/a0w/e?nooverride=1&retURL=%2Fa0w%2Fo&Name=Partner+Plan';  
 </script>
</apex:page>


#To prevent multiple redirect it's necessary to add parameter "nooverride=1".

After clicking on the button "New" you will be redirected to the page with these paramerers and field "Name" will be filled.
Partner Plan Name required update after saving. It should be consists "Partner Plan" + "Partner Name" + "Year".

#This was done using Workflow Rule.
  • Rule Criteria: true;
  • Evaluation Criteria: Evaluate the rule when a record is created, and every time it's edited
  • Immediate Workflow Action: field update;
  • Formula value: "Partner Plan - " + Practice_Group_Lead__r.FirstName + " " + Practice_Group_Lead__r.LastName + " " - " + TEXT(Plan_Year__c)
After saving:

Monday, August 22, 2016

Storage Usage issue - remove legacy and redundant Task records

Salesforce Orgs with long usage history (more than 3 years) and small Data Storage limits (~1GB) could suffer storage limit issues (> 100%).

In most cases Task object brings the issues with Data Storage overuse.

Unfortunately as per SFDC limitation there is no quick way to delete/remove Tasks created 2 years ago and backwards.

However there is a workaround using custom reports/data export and data loader. Please do not hesitate to contact us if you will need help to leverage this approach.

STEP 1: Export Data
Go to the Setup -> Data Management -> Data Export and click Export Now or Schedule Export button.


Tick Task then click Start Export.


As a result you will be able to download .zip file which contains .csv with tasks data.



STEP 2: Do clean up using Data Loader
Open as excel file and sort data by created date, leave id of tasks which needed to delete. Then use Data Loader to delete these tasks

Approach is a little bit time consuming but works.

Wednesday, July 13, 2016

Native fields in Lead mapping for conversion

For Leads conversion process Salesforce provides out-of-box functionality for mapping Lead fields to Account/Contact/Opportunity.

Unfortunately there is a limitations - you can't map native Leads fields to target object's native field. For instance you can't map value from LeadSource ("native" Lead field) to AccountSource ("native" Account field).

Here is one of the possible ways to workaround the limitation. Solution could be modified based on particular business requirements. Please do not hesitate to contact us if you want to leverage this solutions in your Saleforce instance.

STEP 1: Create custom Formula(Text) field for Lead object
Name: LeadSourceCustom
Formula:  TEXT(LeadSource)

STEP 2: Create custom Text(100) field for Account object
Name: AccountSourceCustom

STEP 3: Amend Lead fields mapping to write value from LeadSourceCustom to AccountSourceCustom.

STEP 4: Create simple Trigger on Account object
/*
    Version        : 1.0
    Company        : Websolo inc. 
    Date           : 07/2016
    Description    : 
    Update History : 
*/
trigger AccountSourceTrigger on Account (before update,before insert) {
    for(Account a: Trigger.new){
        if(a.AccountSourceCustom__c != null && a.AccountSourceCustom__c != '' && (a.AccountSource == null || a.AccountSource == '')){
            a.AccountSource = a.AccountSourceCustom__c;
        } 
    }
}

As a result this trigger will update native AccountSource field with value from "native" LeadSource field.

EXTRA NOTE:
We can't use WorkFlow as a workaround because AccountSource field is picklist and when we create workflow action to update this field we can write only specific value from the picklist.

Friday, April 1, 2016

Account Contact Roles on Contact layout

Salesforce has Contact Roles feature. Contact Role(s) could be applied for any Account and appear on Account layout as a related list. These Roles represent the role(s) that Contact(s) from the same or any other Account(s) plays in the Account. Such Roles could be also specified for Case, Contract, or Opportunity. When Contact roles defined, your team has more information to determine who to contact in particular circumstances.

Unfortunately Salesfroce does not provide any out-of-box tool to let User to see in which Account(s) the particular Contact involved. Means on Contact layout User can's see in which Account the Contact plays "Contact Role".

We want to share pretty easy solution to provide your Users such capability. Solution could be modified based on particular business requirements. Please do not hesitate to contact us if you want to leverage this solutions in your Salesforce instance.

STEP 1: Create VF page and APEX controller 

First you need to create custom Visualforce page which will display list of related to Contact Accounts and Contact Roles. Page's controller will consist of  SOQL SELECT related Account for current Contact. In our example we build Contact Roles for Account.

VF page:
ContactAccountsRolesDetails

<!-- 
Version      : 1.0
Company      : WebSolo Inc.
Date         : 03.2016
Description  : VF page "ContactAccountsRolesDetails" 
History      :             
-->
<apex:page standardController="Contact" extensions="ContactAccountsRolesDetailsContrExt" sidebar="false" showHeader="false" cache="false">
<style>
ul {
    margin-left: 0; 
    padding-left: 0;
}

li {
    list-style-type: none;
}    
</style>
    <apex:pageBlock >
        <apex:Messages />
        <apex:pageBlockTable value="{!listAccountContactRole}" var="acr" rendered="{!records}">
            <apex:column HeaderValue="Account Name">
                <!--<apex:outputText value="{!acr.Account.Name}"/>-->
                <a href="../{!acr.AccountId}" target="_parent"><apex:outputText value="{!acr.Account.Name}"/></a>
            </apex:column>
            <apex:column HeaderValue="Role">
                <apex:outputText value="{!acr.Role}"/>
            </apex:column>
            <apex:column HeaderValue="Primary">
                <apex:outputField value="{!acr.IsPrimary}"/>
            </apex:column>
        </apex:pageBlockTable>
        
    </apex:pageBlock>
</apex:page>


Controller:
ContactAccountsRolesDetailsContrExt


/*
    Version        : 1.0
    Company        : Websolo inc. 
    Date           : 03/2016
    Description    : 
    Update History : 
*/
public class ContactAccountsRolesDetailsContrExt
{ 
    public Id idContact;
    public Boolean records {get; set;}
    public List listAccountContactRole {get; set;}
    
    public ContactAccountsRolesDetailsContrExt(ApexPages.StandardController Controller) 
    {
        records = false;
        idContact = Controller.getRecord().id;
        listAccountContactRole = [SELECT id, AccountId, Account.Name, ContactId, Role, IsPrimary FROM AccountContactRole WHERE ContactId =: idContact];
        
        if (listAccountContactRole.size() != 0)
        {
            records = true;
        }
        else
        {
            Apexpages.Message noRecords = new Apexpages.Message(ApexPages.severity.INFO, 'No records to display');
            Apexpages.addMessage(noRecords);
        }
    }
}

STEP 2: Add VF page to layout

When created VF page could be added to Contact Layout as in example below.






Friday, March 4, 2016

Disable and animate APEX command button after click to avoid double submission


There is wide known scenario when click to APEX command button on custom VF page would take quite long time to perform calculation on server side and return the result back to UI. User may try to click the button again which will result in unexpected and undesired consequences. Especially if click to the button invokes DML operations.

There are a few workarounds to avoid such scenarios. In this post you will the most popular workarounds with analysis about their PROS and CONS.

Solution could be modified based on particular business requirements. Please do not hesitate to contact us if you want to leverage any of this solutions in your Salesforce instance.

Notes related to all examples below:

  • As a $Resource.statusbar you can use any .gif animated "in progress" image
  • In all demos we used external controller to build mathematic curves to simulate server activity

#1 Disabling of APEX command button using jQuery. Replacing button with animated GIF

Disabling the button once the user clicks the button could be the straight forward solution. In this solution we will replace the button once the user clicks the button using jQuery

VF page:
DisableApexCommandBut
ton to demo disable Button
<!-- 
Version      : 1.0
Company      : WebSolo Inc.
Date         : 03.2016
Description  : VF page "DisableApexCommandButton" to demo disable Button
History      :             
-->

<apex:page sidebar="false" showheader="false" cache="false" expires="0" controller="DisableApexCommandButtonContr">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
    <apex:composition template="{!$Site.Template}">
    <apex:define name="body">
    <apex:form style="width:550px;" id="f1">
        <apex:outputPanel >
            <apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px" styleClass="b1" value="Generate" status="status" action="{!run}" rerender="f1"/>
            <div style="display: none;   margin-left: 250px;"  class="statusbardiv">
                <apex:image width="70px" url="{!$Resource.statusbar}" />
            </div>
            <apex:selectRadio value="{!chart}">
                <apex:selectOptions value="{!items}"/>
            </apex:selectRadio><p/>
            <apex:actionStatus id="status" onstart="$('.statusbardiv').css('display','block');$('.b1').css('display','none');" onstop="$('.statusbardiv').css('display','none');$('.b1').css('display','block');" />    
            <div>
                <apex:chart height="300" width="500" data="{!data}">
                    <apex:legend position="top"/>
                    <apex:axis title="Y" type="Numeric" position="left" grid="true" fields="valY"/>
                    <apex:axis title="X" type="Numeric" position="bottom" fields="valX" />
                    <apex:lineSeries title="{!chart}" axis="left" xField="valX" yField="valY"/>
                </apex:chart>
            </div>  
        </apex:outputPanel>
    </apex:form>
    </apex:define>
    </apex:composition>
</apex:page>

PROS: Easy to implement. Looks good.
CONS:  Requires jQuery library.

#2 Call custom div as an overhead "modal window"

Another solution would be not try to "disable" buttons, but to call custom modal window which will prevent user from multiple clicks and multiple form submission.

VF page: DisableApexCommandButton
<!--
    Version        : 1.0
    Company        : Websolo inc. 
    Date           : 03/2015
    Description    : Call custom div as an overhead "modal window"
    Update History : 
-->
<apex:page sidebar="false" showheader="false" cache="false" expires="0" controller="DisableApexCommandButtonContr">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
    <style>
    .disabledbutton {
        pointer-events: none;
        opacity: 0.4;
        background-color:#E1E1E1;
    }
    </style>
    <apex:composition template="{!$Site.Template}">
    <apex:define name="body">
    <apex:form style="width:550px;" id="f1">
        <apex:actionStatus id="status" onstart="$('#mydiv').addClass('disabledbutton');$('.statusbardiv').css('display','block');" onstop="$('#mydiv').removeclass('disabledbutton');$('.statusbardiv').css('display','none');" />
        <div style="display: none; position: fixed; z-index: 999; margin-left: 250px; margin-top: 200px"  class="statusbardiv">
            <apex:image width="70px" url="{!$Resource.statusbar2}" />
        </div>
        <div id="mydiv">
            <apex:outputPanel >
                <apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px" styleClass="b1" value="Generate" status="status" action="{!run}" rerender="f1"/>
                <apex:selectRadio value="{!chart}">
                    <apex:selectOptions value="{!items}"/>
                </apex:selectRadio><p/>    
                <div>
                    <apex:chart height="300" width="500" data="{!data}">
                        <apex:legend position="top"/>
                        <apex:axis title="Y" type="Numeric" position="left" grid="true" fields="valY"/>
                        <apex:axis title="X" type="Numeric" position="bottom" fields="valX" />
                        <apex:lineSeries title="{!chart}" axis="left" xField="valX" yField="valY"/>
                    </apex:chart>
                </div>  
            </apex:outputPanel>
        </div>
    </apex:form>
    </apex:define>
    </apex:composition>
</apex:page>

PROS: Works better in some cases
CONS: Not visually perfect for some users who not used to such approach. Also requires jQuery library.

#3 Disabling of APEX command button using apex:facet method

In this solution we will replace the button with "Processing..." text once the user clicks the button

VF page:
DisableApexCommandButton
<!--
    Version        : 1.0
    Company        : Websolo inc. 
    Date           : 03/2015
    Description    : Disabling of APEX command button using apex:facet method
    Update History : 
-->
<apex:page sidebar="false" showheader="false" cache="false" expires="0" controller="DisableApexCommandButtonContr">

    <apex:composition template="{!$Site.Template}">
    <apex:define name="body">
    <apex:form style="width:550px;" id="f1">
        <apex:outputPanel >
            <apex:actionStatus id="status">
            <apex:facet name="stop">
                <apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px;width:90px" action="{!run}" status="status" value="Generate" disabled="false" rerender="f1"/>
            </apex:facet> 
            <apex:facet name="start">
                <apex:commandButton style="margin-top:20px;margin-left:250px;margin-bottom:25px;height:30px;font-size:12px;width:90px" action="{!run}" status="status" value="Processing..." disabled="true"/>
            </apex:facet>
            </apex:actionStatus>
            <apex:selectRadio value="{!chart}">
                <apex:selectOptions value="{!items}"/>
            </apex:selectRadio><p/>
            <div>
                <apex:chart height="300" width="500" data="{!data}">
                    <apex:legend position="top"/>
                    <apex:axis title="Y" type="Numeric" position="left" grid="true" fields="valY"/>
                    <apex:axis title="X" type="Numeric" position="bottom" fields="valX" />
                    <apex:lineSeries title="{!chart}" axis="left" xField="valX" yField="valY"/>
                </apex:chart>
            </div>  
        </apex:outputPanel>
    </apex:form>
    </apex:define>
    </apex:composition>
</apex:page>

PROS: Efficient and user friendly
CONS: None