Visualforce for Lightning Experience by Example

This post provides a code example for a multi-purpose Visualforce page that dynamically switches between Salesforce Classic and Lightning Experience (LEX) views based on the detected user setting.

At some future stage (perhaps Dreamforce ’16 will enlighten us here) the platform will likely provide support for auto-conversion of legacy Visualforce assets to the new Lightning UI. For those that can’t wait for this capability and require the means to build custom UI that blends with both Salesforce Classic and Lightning Experience the following example hopefully provides a useful reference. Please note, this is provided for information purposes only.

In conjunction with the example Visualforce markup, the following points should be considered:

1. For each interaction (page or sub-page) a decision should be taken as to the development approach:

CSS Switching: UI structure is consistent across views (and no page block components), therefore style class switching may work.

Conditional Visualforce: UI structure is partially consistent; perhaps the LEX view introduces a new activity timeline component for example but is otherwise closely aligned to the Salesforce Classic view. A lack of Lightning Component Framework expertise or availability may also lead to this approach.

Lightning Components: The LEX view is developed with the Lightning Component Framework; components are added to the Visualforce page via apex:includeLightning and conditionally rendered.

2. The Salesforce Lightning Design System is a CSS framework, not a JavaScript framework. In order to build UI with the SLDS requires working knowledge of a JavaScript library such as JQuery to add interactivity such as tab-switching, drop-down menu animation and tooltip display.

3. The ideal of a completely dynamic solution where certain users have the LEX view and others the Salesforce Classic view is somewhat spoiled by the constraint that application and tab icons must be shared between the two views. Predictably, icons that work well in the LEX Navigation Menu do not work at all in Salesforce Classic. Equally the square application icon style preferred in LEX looks visually jarring when rendered in the Salesforce Classic header.

4. LEX compatible pages should be responsive, Salesforce Classic pages rarely have this requirement.

5. Key Lightning Ready requirements include correct handling of the Navigation Menu expand/collqpse events, use of the Salesforce font and visual consistency with the SLDS guidelines.

The screenshots below show how the example page renders in both Salesforce Classic and LEX views.

Settings - Salesforce Classic View

Settings - LEX View

Example Visualforce page; note this example takes the “Conditional Visualforce” approach introduced above.


<!-- 
Name: Settings.page (abbreviated)
Copyright © 2016  Force365
======================================================
======================================================
Purpose:
-------
Example Settings management page.

Multiple purpose page shared between LEX and Classic themes.
> Theme3—Salesforce Classic 2010 user interface theme
> Theme4d—Modern “Lightning Experience” Salesforce theme
> Theme4t—Salesforce1 mobile Salesforce theme

Functions :
View and maintain Application Settings, Target Objects and Data Sources.
======================================================
======================================================
History
------- 
redacted.
 -->
<apex:page tabStyle="Settings__tab" 
            controller="SettingsController" 
            readOnly="false" 
            title="{!$Label.UI_Text_MDM} {!$Label.UI_Text_Settings}"
            docType="html-5.0">

    <apex:form >

        <!--  Lightning Experience Theme [Theme4d] + Theme4t—Salesforce1 mobile Salesforce theme -->
        <apex:outputPanel id="lexPanel" rendered="{!OR( $User.UIThemeDisplayed == 'Theme4d', $User.UIThemeDisplayed == 'Theme4t') }">
            
            <apex:includeScript value="{!URLFOR($Resource.PrimaryResources, '/js/jquery-1.11.3.min.js')}" />
            <apex:stylesheet value="{!URLFOR($Resource.SLDS202, 'assets/styles/salesforce-lightning-design-system-vf.css')}" />     
    
            <script type='text/javascript'>
                var $j = jQuery.noConflict();
        
                $j(document).ready(function(){              
                    init();
                });
                
                function init(){            
                    // tab hide/show content.
                    $j('.slds-tabs--scoped__item').off('click');
                    
                    $j('.slds-tabs--scoped__item').on('click', function(){
                        $j(this).addClass('slds-active');
                        $j(this).find('a').attr('aria-selected', true);
                        var contentToShow = $j('#'+$j(this).find('a').attr('aria-controls'));
                        
                        $j(contentToShow).removeClass('slds-hide');
                        $j(contentToShow).addClass('slds-show');                        
                    
                        $j(this).siblings().removeClass('slds-active');
                        $j(this).siblings().find('a').attr('aria-selected', false);
                        $j(contentToShow).siblings('.slds-tabs--scoped__content').removeClass('slds-show');
                        $j(contentToShow).siblings('.slds-tabs--scoped__content').addClass('slds-hide');
                    }); 
                
                    // data table menu drop downs.
                    $j('.slds-dropdown-trigger .slds-button').off('click');
                    
                    $j('.slds-dropdown-trigger .slds-button').on('click', function(){
                    
                        event.stopPropagation();
                        
                        if ($j(this).parent().hasClass('slds-is-open')){
                            $j(this).parent().removeClass('slds-is-open');
                            $j(this).parent().attr('aria-expanded', false); 
                        } else {                        
                            $j('.slds-dropdown-trigger').removeClass('slds-is-open');
                            $j('.slds-dropdown-trigger').attr('aria-expanded', false);                      
                                                
                            $j(this).parent().addClass('slds-is-open');
                            $j(this).parent().attr('aria-expanded', true);
                        }
                    });
                                        
                    $j(document).click( function(){
                        $j('.slds-dropdown-trigger').removeClass('slds-is-open');
                        $j('.slds-dropdown-trigger').attr('aria-expanded', false);                      
                    });
                            
                    // tooltip hide/show on hover over information icons related to form elements.
                    $j( '.slds-form-element__icon' ).hover(
                        function() {
                        
                            var tooltip = $j(this).find('.slds-popover');
                            tooltip.css( { position:'absolute',left:'0px',margin:'-1.5rem',width:'20rem' } );
                            
                            var t = $j(tooltip).outerHeight()-15;                           
                            t*=-1;
                            tooltip.css( { top:t } );
                            
                            tooltip.removeClass('slds-hide');
                            tooltip.addClass('slds-show');                      
                        }, function() {
                            
                            var tooltip = $j(this).find('.slds-popover');
                            tooltip.removeClass('slds-show');
                            tooltip.addClass('slds-hide');
                        }
                    );                      
                }
                
                function showSpinner(){
                    $j('.slds-spinner_container').removeClass('slds-hide');
                    $j('.slds-spinner_container').addClass('slds-show');    

                    var winWidth = $j(document).width();
                    var winHeight = $j(document).height();
                    
                    $j('.slds-spinner_container').css({ 'width': winWidth,'height': winHeight });                   
                }
                
                function hideSpinner(){                 
                    $j('.slds-spinner_container').removeClass('slds-show');
                    $j('.slds-spinner_container').addClass('slds-hide');    
                }   
                            
            </script>
                        
            <style>
                .myapp-icon-container-background-color {
                    background-color: #73BFB3;
                }
                .myapp-column--overflow-visible {
                    overflow: visible !important;
                }
                html body.sfdcBody {
                    padding: 0 !important;
                }               
            </style>
                        
            <!-- custom scope wrapper -->           
            <div class="myapp">
            
                <div class="slds-spinner_container slds-hide">
                    <div class="slds-spinner--brand slds-spinner slds-spinner--medium" aria-hidden="false" role="alert">
                        <div class="slds-spinner__dot-a"></div>
                        <div class="slds-spinner__dot-b"></div>
                    </div>
                </div>      

                <!-- header -->
                <div class="slds-page-header" role="banner">
                    <div class="slds-media slds-media--center">                     
                        <div class="slds-media__figure">
                            <span class="slds-icon_container myapp-icon-container-background-color">
                                <img class="slds-icon" src="{!URLFOR($Resource.myapp__IconResources, 'Settings-icons_white-01.png')}" alt="portrait" />                          
                            </span>
                        </div>
                        <div class="slds-media__body">
                            <p class="slds-text-body--small page-header__info">{!$Label.UI_Text_MDM}</p>
                            <p class="slds-page-header__title slds-truncate slds-align-middle" title="{!$Label.UI_Text_Settings}">{!$Label.UI_Text_Settings}</p>                            
                        </div>
                    </div>
                </div>                  
                <!-- / header -->

                <!-- content -->
                <apex:outputPanel id="missingStandardPermissionsPanelLEX" rendered="{!NOT(hasStandardPermissions)}">
                    <div class="slds-notify slds-notify--alert slds-theme--error" role="alert">
                        <span class="slds-assistive-text">Info</span>
                        <h2>{!$Label.UI_Error_No_Customize_Application_Permissions}</h2>
                    </div>
                </apex:outputPanel>
            
                <apex:outputPanel id="noCustomPermissionPanelLEX" rendered="{!AND(hasStandardPermissions,NOT($Permission.Manage_Settings))}">
                    <div class="slds-notify slds-notify--alert slds-theme--error" role="alert">
                        <span class="slds-assistive-text">Info</span>
                        <h2>{!$Label.UI_Error_No_Manage_Settings_Permission}</h2>
                    </div>
                </apex:outputPanel>
    
                <apex:outputPanel id="msgsRefreshPanelLEX">
                    <span class="slds-assistive-text">Info</span>
                    <apex:Messages id="msgsLEX" styleclass="slds-notify slds-notify--alert slds-theme--error" html-role="alert"/>
                </apex:outputPanel>
            
                <apex:outputPanel id="mainPanelLEX" rendered="{!AND(showMainPanel,$Permission.Manage_Settings,hasStandardPermissions)}">                
                    
                    <div class="slds-tabs--scoped">
                        <ul class="slds-tabs--scoped__nav" role="tablist">
                            <li class="slds-tabs--scoped__item slds-text-heading--label {!IF(selectedTab=='applicationSettings', 'slds-active', '')}" title="{!$Label.UI_Text_Application_Settings}" role="presentation">
                                <a class="slds-tabs--scoped__link" href="javascript:void(0);" role="tab" tabindex="0" aria-selected="true" aria-controls="tab-scoped-1" id="tab-scoped-1__item">{!$Label.UI_Text_Application_Settings}</a>
                            </li>                       
                            <li class="slds-tabs--scoped__item slds-text-heading--label {!IF(selectedTab=='targetObjects', 'slds-active', '')} {!IF(appSettings.IsActive__c, 'slds-show', 'slds-hide')}" title="{!$Label.UI_Text_Target_Objects}" role="presentation">
                                <a class="slds-tabs--scoped__link" href="javascript:void(0);" role="tab" tabindex="-1" aria-selected="false" aria-controls="tab-scoped-2" id="tab-scoped-2__item">{!$Label.UI_Text_Target_Objects}</a>
                            </li>
                            <li class="slds-tabs--scoped__item slds-text-heading--label {!IF(selectedTab=='dataSources', 'slds-active', '')} {!IF(appSettings.IsActive__c, 'slds-show', 'slds-hide')}" title="{!$Label.UI_Text_Data_Sources}" role="presentation">
                                <a class="slds-tabs--scoped__link" href="javascript:void(0);" role="tab" tabindex="-1" aria-selected="false" aria-controls="tab-scoped-3" id="tab-scoped-3__item">{!$Label.UI_Text_Data_Sources}</a>
                            </li>
                            <li class="slds-tabs--scoped__item slds-text-heading--label {!IF(selectedTab=='dynamicHierarchies', 'slds-active', '')} {!IF(appSettings.IsActive__c, 'slds-show', 'slds-hide')}" title="{!$Label.UI_Text_Dynamic_Hierarchies}" role="presentation">
                                <a class="slds-tabs--scoped__link" href="javascript:void(0);" role="tab" tabindex="-1" aria-selected="false" aria-controls="tab-scoped-4" id="tab-scoped-4__item">{!$Label.UI_Text_Dynamic_Hierarchies}</a>
                            </li>
                            <li class="slds-tabs--scoped__item slds-text-heading--label {!IF(selectedTab=='customRollups', 'slds-active', '')} {!IF(appSettings.IsActive__c, 'slds-show', 'slds-hide')}" title="{!$Label.UI_Text_Custom_Rollups}" role="presentation">
                                <a class="slds-tabs--scoped__link" href="javascript:void(0);" role="tab" tabindex="-1" aria-selected="false" aria-controls="tab-scoped-5" id="tab-scoped-5__item">{!$Label.UI_Text_Custom_Rollups}</a>
                            </li>
                        </ul>
                        <div id="tab-scoped-1" class="slds-tabs--scoped__content {!IF(selectedTab=='applicationSettings', 'slds-show', 'slds-hide')}" role="tabpanel" aria-labelledby="tab-scoped-1__item">
                            
                            <apex:outputPanel layout="block" id="applicationSettingsRefreshPanelLEX">
                            
                                <h3 class="slds-section-title--divider">{!$Label.UI_Text_Application_Settings}</h3>
                                <div class="slds-m-vertical--small"></div>
                                <fieldset class="slds-form--compound">
                                    <div class="form-element__group">
                                        <div class="slds-form-element__row">
                                            <div class="slds-form-element slds-size--1-of-2">
                                            
                                                <label class="slds-checkbox">
                                                    <apex:inputCheckbox value="{!appSettings.IsActive__c}" />
                                                    <span class="slds-checkbox--faux"></span>
                                                    <span class="slds-form-element__label slds-truncate">{!$ObjectType.ApplicationSettings__c.fields.IsActive__c.Label}</span>
                                                </label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-IsActive" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.IsActive__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>
                                                </div>                                      
                                            </div>
                                            <div class="slds-form-element slds-size--1-of-2">
        
                                                <label class="slds-checkbox">
                                                    <apex:inputCheckbox value="{!appSettings.TriggersActive__c}" />
                                                    <span class="slds-checkbox--faux"></span>
                                                    <span class="slds-form-element__label slds-truncate">{!$ObjectType.ApplicationSettings__c.fields.TriggersActive__c.Label}</span>
                                                </label>    
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-TriggersActive" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.TriggersActive__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>                              
                                                </div>                                      
                                            </div>
                                        </div>
                                        <div class="slds-form-element__row">
                                            <div class="slds-form-element slds-size--1-of-2">
        
                                                <label class="slds-checkbox">
                                                    <apex:inputCheckbox value="{!appSettings.IsHierarchiesActive__c}" />
                                                    <span class="slds-checkbox--faux"></span>
                                                    <span class="slds-form-element__label slds-truncate">{!$ObjectType.ApplicationSettings__c.fields.IsHierarchiesActive__c.Label}</span>
                                                </label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-IsHierarchiesActive" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.IsHierarchiesActive__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>
                                                </div>                                          
                                            </div>
                                            <div class="slds-form-element slds-size--1-of-2">
        
                                                <label class="slds-checkbox">
                                                    <apex:inputCheckbox value="{!appSettings.PersistNoMatches__c}" />
                                                    <span class="slds-checkbox--faux"></span>
                                                    <span class="slds-form-element__label slds-truncate">{!$ObjectType.ApplicationSettings__c.fields.PersistNoMatches__c.Label}</span>
                                                </label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-PersistNoMatches" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.PersistNoMatches__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>
                                                </div>                                          
                                            </div>
                                        </div>
                                        <div class="slds-form-element__row">
                                            <div class="slds-form-element slds-size--1-of-2">
        
                                                <label class="slds-checkbox">
                                                    <apex:inputCheckbox value="{!appSettings.ShowHelpCaptions__c}" />
                                                    <span class="slds-checkbox--faux"></span>
                                                    <span class="slds-form-element__label slds-truncate">{!$ObjectType.ApplicationSettings__c.fields.ShowHelpCaptions__c.Label}</span>
                                                </label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-ShowHelpCaptions" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.ShowHelpCaptions__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>
                                                </div>                                                                
                                            </div>
                                            <div class="slds-form-element slds-size--1-of-2">
        
                                                <label class="slds-checkbox">
                                                    <apex:inputCheckbox value="{!sysSettings.DisplayOnSettingsPage__c}" />
                                                    <span class="slds-checkbox--faux"></span>
                                                    <span class="slds-form-element__label slds-truncate">{!$ObjectType.SystemSettings__c.fields.DisplayOnSettingsPage__c.Label}</span>
                                                </label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-DisplayOnSettingsPage" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.SystemSettings__c.fields.DisplayOnSettingsPage__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>
                                                </div>                                                                
                                            </div>
                                        </div>
                                        <div class="slds-form-element__row">
                                            <div class="slds-form-element slds-size--1-of-2">
                                  
                                                <label class="slds-form-element__label slds-truncate" for="select-AuditLogLevel">{!$ObjectType.ApplicationSettings__c.fields.AuditLogLevel__c.Label}</label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-AuditLogLevel" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.AuditLogLevel__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>                              
                                                </div>                                                                              
                                                <div class="slds-form-element__control slds-size--1-of-1 slds-small-size--1-of-1 slds-medium-size--2-of-3 slds-large-size--1-of-2">
                                                    <div class="slds-select_container">
                                                        <apex:selectList id="select-AuditLogLevel"
                                                                            styleClass="slds-select" 
                                                                            value="{!selectedAuditLogLevel}" 
                                                                            multiselect="false" 
                                                                            size="1"                                                                    
                                                                            title="{!$ObjectType.ApplicationSettings__c.fields.AuditLogLevel__c.inlineHelpText}">
                                                            <apex:selectOptions value="{!auditLogLevelOptions}"/>
                                                        </apex:selectList>                                          
                                                    </div>
                                                </div>                                                
                                            </div>
                                            <div class="slds-form-element slds-size--1-of-2"></div>
                                        </div>
                                        <div class="slds-form-element__row">
                                            <div class="slds-form-element slds-size--1-of-2">
                                            
                                                <label class="slds-form-element__label slds-truncate" for="text-MaxJobsDisplayDays">{!$ObjectType.ApplicationSettings__c.fields.MaxJobsDisplayDays__c.Label}</label>
                                                <div class="slds-form-element__icon">
                                                    <a href="javascript:void(0);">
                                                        <svg aria-hidden="true" class="slds-icon slds-icon--x-small slds-icon-text-default">
                                                            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#info')}" />
                                                        </svg>
                                                        <span class="slds-assistive-text">Help</span>
                                                    </a>
                                                    <div id="help-MaxJobsDisplayDays" class="slds-popover slds-popover--tooltip slds-nubbin--bottom-left slds-hide" role="tooltip" aria-live="polite">
                                                        <div class="slds-popover__body slds-text-longform">
                                                            <p>{!$ObjectType.ApplicationSettings__c.fields.MaxJobsDisplayDays__c.inlineHelpText}</p>
                                                        </div>
                                                    </div>                                          
                                                </div>                                      
                                                <div class="slds-form-element__control slds-size--1-of-1 slds-small-size--1-of-1 slds-medium-size--2-of-3 slds-large-size--1-of-2">
                                                    <apex:inputText id="text-MaxJobsDisplayDays" styleClass="slds-input" value="{!appSettings.MaxJobsDisplayDays__c}" html-type="number" />
                                                </div>
                                            </div>
                                            <div class="slds-form-element slds-size--1-of-2"></div>
                                        </div>
                                    </div>
                                </fieldset>
                                
                                <div class="slds-m-vertical--small"></div>
                            </apex:outputPanel> <!--  application settings refresh panel -->
                            
                            <apex:commandButton id="saveButtonLEX" 
                                                action="{!saveApplicationSettingsAction}" 
                                                value="{!$Label.UI_Button_Label_Save}" 
                                                rerender="mainPanelLEX,msgsRefreshPanelLEX" 
                                                onclick="showSpinner();"
                                                oncomplete="init();hideSpinner();"
                                                styleclass="slds-button slds-button--neutral" />
                                                
                            <div class="slds-m-vertical--large"></div>                                                                                                                  
                        </div>
                        <div id="tab-scoped-2" class="slds-tabs--scoped__content {!IF(selectedTab=='targetObjects', 'slds-show', 'slds-hide')}" role="tabpanel" aria-labelledby="tab-scoped-2__item">                   
                            
                            <apex:outputPanel id="targetObjectsRefreshPanelLEX">
                                <apex:outputPanel rendered="{!$Setup.ApplicationSettings__c.ShowHelpCaptions__c}">
                                    <div class="slds-notify slds-notify--alert slds-theme--alert-texture" role="alert">
                                        <span class="slds-assistive-text">Info</span>
                                        <h2>{!$Label.UI_Help_Caption_Target_Object_Settings}</h2>
                                    </div>
                                </apex:outputPanel>
                            
                                <apex:dataTable value="{!targetObjects}" var="to" headerClass="slds-text-heading--label slds-truncate" 
                                                styleClass="slds-table slds-table--bordered slds-max-medium-table--stacked-horizontal slds-no-row-hover slds-table--striped" 
                                                columnClasses="slds-cell-wrap slds-truncate" >
                                    <apex:column value="{!to.targetObjectLabel}" headerValue="{!$Label.UI_Column_Header_Setting_Name}" html-data-label="{!$Label.UI_Column_Header_Setting_Name}"/>  
                                    
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Normalisation}" html-data-label="{!$Label.UI_Column_Header_Active_For_Normalisation}">
                                        <apex:inputCheckbox value="{!to.isActiveForNormalisation}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Matching}" html-data-label="{!$Label.UI_Column_Header_Active_For_Matching}">
                                        <apex:inputCheckbox value="{!to.isActiveForMatching}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Merge}" html-data-label="{!$Label.UI_Column_Header_Active_For_Merge}">
                                        <apex:inputCheckbox value="{!to.isActiveForMerge}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Conversion}" html-data-label="{!$Label.UI_Column_Header_Active_For_Conversion}">
                                        <apex:inputCheckbox value="{!to.isActiveForConversion}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Reparenting}" html-data-label="{!$Label.UI_Column_Header_Active_For_Reparenting}">
                                        <apex:inputCheckbox value="{!to.isActiveForReparenting}" disabled="true" />
                                    </apex:column>
                                    <apex:column html-data-label="{!$Label.UI_Text_Action}" styleClass="myapp-column--overflow-visible">
                                    
                                        <div class="slds-dropdown-trigger slds-dropdown-trigger--click" aria-expanded="false">
                                            <button type="button" class="slds-button slds-button--icon-border-filled" aria-haspopup="true">
                                                <svg aria-hidden="true" class="slds-button__icon">
                                                    <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Resource.myapp__SLDS202, '/assets/icons/utility-sprite/svg/symbols.svg#down')}" />
                                                </svg>
                                                <span class="slds-assistive-text">{!$Label.UI_Text_Actions}</span>
                                            </button>
                                            <div class="slds-dropdown slds-dropdown--right">
                                                <ul class="slds-dropdown__list" role="menu">
                                                
                                                    <apex:outputPanel rendered="{!hasTargetObjectPageAccess}">
                                                        <li class="slds-dropdown__item">                                                                                    
                                                            <apex:commandLink style="color:#0070d2;" action="{!editTargetObjectAction}" value="{!$Label.UI_Text_Edit}" id="targetObjectEditCommandLinkLEX" styleclass="slds-truncate" html-role="menuitem">
                                                                <apex:param name="s" value="{!to.targetObjectName}"/>
                                                            </apex:commandLink>
                                                        </li>
                                                    </apex:outputPanel>                                             
                                                
                                                    <li class="slds-dropdown__item">
                                                        <apex:commandLink style="color:#0070d2;" 
                                                                        action="{!deleteTargetObjectAction}" 
                                                                        rerender="targetObjectsRefreshPanelLEX,msgsRefreshPanelLEX" 
                                                                        value="{!$Label.UI_Text_Del}" 
                                                                        id="targetObjectDeleteCommandLinkLEX" 
                                                                        onclick="if(!confirm('{!$Label.UI_Text_Confirmation_Proceed_To_Delete}')){return};"
                                                                        oncomplete="init();" >
                                                            <apex:param name="s" value="{!to.targetObjectName}"/>
                                                        </apex:commandLink>                                       
                                                    </li>
                                                        
                                                </ul>
                                            </div>
                                        </div>                                  
                                    </apex:column>
                                                      
                                </apex:dataTable>
    
                                <!-- if no target objects show columns headers and line "No results" beneath -->    
                                <apex:outputPanel rendered="{!IF(NOT(hasTargetObjects),true,false)}">
                                    <div class="slds-text-heading--label-normal slds-text-align--center slds-m-vertical--xx-large">{!$Label.UI_Text_No_Items_To_Display}</div>
                                </apex:outputPanel> 
                                
                                <div class="slds-m-vertical--small"></div>
                                <apex:actionFunction name="addTargetObject" action="{!addTargetObjectAction}" />
                                <button class="slds-button slds-button--neutral" onclick="addTargetObject()">{!$Label.UI_Button_Label_Add}</button>
                                <div class="slds-m-vertical--large"></div>
                                
                            </apex:outputPanel> <!-- targetObjectsRefreshPanelLEX -->               
                        </div>
                        <div id="tab-scoped-3" class="slds-tabs--scoped__content {!IF(selectedTab=='dataSources', 'slds-show', 'slds-hide')}" role="tabpanel" aria-labelledby="tab-scoped-3__item">
                        </div>
                        <div id="tab-scoped-4" class="slds-tabs--scoped__content {!IF(selectedTab=='dynamicHierarchies', 'slds-show', 'slds-hide')}" role="tabpanel" aria-labelledby="tab-scoped-4__item">
                        </div>
                        <div id="tab-scoped-5" class="slds-tabs--scoped__content {!IF(selectedTab=='customRollups', 'slds-show', 'slds-hide')}" role="tabpanel" aria-labelledby="tab-scoped-5__item">
                        </div>                        
                    </div>  
                    <div class="slds-m-vertical--large"></div>
                    
                </apex:outputPanel>                     
                <!-- / content -->
        
                <!-- footer -->
                <!-- / footer -->
                
            </div> <!-- custom scope wrapper -->            
        
        
        </apex:outputPanel> <!-- Theme4d/Theme4t -->

        <!--  Salesforce Classic Theme [Theme3] -->
        <apex:outputPanel rendered="{!AND( $User.UIThemeDisplayed <> 'Theme4d', $User.UIThemeDisplayed <> 'Theme4t') }">
        
            <style>
              .activeTab { background-color: #F1F1F1; color:black; background-image:none; height:22px; font-family:Arial,​Helvetica,​sans-serif; font-size:12px }
              .inactiveTab { background-color: #C0C0C0; color:black; background-image:none; height:22px; font-family:Arial,​Helvetica,​sans-serif; font-size:12px }
            </style>        
            
            <apex:sectionHeader subtitle="{!$Label.UI_Text_Settings}" title="{!$Label.UI_Text_MDM}" />
    
            <apex:outputPanel id="missingStandardPermissionsPanel" rendered="{!NOT(hasStandardPermissions)}">
                <apex:pageMessage summary="{!$Label.UI_Error_No_Customize_Application_Permissions}" severity="warning" strength="1" />    
            </apex:outputPanel>
            
            <apex:outputPanel id="noCustomPermissionPanel" rendered="{!AND(hasStandardPermissions,NOT($Permission.Manage_Settings))}">
                <apex:pageMessage summary="{!$Label.UI_Error_No_Manage_Settings_Permission}" severity="warning" strength="1" />    
            </apex:outputPanel>
    
            <apex:outputPanel id="msgsPanel">
                <apex:pageMessages id="msgs" />
            </apex:outputPanel>
            
            <apex:outputPanel id="mainPanel" rendered="{!AND(showMainPanel,$Permission.Manage_Settings,hasStandardPermissions)}">       
                <apex:tabPanel switchType="client" value="{!selectedTab}" id="mainTabPanel" tabClass="activeTab" inactiveTabClass="inactiveTab">
                    <apex:tab label="{!$Label.UI_Text_Application_Settings}" name="applicationSettings" id="applicationSettingsTab">
                    
                    
                        <div style="position: relative; min-height:400px; height:auto !important; height:400px;">               
                            <apex:actionstatus id="pageActionStatus">
                                <apex:facet name="start">
                                    <div class="waitingSearchDiv" id="loaderDiv" style="background-color: #fbfbfb; height: 100%;opacity:0.65;width:100%;"> 
                                    <div class="waitingHolder" style="top: 74.2px; width: 91px;">
                                        <img class="waitingImage" src="/img/loading.gif" title="Please Wait..." />
                                            <span class="waitingDescription">{!$Label.UI_Text_Saving}</span>
                                        </div>
                                    </div>
                                </apex:facet>                       
                                <apex:facet name="stop"></apex:facet>                                               
                            </apex:actionstatus>                  
                    
                            <apex:pageBlock id="applicationSettingsPageBlock" title="{!$Label.UI_Text_Application_Settings}" mode="edit" >              
                                
                                <apex:pageBlockButtons >
                                    <apex:commandButton id="saveButton" 
                                                        action="{!saveApplicationSettingsAction}" 
                                                        value="{!$Label.UI_Button_Label_Save}" 
                                                        rerender="mainPanel" 
                                                        status="pageActionStatus"/>
                                </apex:pageBlockButtons>
                            
                                <apex:pageMessage summary="{!$Label.UI_Help_Caption_Application_Settings}" severity="info" strength="1" rendered="{!$Setup.ApplicationSettings__c.ShowHelpCaptions__c}" />                  
                                
                                <apex:pageBlockSection showHeader="true" columns="2" collapsible="false" title="{!$Label.UI_Text_Application_Settings}">
                                    <apex:inputField value="{!appSettings.IsActive__c}" />
                                    <apex:inputField value="{!appSettings.TriggersActive__c}" />
                                    <apex:inputField value="{!appSettings.IsHierarchiesActive__c}" />
                                    <apex:inputField value="{!appSettings.PersistNoMatches__c}" />
                                    <apex:inputField value="{!appSettings.ShowHelpCaptions__c}" />  
                                    <apex:pageBlockSectionItem /> 
                                    
                                    <apex:pageBlockSectionItem >
                                        <apex:outputLabel value="{!$Label.UI_Text_Audit_Log_Level}" for="auditLogLevelSelectList" />
                                        <apex:selectList id="auditLogLevelSelectList" 
                                                        value="{!selectedAuditLogLevel}" 
                                                        multiselect="false" 
                                                        size="1" 
                                                        style="width:100px"
                                                        title="{!$ObjectType.ApplicationSettings__c.fields.AuditLogLevel__c.inlineHelpText}">
                                            <apex:selectOptions value="{!auditLogLevelOptions}"/>
                                        </apex:selectList>
                                    </apex:pageBlockSectionItem>
                                    <apex:pageBlockSectionItem />                                     
                                    
                                    <apex:inputField value="{!appSettings.MaxJobsDisplayDays__c}" />
                                    <apex:inputField value="{!appSettings.MaxSearchResultsPerDataSource__c}" />                                                     
                                    <apex:inputField value="{!appSettings.SearchTermMinimumCharacterCount__c}" />                           
                                    <apex:inputField value="{!appSettings.DefaultFuzzyMatchThresholdPercentage__c}" />
                                    <apex:pageBlockSectionItem />
                                    <apex:pageBlockSectionItem />
                                
                                    <apex:inputField value="{!appSettings.MaxRecordsPerDataExport__c}" />                               
                                    <apex:pageBlockSectionItem />                                                               
                                    <apex:inputField value="{!appSettings.MaxChildRecordSearchResults__c}" />                               
                                    <apex:pageBlockSectionItem />
                                                                                                
                                    <apex:outputField value="{!appSettings.NamespaceApexPrefix__c}" />
                                    <apex:outputField value="{!appSettings.NamespaceComponentPrefix__c}" />
                                    <apex:outputField value="{!appSettings.NamespacePrefix__c}" />
                                </apex:pageBlockSection>
                                                                
                            </apex:pageBlock>
                        </div>
                    </apex:tab>
    
                    <apex:tab label="{!$Label.UI_Text_Target_Objects}" name="targetObjects" id="targetObjectsTab" rendered="{!$Setup.ApplicationSettings__c.IsActive__c}">
                        <apex:outputPanel id="targetObjectsRefreshPanel">
                            <apex:pageBlock id="targetObjectsPageBlock" title="{!$Label.UI_Text_Target_Object_Settings}" mode="edit" >                    
                                <apex:pageBlockButtons >
                                    <apex:commandButton id="addTargetObjectButton" action="{!addTargetObjectAction}" value="{!$Label.UI_Button_Label_Add}" rendered="{!hasTargetObjectPageAccess}" />
                                </apex:pageBlockButtons>
                                            
                                <apex:pageMessage summary="{!$Label.UI_Help_Caption_Target_Object_Settings}" severity="info" strength="1" rendered="{!$Setup.ApplicationSettings__c.ShowHelpCaptions__c}" />
                            
                                <apex:pageBlockTable value="{!targetObjects}" var="to" columnsWidth="15px,20%,15%,15%,15%,15%,15%" >
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Action}">
                                        <apex:commandLink action="{!editTargetObjectAction}" value="{!$Label.UI_Text_Edit}" id="targetObjectEditCommandLink" style="color:#015BA7;" rendered="{!hasTargetObjectPageAccess}">
                                            <apex:param name="s" value="{!to.targetObjectName}"/>
                                        </apex:commandLink>
                                        <apex:commandLink action="{!deleteTargetObjectAction}" rerender="targetObjectsRefreshPanel,msgsPanel" value="{!$Label.UI_Text_Del}" id="targetObjectDeleteCommandLink" style="padding-left: 10px; color:#015BA7;" onclick="if(!confirm('{!$Label.UI_Text_Confirmation_Proceed_To_Delete}')){return};" >
                                            <apex:param name="s" value="{!to.targetObjectName}"/>
                                        </apex:commandLink>                                
                                    </apex:column>                  
                                    <apex:column value="{!to.targetObjectLabel}" headerValue="{!$Label.UI_Column_Header_Setting_Name}" />    
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Normalisation}">
                                        <apex:inputCheckbox value="{!to.isActiveForNormalisation}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Matching}">
                                        <apex:inputCheckbox value="{!to.isActiveForMatching}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Merge}">
                                        <apex:inputCheckbox value="{!to.isActiveForMerge}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Conversion}">
                                        <apex:inputCheckbox value="{!to.isActiveForConversion}" disabled="true" />
                                    </apex:column>
                                    <apex:column headerValue="{!$Label.UI_Column_Header_Active_For_Reparenting}">
                                        <apex:inputCheckbox value="{!to.isActiveForReparenting}" disabled="true" />
                                    </apex:column>                                                                                    
                                                                                                                                                                    
                                </apex:pageBlockTable>
        
                                <!-- if no target objects show columns headers and line "No results" beneath -->    
                                <apex:outputPanel rendered="{!IF(NOT(hasTargetObjects),true,false)}">
                                    <div style="padding: 5px; color: #808080; font: 11px Arial,Helvetica,sans-serif;">{!$Label.UI_Text_No_Search_Results}</div>
                                </apex:outputPanel> 
                                            
                            </apex:pageBlock>
                        </apex:outputPanel>
                    </apex:tab>
                    
                    <apex:tab label="{!$Label.UI_Text_Data_Sources}" name="dataSources" id="datasourcesTab" rendered="{!$Setup.ApplicationSettings__c.IsActive__c}">                           
                    </apex:tab>                
                    
                    <apex:tab label="{!$Label.UI_Text_Dynamic_Hierarchies}" name="dynamicHierarchies" id="dynamicHierachiesTab" rendered="{!AND($Setup.ApplicationSettings__c.IsActive__c, $Setup.ApplicationSettings__c.IsHierarchiesActive__c)}">
                    </apex:tab>  
                    
                    <apex:tab label="{!$Label.UI_Text_Custom_Rollups}" name="customRollups" id="customRollupsTab" rendered="{!AND($Setup.ApplicationSettings__c.IsActive__c)}">                        
                    </apex:tab>                                
                </apex:tabPanel>        
            </apex:outputPanel> <!-- mainPanel -->      
    
        </apex:outputPanel> <!-- NOT Theme4d/Theme4t = Salesforce Classic -->        
    
    </apex:form>
    
</apex:page>

Salesforce1 Lightning

Once again the annual Dreamforce event has been and gone leaving most practitioners with an uncomfortable knowledge deficit in terms of the real detail of the wealth of new innovations announced. This autumn period, post-Dreamforce, is often a time for investigation; piecing together information gathered from press releases, blog post, social media and event session replays.

This post provides the output of my own brief investigations into the Salesforce1 Lightning announcement. Note, I may be vague or incorrect in some areas, I have made assumptions (safe I hope).

Salesforce1 Lightning – What is it?
Salesforce1 Lightning is the next generation of the Salesforce1 Platform, it is framed specifically as a platform-as-a-service (PaaS) play and heralded as the world’s number 1. The platform is comprised of a number of new and re-branded technologies that collectively target the rapid delivery of cross-device, responsive applications via clicks-not-code. Responsive in this context relating to dynamic components that adapt their layout to the viewing environment, to provide an optimised viewing experience, regardless of the dimensions (i.e. a single view layer that supports desktop computers, smartphones, tablets and wearables).

Salesforce1 Lightning – Key Features
– Lightning Framework (GA now)
Saleforce developed UI framework for rapid web application development based on the single-page model, loosely coupled components and event-driven programming. The underlying Aura Framework was open-sourced last year and is used internally by Salesforce for the development of Salesforce1 plus recently introduced platform functionality (Opportunity Splits UI etc.). The Lightning Framework represents the availability of a subset of the Aura Framework functionality as a platform service for custom application development.

– Lightning Components (beta now, GA Spring 2015)
In order to build with the Salesforce1 Lightning Framework we need components. Components represent cleanly encapsulated units of functionality from which user interactions are comprised (i.e. apps). Component communication is supported by an event-based-architecture.

Standard Components – e.g. Button, Report Chart, Chatter Feed. Standard Components enable custom applications to be composed that inherit the Lightning UI (meaning the Salesforce1 UI).

Custom Components. Using the Lightning Component Framework, custom components can be developed to deliver any required experience (style, branding, function etc.). Developers build components with predominantly client-side behaviours, Apex can be employed to provide server-side controller functionality.

AppExchange Component. 3rd party commercial components installed from the AppExchange.

– Lightning App Builder (beta Spring 2015)
As the name implies the App Builder enables application composition through the drag-and-drop of components – meaning no-code. The builder provides Desktop, Laptop, Tablet, Phone and Smart Watch views onto which optimal component layouts are configured – the responsive behaviour to the components themselves therefore applies within the device type view – this makes good sense as an optimal laptop experience is not the phone view with proportionally bigger components. This approach is commonplace, wix.com for example works this way – i.e. build a generic device type specific view with components that adjust to the device specific viewing environment.

– Lightning Process Builder (GA Spring 2015)
Re-branded Force.com Flow / Visual Workflow functionality. In this context the point being that the business process logic is configurable through a drag-and-drop visual tool. Edit: my assumption here in relation to re-branding may well be incorrect, it has been suggested that the tool is in fact a distinct technology from Force.com Flow and may coexist. I’ll update this when I understand more – the functional overlap between Force.com Flow and Process Builder looks to be significant.

– Lightning Schema Builder (GA now)
Re-branded Schema Builder functionality. In this context the point being that the data schema is open to visual (i.e. drag-and-drop) manipulation.

– Lightning Connect (GA now)
Re-branded Platform Connect, or External Objects. The ability to configure virtual objects that query external data sources in real-time via the OData protocol. Connect underpins the concept of real-time external data access via the Salesforce1 platform and its constituent mobile applications.

– Lightning Community Builder (beta now, GA Spring 2015)
As the name implies a drag-and-drop tool for the configuration of Salesforce Communities plus the content delivered.

Entry Points
Salesforce1 Lightning Components can be accessed in the following ways.

Standalone App – access via /NAMESPACE/APPNAME.app
Salesforce1 Mobile App Tabs – create a Tab linked to a Lightning Component and add to the Mobile Navigation Menu.
Salesforce1 Mobile App Extensions – currently in Pilot, a means by which custom components can override standard components.

Future – All Visualforce entry points should ultimately become options for Lightning Components. This one is definitely an assumption. Standard components with which exhibit the standard Salesforce (Aloha) styling would be required for this.

Considerations
The following list represents some initial thoughts regarding the significance of Salesforce1 Lightning – formed without the insight gained through practical experience.

– Mobile-first.
The design principle is simple, the most effective way to build compelling user experiences across multiple devices is to start with the simplest (or perhaps smallest) case, i.e. the mobile. Note, the introduction of wearables possibly invalidates this slightly. The alternate approach of trying to shoe-horn complex desktop experiences onto smaller viewing environments rarely produces anything worthwhile.

– Rapid Development.
Quickly, faster, speed-of-business plus various other speed-related adverbs litter the press releases for Salesforce1 Lightning. If the name itself isn’t sufficient to highlight this, the platform is all about rapid development. In context rapid is realised by configuration-over-code; assembling apps with pre-fabricated, road-tested components delivers the shortest development cycle. That said, regardless of whatever form development takes a development lifecycle is absolutely necessary – the vision of analysts configuring apps over lunch and releasing immediately to business users is prone to disaster; I don’t believe this to be the message of Salesforce1 Lightning however.

– Technical Barriers versus Organisational Friction.
Removing technical barriers to rapid application development is only one part of the equation, organisations need to be culturally ready to embrace and therefore benefit from such agility. Building an enterprise app in a week makes little sense if it takes 3 months for acceptance, approval and adoption processes to run. The concept of turning IT departments into centres of innovation is an incredibly powerful aspiration, however this relies heavily on empowerment, trust and many other agile principles some organisations struggle with.

– Development model.
The Lightning development model is fully consistent with the age-old Salesforce philosophy of rapid declarative development via pre-fabricated componentry. Application architects, admins, analysts etc. assemble the apps with components supplied by Salesforce, built in-house or procured from the AppExchange. Developers will therefore focus more on building robust and reusable components than actual applications – which makes good sense assuming appropriate skills are applied to component specification and application design. The model requires non-technical resource to adopt a development mindset, this may be problematic for some.

– Developer Skills.
To build with the Salesforce1 Lightning Component Framework developers must be proficient in JavaScript, beyond intermediate level in my view. Lightning is a comprehensive component-based client-server UI framework with an event driven architecture. Developers with previous exposure limited to basic JavaScript augmentations to Visualforce face a learning curve. Anyone still under the false impression that JavaScript programming is simple compared to languages such as Java, C, C++ etc. may want to reconsider this before embarking on a Lightning project.

– Use Cases.
The viability of the proposed development model may ultimately come down to the complexity of the use cases/user experiences that can be supported without reverting to custom component development. By their very nature mobile interactions should be simplistic, but for desktop interactions it will be interesting to understand the scope of the potential for complex applications.

– Adoption.
Salesforce1 Lightning follows a similar paradigm to both Site.com and Force.com Flow, where historically technically oriented tasks are made possible for non-technical users; drag and drop visual composition of business process realisations and interactive web site development respectively. Both technologies, as innovative and empowering as they are, do not appear to have radically changed the development models to which they pertain. An obvious question therefore is whether the empowering technology alone is enough to drive adoption.

References
Aura Documentation Site
Lightning Components Developer’s Guide
Lightning QuickStart

Visualforce Controller Class Convention

A quick post to outline an informal convention for the definition of a Visualforce controller class, key maintainability characteristics being predictable structure, readability and prominent revision history. All developers have a subjective preference in this regard, however consistency is key, particularly in the Salesforce context where multiple developers/consultancies contribute to a codebase over its lifetime. A simple, logical approach always makes sense to maximise adoption.

/*
Name:  MyPageController.cls
Copyright © 2014  Force365
======================================================
======================================================
Purpose:
-------
Controller class for the VF page - MyPage.page
======================================================
======================================================
History
------- 
Ver. Author        Date        Detail
1.0  Mark Cane&    2014-05-20  Class creation.
1.1  Mark Cane&    2014-05-21  Initial coding for page initialisation.
*/
public with sharing class MyPageController {
	//& public-scoped properties.	
	public List<MyWrapperClass> wrapperClassInstances { get; set; }
	//& End public-scoped properties.
   				
	//& private-scoped variables.
	private Boolean isInitialised=false;
	//& End private-scoped variables.
	
	//& page initialisation code.
	public MyPageController(){
		initialise();
	}
	
	private void initialise(){ isInitialised=true; }
	//& End page initialisation code.
	
	//& page actions.
	public PageReference saveAction(){
		return null;
	}
	//& End page actions.
	
	//& data access helpers (class methods accessed from binding expressions).
	//& End data access helpers.
	
	//& controller code Helpers (class methods providing helper functions to data access helpers or actions).
	//& End controller code helpers.
	
	//& inner classes (wrapper classes typically, extending SObjects for convenience in the UI).
	public class MyWrapperClass {
		public MyWrapperClass(){}
	}
	//& End inner classes.
}

Salesforce1 App Customisation

At the time I started writing this post we were living in a Chatter Mobile world, this is no longer the case. As announced recently at Dreamforce ’13 we have entered the Salesforce1 era, the direction of which will no doubt become clearer as we transition to the Spring ’14 release. What is clear now however is that the unified platform is the focus this time, providing mobile enablement (apps) via a rich set of platform services (APIs x 10). A rich set of robust APIs is always great news for any development community, whether you’re connecting a mobile device or sensor device (IoT). I won’t venture further into what Salesforce1 is in concept or the implications, some of this is yet to be seen. In any case this blog is focused on the practical application of the Salesforce and Force.com technologies, and as such the following summary is a point-in-time outline of the information available at the time of writing.

So, on to the topic in hand, customisation options for the Salesforce1 mobile application.

As with most things in life, to make good design decisions it helps to understand the full set of options available (and to see the big picture). This obvious point is fundamental in architecting Salesforce platform solutions; it’s key to have knowledge of the complete standard feature set and extension points (declarative build model) such that technical solution components are minimised. Simply put, in the mobile context this guiding principle means exhaust the potential of the standard apps (meaning Salesforce1) and the customisation options before building a new custom mobile app from scratch.

The following summary notes attempt to clarify the function of the Salesforce1 app and the various customisation options.

The Salesforce1 App – What is it?
In application development terms the Salesforce1 app provides a flexible mobile container into which a blend of standard and custom functionality can be presented on a variety of connected devices (mobile, tablet etc.). The container itself comes in 2 forms; downloadable native app (iOS and Android) and mobile browser app. In either form Salesforce1 employs a feed-first paradigm, with publisher actions and a notification platform. The mobile browser app must be enabled from the setup menu under Mobile Administration->Salesforce1 and becomes the default when accessing Salesforce from supported devices (as-per Salesforce Touch). Salesforce1 does not replace the standard Salesforce web app, or Full Site as it is referred. Salesforce1 does replace Salesforce Touch, Chatter Mobile and Salesforce mobile apps.

Ok, so assuming we’re comfortable with the idea that Salesforce1 is a application container, how can we extend the functionality of the container to deliver custom application interactions?

Customisation Options
The following summary notes attempt to clarify the customisation options available with the Salesforce1 mobile app. Also included in the list are standard capabilities of the container app, provided for completeness. The Salesforce1 App Development Guide provides a comprehensive coverage of the topics including example code that can be loaded into a development org (via github) to experiment with.

— Mobile Navigation
Within the setup menu under Mobile Administration, the Mobile Navigation option provides the ability to define the content and structure of the left hand side menu of the container app (as shown in the screenshot below).

Salesforce1 Tablet

Note. The first item in the list becomes the home page.

— Custom Objects (declarative)
Self explanatory. Recently searched for objects appear under the Recent section in the mobile menu structure according to profile permissions. It would appear that page layouts are shared between Salesforce1 and the standard Salesforce web app. This may prove difficult in terms of providing a balanced structure that is effective in both views.

— Flexible Page Tabs (declarative and technical)
I’m not 100% clear on the use cases for Flexible pages, however they do exist and can be added to Flexible page Tabs and ultimately rendered in the container app via inclusion in the menu structure (via Mobile Navigation). I need to do some further investigation into the potential of Flexible Pages, perhaps I’m missing something obvious.

A Flexible Page can have global publisher actions, list views and scoped recently used items. A maximum of 25 components can be added to the page, this constraint applies to listviews and recently used items.

In terms of definition a Flexible page is manually created in XML and deployed to an org via the Force.com Migration Tool or IDE. Once a Flexible page exists in an org the Create->Tabs page will present a Flexible Page Tabs list (as per Visualforce tabs etc.).

— Publisher Actions (declarative and technical)
Publisher actions are accessed via the (+) button on the home page, Chatter tab, Chatter group and record detail pages.

Actions can be Standard or Custom and Global or Object scoped.

Standard actions include;
Create a Record – respects Validation Rules etc. A Create action defined for a object will automatically relate created child records.
Log a Call – record a completed Task
Update – update a record from the Chatter feed

Default standard actions are provided for the core standard objects (create and update), such actions have configurable layouts and the ability to predefine field values.

Custom actions include;
Visualforce pages
Canvas apps

Interestingly actions can be described and invoked via the REST API, resource paths below.
/services/data/v29.0/quickActions (Global)
/services/data/v29.0/sobjects/[object]/quickActions (Object)

— Compact Layouts (declarative)
Compact layouts are defined at the object level and are simply an ordered list of fields by definition. Compact layouts apply in the following areas;

Record highlights fields (first 4 fields in the ordered field list). The record highlights are displayed in the top region of a record detail display under the icon.
Defines the fields that appear in the Chatter feed if a record us created via publisher action.
Enhanced lookup mobile card (more on this below)
Object record preview card

— Mobile Cards (declarative)
Page layouts now have a section for Mobile Cards, the following items can be added which then appear as a swipe-to card within the record detail view within the Salesforce1 app only.

Visualforce Page
Expanded Lookup. Related object (via Lookup) Custom Layout fields are displayed.

— Visualforce Pages (technical)
There are a number of extension points within the Salesforce1 app where Visualforce pages can be employed.

Mobile Card
Custom Publisher Action
Visualforce Tab added to the Mobile Navigation

In all cases the page must be mobile enabled and designed specifically for rendering on constrained devices, i.e mobile plus tablet on a variety of form factors. A best practice to consider in this respect is the development of adaptable pages that support both Salesforce1 and standard Salesforce web app execution. There are 3 development models in this respect classic Visualforce, Visualforce mixed with JavaScript, JavaScript and static HTML only with JS remoting. In all models the recently added (to Visualforce) ability to pass through attributes (HTML5) will be of use. Additionally, Salesforce1 comes with a highly useful navigation framework JavaScript library.

In short, where branded or complex UI interactions are required Visualforce provides a good option. Such pages must be carefully designed to deliver a optimal mobile experience and where possible be adaptable to also work in the standard Salesforce web app. This latter point relates to the cost of development and maintenance, adaptability is better than duplication – at the cost of smart design.

— Force.com Canvas Apps (technical)
There are a number of extension points within the Salesforce1 app where Force.com Canvas apps can be employed. Pilot program currently.

Custom Publisher action – post to the feed from a Canvas app
Chatter Feed – Canvas app rendered in a feed item

The use cases for the above will be varied and no doubt very interesting. Over time I think we’ll see more traction in the area of application composition. The ability to compose experiences and interactions across applications seamlessly (with context) and securely is a powerful concept.

— Smart Search Items
Recently searched for objects for the user. Searches performed in the full web app appear can appear In the left navigation menu, pin objects in the full site to keep at the top of this list.

— Notifications (declarative)
The Salesforce1 notifications platform supports 2 types of notification;

In App – Appear in the app itself and can be accessed manually via the bell icon (top right hand corner)
Push – Mobile device applications only, push notifications not supported by the mobile browser app. Such notifications will appear as badges on the app icon etc.

The notifications are limited to Chatter (mentions, posts, comments etc.) and Approval requests. Notifications are enabled at the org-level, not per-user.

Final Thoughts
In application development terms, Salesforce1 provides a rich set of declarative and programmatic techniques for the development of mobile interactions. Building out compelling mobile experiences that replace or augment existing Saleforce functionality (custom or standard) should be highly achievable whether you’re technically minded or not. This in my view is the real power of the Saleforce1 app, once you accept the proprietary approach there are no barriers to rapid mobile enablement.

UI Tips and Tricks – Inline Visualforce Resize

A real frustration with inline Visualforce pages (added to standard page layouts) is the static nature of the height setting. From the layout we can set a specific height, but ideally we want the height set dynamically from the content height. Sounds like a simple enough requirement, however, the fact that the VF page section is implemented as an iFrame loaded from another domain makes cross-domain communication a non-trivial task. Note, for security reasons browsers enforce a same-origin policy, preventing script running across domain boundaries. Workarounds to this restriction include the HTML5 postMessage function on the client and proxy services on the server. So the question becomes, how can the iFrame content communicate across domains to tell the host page the correct height for the iFrame? The answer to this is somewhat contrived, but hopefully my basic approach below tells a clear enough story.

Here we go.
1. The inline VF page contains an iFrame, into which we load a helper script file from the base salesforce domain with a height parameter in the querystring.

document.getElementById(‘helpframe’).src=’https://emea.salesforce.com{!$Resource.iFrameHelper}?height=’+h+’&iframename=MYPAGENAME&cacheb=’+Math.random();

The random parameter is there to avoid caching issues. Crucially as this helper script is running on the same domain as the standard page layout, it can call a script in the page itself. Note the helper script is loaded from a static resource. To keep the solution generic the page name is passed as a parameter also, handily the title attribute in the host page is set to the page name, we’ll use this later to find the id for the iFrame.

2. In the helper script we extract the 2 parameters from the querystring and call a script function in the host page (via parent.parent – which traverses up the DOM to the parent page).

3. In order to add script to the host page we use the Sidebar injection technique (or hack) and introduce a simple Javascript function (via a narrow component) which takes the page name and height, finding the former in the DOM using Ext.query (Ext is already referenced in the page), and setting the element height to the latter.

Example solution components::

0. Pre-requisites:
User Profiles must have the “Show Custom Sidebar On All Pages” General User Permission ticked.

1. Add a HTML file static resource, named iFrameHelper – content below.

<html>
  <body onload="parentIframeResize()">  
    <script>  
      // Tell the parent iframe what height the iframe needs to be 
      function parentIframeResize(){ 
         var height = getParam('height'); 
         var iframename = getParam('iframename');
         // This works as our parent's parent is on our domain.. 
         parent.parent.resizeIframe(height,iframename); 
      }  
      // Helper function, parse param from request string 
      function getParam(name){ 
        name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); 
        var regexS = "[\\?&]"+name+"=([^&#]*)"; 
        var regex = new RegExp( regexS ); 
        var results = regex.exec( window.location.href ); 
        if( results == null ) 
          return ""; 
        else 
          return results[1]; 
      }
    </script>  
  </body>  
</html>

2. Add a HTML sidebar component (narrow left) – click “Show HTML” and paste in markup below.

<script>
  function resizeIframe(h, ifn){
   var e = Ext.query("iframe[title='"+ifn+"']");  
   console.log(e); 
   var itarget = e[0].getAttribute('id'); 
   Ext.get(itarget).set({height: parseInt(h)+10});
  }
</script>

3. Add a Visualforce page named MyTestInlineVFPage – paste in markup below.

<apex:page docType="html-5.0" standardController="Account">
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  This is your new Page<br/>
  
  <script type="text/javascript">      
        function resizeParentIFrame(){
            var h = document.body.scrollHeight;
            //TODO - replace with the relevant page name.
            var iframename = 'MyTestInlineVFPage';
            //TODO - replace with the relevant base url - page runs on the VF domain so functions return the VF domain. 
            var baseUrlForInstance = 'https://emea.salesforce.com';
            document.getElementById('helpframe').src = baseUrlForInstance+'{!$Resource.iFrameHelper}?height='+h+'&iframename='+iframename+'&cacheb='+Math.random();
        }
        
        function forceParentIFrameResize(){
            document.getElementById('helpframe').src=document.getElementById('helpframe').src;
        }
        
        window.onload=function(){
            resizeParentIFrame();
        }
  </script>  
</apex:page>

4. Add the VF page to a new section on an Account layout.

The solution above needs further work in the areas below. I’m planning to improve this as part of a commercial AppExchange package I’m working on and will post the improved resize solution.

Code quality.
Exception handling.
Calculation of the base salesforce domain – currently hardcoded in the inline page.

I’d be delighted to hear about other improvements, or indeed alternative approaches.

Salesforce Touch Platform Guide

The newly released Salesforce Touch Platform Guide provides a very useful consolidation of both technical information and design guidance pertinent to mobile development on the Force.com platform. Topics covered include Identity, Connected Apps, Mobile Components for Visualforce etc. All in all an essential read for anyone considering mobile app development.

Of particular interest is the content related to the Hybrid model of mobile application development (chapter 6 & 7), combining the benefits of native app development, i.e. on-device integration (Contacts, Calendar, local storage etc.), with the rapid development and ease of update of HTML5 development and web apps.

UI Tips and Tricks – Country List

This tip introduces a simple pattern used by many AppExchange solutions to manipulate standard layouts. In short, a custom sidebar component is added which contains JavaScript code which manipulates the page at the DOM level. The following simple example shows how the country fields on the account layout can be changed to lists – a common customer request. A Country__c custom object with the Name field populated with country names is required. The Dojo library is also used. Please note, the code below is old and hasn’t been tested recently, I provide this for illustration of the pattern, it certainly won’t be winning any prizes.

So, in the example, Dojo runs the replaceCountryInputs() function when the DOM is ready, this finds the country fields by their ID (Firebug is a good way to browse the page markup), we then remove the original input element and replace with a select element with the same Id. The select element is then populated in JavaScript with the result of a soql query – using the Ajax API. Finally we need to add the inline editing handlers to the new select element – and we’re done.

On the configuration side, the sidebar component must be assigned to the home page layout for relevant users and the setting to enforce display of custom sidebar components must be enabled.

As I’ve said, the use case here isn’t significant it’s the possibility that this technique enables in terms of standard layout manipulation. Be aware that the field ids may be subject to change.


<script src="/js/dojo/0.4.1/dojo.js"></script>
<script src="/soap/ajax/19.0/connection.js" type="text/javascript"></script>
<script type="text/javascript">
var arCountries = getCountries();
var vProcess = replaceCountryInputs();

function replaceCountryInputs()
{
  if (document.getElementById('acc17country')!=null) {
    var defaultVal = document.getElementById('acc17country').value;
    var cInput = swapFieldType('acc17country');
    setCountryListWithDefault(cInput, defaultVal);
  }
  if (document.getElementById('acc18country')!=null) {
    var defaultVal = document.getElementById('acc18country').value;
    var cInput = swapFieldType('acc18country');
    setCountryListWithDefault(cInput, defaultVal);
  }
  if (document.getElementById('acc17_ilecell')!=null) {
    SetupInline('acc17');
  }
  if (document.getElementById('acc18_ilecell')!=null) {
    SetupInline('acc18');
  }
}

function swapFieldType(i)
{
  var cInput = document.getElementById(i);
  var cInputParent = cInput.parentNode;
  cInputParent.removeChild(cInput);
  cInput = document.createElement('select');
  cInput.size = 1;
  cInput.id = i;
  cInput.name = i;
  cInputParent.appendChild(cInput);
  return cInput;
}

function setCountryListWithDefault(i, d)
{
  if(i!=null) {
    if(arCountries.length>0) {
      for(x=0;x<arCountries.length;x++) {
        if(arCountries[x]==d) {
          i.options[x] = new Option(arCountries[x], arCountries[x], false, true);
        } else {
          i.options[x] = new Option(arCountries[x], arCountries[x], false, false);
        }
      }
    } else {
      i.options[x] = new Option('No countries found', 'No countries found', false, true);
    }
  }
}

function SetupInline(prefix) {
  var _element = document.getElementById(prefix + '_ilecell');	
  if (_element) {
    _element.ondblclick = function() {
      var _loaded = false;
      if (!sfdcPage.editMode)
        sfdcPage.activateInlineEditMode();
        
      if (!sfdcPage.inlineEditData.isCurrentField(sfdcPage.getFieldById(_element.id)))
        sfdcPage.inlineEditData.openField(sfdcPage.getFieldById(_element.id));
        
      var idInput = prefix+'country';

      if (document.getElementById(idInput)!=null) {
        var defaultVal = document.getElementById(idInput).value;
        var cInput = swapFieldType(idInput);
        setCountryListWithDefault(cInput, defaultVal);
      }		
    }
  }
}

function getCountries()
{
  sforce.sessionId = getCookie('sid');
  sforce.connection.sessionId=sforce.sessionId;
  var out = [];
  try {
    var queryCountries = sforce.connection.query("Select Id, Name FROM Country__c ORDER BY Name");	
    var countries = queryCountries.getArray('records');
    for(x=0;x<countries.length;x++) {
      out[out.length] = countries[x].Name;
    }					
  } catch(error) {
    alert(error);		
  }	
  return out;
}
dojo.addOnLoad(replaceCountryInputs);
</script>

UI Tips and Tricks – Picture Upload

In the very early 90s I was employed as a professional Visual Basic programmer (and no that isn’t a contradiction in terms) enjoying greatly the development of client-server accounting systems. Good times. In those days tips and tricks sites were a big part of how the community shared knowledge. Following some recent reminiscing, through this series of posts, I’ll share some UI tips and tricks that aren’t necessarily an architects concern however I hope will prove helpful to some.

Ok, so down to business. Today’s tip concerns the upload and display of record images in the context of standard functionality, for example a Product image or Contact photo. The latter I’ll use as an example.

1. Add a field to Contact named [Photo Document Id], of the text type, 18 char length.

2. Add a field to Contact named [Photo], of the text formula type, formula set as below.

<formula>IMAGE('/servlet/servlet.FileDownload?file='Photo_Document_Id__c'')</formula>

3. Add a Visualforce page named ContactPictureUploader, with no markup between the page tags.
4. Add an Apex class ContactPictureUploaderController, code as below, set as the VF page controller and ensure the page action is set to the initialise method.

public with sharing class ContactPictureUploaderController {
	private Id contactId;

	public ContactPictureUploaderController(){
		contactId = ApexPages.currentPage().getParameters().get('cid');
	}

	public PageReference initialise(){
		List<Attachment> listA = [select a.Id from Attachment a 
									where a.createdById =:UserInfo.getUserId() 
									and a.parentId=:contactId order by a.createdDate desc limit 1];														
		if (listA.size()>0){
			Id attachmentId = listA[0].Id;
			
			Contact c = [select Id from Contact where Id=:contactId];
			c.Photo_Document_Id__c = attachmentId;
			update c;
		}
		return new PageReference('/'+contactId);		
	}
}

VF Page markup.

<apex:page controller="ContactPictureUploaderController" action="{!initialise}">
<!-- Controller context page - no markup -->
</apex:page>

5. Add a custom button to the Contact object of the JavaScript type, named Upload Photo, script as below.

parent.window.location.href='p/attach/NoteAttach?pid={!Contact.Id}&retURL=/apex/ContactPictureUploader?cid={!Contact.Id}'

In short, the solution works by invoking the standard attachment page, which on submit redirects to the VF page which copies the uploaded document id to the Contact Photo Document Id field, then redirects back to the contact record. The image field on the Contact object then loads the image using the IMAGE() formula field, easy. The image field can then be used on the contact layout, related lists etc..

FieldSets in Apex

As of the Summer ’12 release FieldSets can be accessed through Dynamic Apex (or Apex Describe). Remember, FieldSets are admin configured arbitrary groupings of fields, added on the object detail page within setup. With the upcoming release, controller code can now dynamically build a soql query that retrieves the data for the fields in the FieldSet, which in turn can then provide data for Visualforce dynamic bindings. Previously the binding was possible, but there was no way to retrieve the fields to construct the soql query – a serious limitation.

Why is this interesting? Well, consider you’re developing a custom app comprised of Visualforce pages, wouldn’t it be great if new custom fields added later could simply just render in the UI (in a logical position) without modifying the page definition or controller code? Or perhaps that obsolete field you no longer wish to display just disappears..

Admin configurable custom pages (and apps) is a good thing. If you’re building custom pages give some thought to FieldSets and dynamic field rendering, this could save you time and your customer money down the line.

Same principle as Custom Labels, you need to rule the dynamic field rendering requirement out, not in.

for(Schema.FieldSetMember f : SObjectType.Player__c.FieldSets.Dimensions.getFields()){
  q+=f.getFieldPath()+',';
}

Visualforce User Agent Detection

The code below provides an example of a page action method used to detect a mobile user-agent and perform redirection. Alternatively the same approach could be used with dynamic Visualforce components to switch between mobile/web optimised page composition.

	public PageReference redirectDevice(){
		String userAgent = ApexPages.currentPage().getHeaders().get('USER-AGENT');
		
		//& some devices use custom headers for the user-agent.
		if (userAgent==null || userAgent.length()==0){
			userAgent = ApexPages.currentPage().getHeaders().get('HTTP_X_OPERAMINI_PHONE_UA');
		}		
		if (userAgent==null || userAgent.length()==0){
			userAgent = ApexPages.currentPage().getHeaders().get('HTTP_X_SKYFIRE_PHONE');
		}
		 		
  		//& replace with custom setting - using (?i) case insensitive mode.		
		String deviceReg = '(?i)(iphone|ipod|ipad|blackberry|android|palm|windows\\s+ce)';
		String desktopReg = '(?i)(windows|linux|os\\s+[x9]|solaris|bsd)';
		String botReg = '(?i)(spider|crawl|slurp|bot)';
		
		Boolean isDevice=false, isDesktop=false, isBot=false;
		
		Matcher m = Pattern.compile(deviceReg).matcher(userAgent);
		if (m.find()){
			isDevice = true;
		} else {
			//& don't compile the patterns unless required.
			m = Pattern.compile(desktopReg).matcher(userAgent);
			if (m.find()) isDesktop = true;
			
			m = Pattern.compile(botReg).matcher(userAgent);
			if (m.find()) isBot = true;
		}
		//& Default is mobile - unless a desktop or bot user-agent identified.					
		if (!isDevice && (isDesktop || isBot)) return null; //& no redirect.						
		return new PageReference('/apex/MobileIndex'); //& redirect.
	}