I am officially a Salesforce Advanced Developer!!!

I was notified today that I passed the programming assignment and essay exam so I am officially a Salesforce Advanced Developer!!!

I had to wait more than one year after passing the multiple choice exam before getting an opportunity to work on the programming assignment. I spent well over the suggested 20 hours working on the assignment (probably more like 80), dedicated an entire week so I could apply laser focus to the assignment, and antagonized over every little detail, every design decision, every code comment, every variable name, every governor limit, every business requirement, every unit test, and lots of other details. It got a little crazy.

I was certain that I would pass after submitting the programming assignment and even more certain after submitting the essay exam BUT that was fourteen stressful weeks ago. So was it all worth it? Heck Yes, the journey alone was TOTALLY worth it because because because Salesforce has transformed my work back into my hobby which is exactly the reason I became a developer in the first place some thirty years ago.

-JSON

cert_dev_adv_rgb

Advertisements

Lightning Connect Quick Start

Tags

This blog is intended to get your hands dirty with Lightning Connect in your development org in about 5 minutes. I intend to write subsequent blogs, which will go into more detail on accessing the BIG DATA stored in relational and no-sql cloud databases by leveraging Open Data Protocol, aka OData, and Salesforce Lightning Connect, but first the basics:

  1. Click Setup > Develop > External Data Source
  2. Click New External Data Source
  3. Enter Northwind for the Label
  4. Select Lightning Connect: OData 2.0 for the Type
  5. Enter http://services.odata.org/V2/Northwind/Northwind.svc/ for the URL
  6. Click Save
  7. Click Validate and Sync
  8. Select Customers, Invoices, Orders, Products, plus any others you like
  9. Click Sync
  10. Click Products

Voilà, you have now connected your development org to the Northwind OData service. To prove it, let’s write a SOQL in anonymous apex:


List<jsonhammerle2__Products__x> products = [SELECT jsonhammerle2__ProductName__c FROM jsonhammerle2__Products__x];

System.Debug(products);

Note that you will have to change or remove the namespace jsonhammerle2, which is mine.  Execute the anonymous apex and then check your debug logs. You should see the external products names accessed directly from SOQL. Do you see the power?

Let’s have a bit more fun and write SOSL in anonymous apex:


List<List<SObject>> products = [FIND 'Gumbo*' IN ALL FIELDS RETURNING jsonhammerle2__Products__x (jsonhammerle2__ProductName__c), Contact, Opportunity, Lead];

System.Debug(products);

Again, you will have to remove or change my jsonhammerle2 namespace prefix, execute the anonymous apex, and view your debug logs. You probably have many questions right now. If you answer those questions then please share as a response to this blog.

Let’s have some declarative fun next.

  1.  Click Setup > Create> Tabs
  2. Click New Custom Object Tab
  3. Select Products
  4. Select a Tab Style (I generally don’t like this step)
  5. Click Next
  6. Click Next
  7. Click the Products Tab
  8. Click Create New View
  9. Enter Product View for the View Name
  10. Select Fields to Display: ProductID, ProductName, UnitPrice, UnitsInStock
  11. Click Save

Do you see the absolute beauty on the screen in what you have done? You have integrated Salesforce with an external data source and created a list view user interface with clicks not code. And best of all, the data is not duplicated. That’s Lightning Connect!

You are now free to experiment with your external data sources in your development org. You’ll find limitations like the data is read-only and is not yet available for reporting but then again this is just Lightning Connect Version One.

– json

Lightning

 

Salesforce 1 Ride the Lightning – Part Three

Tags

, , , , ,

“Struck by lightning, struck by lightning!” J.R.R. Tolkien.

Our simple Salesforce 1 app from Part One and Part Two has been refactored into lightning like magic. In this post, we will examine the magic that is the source code to gain a better understanding of lightning applications, lightning components, and lightning events.

After we examine the source code, we will compare our refactored app and the underlying event-based architecture to our previous two solutions to illustrate a significant Salesforce Development paradigm shift that has begun and will soon bring together a growing community of 1,700,000 developers. I think that right now will be the most exciting time to be a developer in our lifetime.

Let’s ride the lightning!


If you struggle with any of the following code constructs then refer to the Lightning Developers Guide or the Lightning Cheat Sheet. And if you are still stuck, then reply to this blog so we can master the lightning together.

DisplayOpportunity – this is a lightning event that is composed of the EVENT.XML file. This event is fired when a user presses an opportunity in the MobileOpportunity3List lightning component. The event is handled by the MobileOpportunity3Product lightning component.

EVENT.XML

<aura:event type="APPLICATION" description="Event template" >
  <aura:attribute name="opportunityId" type="String"/>
</aura:event>

 

DisplayOpportunityList – this is a lightning event that is composed of the EVENT.XML file. This event is fired when a user presses the back button on the MobileOpportunity3Product to return to the MobileOpportunity3List.

EVENT.XML

<aura:event type="APPLICATION" description="Event template" />

 

MobileOpportunity3List – this is a lightning component that is composed of the COMPONENT.XML, CONTROLLER.JS, HELPER.JS, and STYLE.CSS files. This component displays a list of opportunities and fires a DisplayOpportunityEvent when a user presses an opportunity.

COMPONENT.XML

<aura:component controller="jsonhammerle2.OpportunityController" implements="force:appHostable">

    <aura:attribute name="visible" type="Boolean" default="true" />
    <aura:attribute name="opportunities" type="Opportunity[]"/>  
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" /> 
    <aura:registerEvent name="DisplayOpportunityEvent" type="jsonhammerle2:DisplayOpportunity"/>
    <aura:handler event="jsonhammerle2:DisplayOpportunityList" action="{!c.handleDisplayOpportunityList}"/>

	<!-- Display Opportunity Data from Model -->
    <aura:renderIf isTrue="{!v.visible}">
        <div class="app-wrapper">
            <div class="header">
                <h1>Opportunities</h1>
            </div>
            <div class="list-view">
                <aura:iteration items="{!v.opportunities}" var="opportunity">
                    <li>
                        <a href="{!'#' + opportunity.id}" onclick="{!c.displayOpportunity}" class="content">
                            <span class="icon">
                                <img src="data:image/png;base64,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" />
                            </span>
                            <h2><ui:outputText value="{!opportunity.Name}"/></h2>
                            <h3><ui:outputNumber value="{!opportunity.Amount}" format="¤#,##0.00;(¤#,##0.00)"/></h3>
                        </a>
                    </li>
                </aura:iteration>
            </div>
        </div>
    </aura:renderIf>
</aura:component>

CONTROLLER.JS

({
  //Initialize the view and fetch the list of expenses on load
  doInit : function(component, event, helper) 
  {   
    //Fetch the expense list from the Apex controller   
    helper.getOpportunityList(component);
  },
    
  displayOpportunity : function(component, event, helper) 
  {
    // parese the ID from the DOM element
    var opportunityId = event.currentTarget.hash.substring(1,19); 
    var evt = $A.get("e.jsonhammerle2:DisplayOpportunity");
    evt.setParams({"opportunityId": opportunityId});

    evt.fire(); 
    component.set("v.visible", false);
  },
    
  handleDisplayOpportunityList : function(component, event, helper) 
  {   
    component.set("v.visible", true);

    //Fetch the expense list from the Apex controller   
    helper.getOpportunityList(component);
  },
})

HELPER.JS

({
  //Fetch the expenses from the Apex controller
  getOpportunityList: function(component)
  {
    //Set the action to invoke the Apex controller method
    var action = component.get("c.getOpportunities");

    //Set up the callback
    var self = this;
    action.setCallback(this, function(actionResult)
    {
      //Reset the value of the component list attribute with the records returned
      component.set("v.Opportunities", actionResult.getReturnValue());
    });

    //Enque the action
    $A.enqueueAction(action);
  },
})

STYLE.CSS

.THIS .app-wrapper {
  display: block;
  position: absolute;
  height: auto;
  min-height: 100%;
  width: 100%;
  top: 0px;
  left: 0px;
}

.THIS .header {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 58px;
  background: #78a5ad;
}

.THIS .header h1 {
  font-size: 165%;
  font-weight: bold;    
  color: #e6f0f1;
  display: block;
  position: absolute;
  top: 16px;
  left: 0;
  width: 100%;
  text-align: center;
}

.THIS .list-view li {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  text-decoration: none;
  border-bottom: 1px dotted #cccccc;
}

.THIS .list-view div {
  vertical-align: baseline;
}

.THIS .list-view h2 {
  font-size: 125%;
  padding-left: 20px;
  padding-top: 7px;
  padding-bottom: 5px;
  color: black;
}

.THIS .list-view h3 {
  font-size: 110%;
  padding-left: 20px;
  padding-top: 5px;
  padding-bottom: 5px;
  color: black;
}

.THIS .list-view li:first-child {
  padding-top: 59px;
}

.THIS .list-view li:last-child {
  border-bottom: none;
}

.THIS .icon {
  display: block;
  position: absolute;
  float: right;
  right: 0;
  height: 100%;
  padding-top: 17px;
  padding-right: 20px;
}

 

MobileOpportunity3Product – this is a lightning component that is composed of the COMPONENT.XML, CONTROLLER.JS, and STYLE.CSS files. This component displays a list of opportunities products and fires a DisplayOpportunityList event when a user presses the back button.

COMPONENT.XML

<aura:component controller="jsonhammerle2.OpportunityController" implements="force:appHostable">
	<aura:attribute name="visible" type="Boolean" default="false" />
    <aura:attribute name="OpportunityLineItems" type="OpportunityLineItem[]"/>  
    <aura:handler event="jsonhammerle2:DisplayOpportunity" action="{!c.handleDisplayOpportunity}"/>
    <aura:registerEvent name="DisplayOpportunityEvent" type="jsonhammerle2:DisplayOpportunityList"/>

    <aura:renderIf isTrue="{!v.visible}">
        <div class="app-wrapper">
            <div class="header">
            	<span class="icon">
                	<a href="#" onclick="{!c.displayOpportunityList}">
                        <img src="data:image/png;base64,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" />
                	</a>
                </span>
                <h1>OpportunityProducts</h1>
            </div>
            <div class="list-view">
                <aura:iteration items="{!v.OpportunityLineItems}" var="opportunityLineItem">
                    <li>
                        <h2><ui:outputText value="{!opportunityLineItem.Name}"/></h2>
                        <h3><ui:outputNumber value="{!opportunityLineItem.TotalPrice}" format="¤#,##0.00;(¤#,##0.00)"/></h3>
                    </li>
                </aura:iteration>
            </div>
        </div>
    </aura:renderIf>
</aura:component>

CONTROLLER.JS

({
  handleDisplayOpportunity : function(component, event, helper)
  {
    // Make component visible now that we have an opportunity to display
    component.set("v.visible", true);

    // Set the action to invoke the Apex controller method
    var action = component.get("c.getOpportunityProducts");
    action.setParams({ opportunityId : event.getParam("opportunityId") });

    var self = this;
    action.setCallback(this, function(actionResult)
    {
      //Reset the value of the component list attribute with the records returned
      component.set("v.OpportunityLineItems", actionResult.getReturnValue());
    });

    //Enque the action
    $A.enqueueAction(action);
  },

  displayOpportunityList : function(component, event, helper)
  {
    // make component visible now that we have an opportunity to display
    component.set("v.visible", false);

    var evt = $A.get("e.jsonhammerle2:DisplayOpportunityList");
    evt.fire();
  },

})

STYLE.CSS

.THIS .app-wrapper {
  display: block;
  position: absolute;
  height: auto;
  min-height: 100%;
  width: 100%;
  top: 0px;
  left: 0px;
}

.THIS .header {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 58px;
  background: #78a5ad;
}

.THIS .header h1 {
  font-size: 165%;
  font-weight: bold;    
  color: #e6f0f1;
  display: block;
  position: absolute;
  top: 16px;
  left: 0;
  width: 100%;
  text-align: center;
}

.THIS .list-view li {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  text-decoration: none;
  border-bottom: 1px dotted #cccccc;
}

.THIS .list-view div {
  vertical-align: baseline;
}

.THIS .list-view h2 {
  font-size: 125%;
  padding-left: 20px;
  padding-top: 7px;
  padding-bottom: 5px;
  color: black;
}

.THIS .list-view h3 {
  font-size: 110%;
  padding-left: 20px;
  padding-top: 5px;
  padding-bottom: 5px;
  color: black;
}

.THIS .list-view li:first-child {
  padding-top: 59px;
}

.THIS .list-view li:last-child {
  border-bottom: none;
}

.THIS .icon {
  display: block;
  position: absolute;
  float: left;
  left: 0;
  height: 100%;
  padding-top: 13px;
  padding-left: 15px;
  z-index:10;
}

 

MobileOpportunity3Connector – this lightning application enables the two decoupled components to communicate using APPLICATION events. I like to think of this as the metadata from the lightning application builder that represents an admin dragging and dropping components to build a composite application. This is just a guess as the lightning application builder has yet to be released.  Perhaps someone in the know will verify or correct this assumption.

APPLICATION.XML

<aura:component>
  <jsonhammerle2:MobileOpportunity3List/>
  <jsonhammerle2:MobileOpportunity3Product/>
</aura:component>

 

MobileOpportunity3CompositeApp – this lightning application may be used to test the composite application in the FORCE.IDE.

APPLICATION.XML

<aura:application>
  <jsonhammerle2:MobileOpportunity3Connector />
</aura:application>

 

And there you have the lightning!

I like this app because the Opportunity List is decoupled from the Opportunity Product List. This was achieved by replacing static references and calls with events and event handlers. This subtle difference allows the Salesforce administrators to declaratively create composite applications made up of lightning components using the Lightning App Builder. Moreover, the component developer is free to focus on developing the component without being too concerned about the complexities of the application. This is in contrast to our version from Part One where the Opportunity List, the Opportunity Product List, the business logic, the View, the Data Model, State Transitions, and the many references to external JavaScipt libararies and Cascading Style Sheets all reside in a single VisualForce page that glued it all together in one great big monolithic hot mess.

I also like this app ALOT because the components have no external references to JavaScript libraries nor Cascading Style Sheets which allowed me – the component developer – to fully understand every aspect of my component, how my component communicated with other components, and how my component communicated with Salesforce. This is in contrast with Angular version, which took control of the app except for when Angular called my injected methods.

With that said, I see similarities between AngularJS modules and Lightning Components in how they both seek to encapsulate logical and user interface aspects using different means, e.g. dependency injection versus event-architecture. We will explore how we can leverage the best of both later in this series.

I am not sure yet where we will go in the next version but Part Three is clearly the beginning of a very exciting ride.

– JSON

lightning

Salesforce 1 Ride The Lightning – Part Two

Tags

, , , , , ,

We will improve the app created in Part One by leveraging AngularJS to separate concerns on the client side – and then in Part Three we will convert our simple example app to LIGHTNING!

lightning

Here is the Visualforce page which displays a list of Opportunities and the related Opportunity Products in Salesforce 1 but this time we will use AngularJS instead of Underscore:

<!--
  -  This visualforce page defines all the pages for the Salesforce
  -  1 Mobile Opportunity example app, which illustrates one way to
  -  implement a Salesforce 1 app using the Angular javascript
  -  library to separate MVC concerns on the client side.
  -
  -  @author Jason Hammerle
  -->
 
<apex:page docType="html-5.0"
           showHeader="false"
           sidebar="false"
           standardStylesheets="false"
           standardController="Opportunity"
           extensions="OpportunityController">
 
<html class="no-js">
  <head>
      <title>Mobile Opportunity 2</title>
      <meta charset="utf-8"/>
      <meta name="viewport" content="width=device-width"/>
      <meta name="apple-mobile-web-app-capable" content="yes" />
 
      <!-- Mobile Design Template Styles -->
      <apex:stylesheet value="{!URLFOR($Resource.Mobile_Design_Templates,
        'Mobile-Design-Templates-master/common/css/app.min.css')}"/>
 
      <!-- Javascript libs (static resource) on Salesforce Server -->
      <apex:includeScript value="{!URLFOR($Resource.AngularJS,
        'libs/jquery/dist/jquery.js')}"/>
      <apex:includeScript value="{!URLFOR($Resource.AngularJS,
        'libs/angular/angular.js')}"/>
      <apex:includeScript value="{!URLFOR($Resource.AngularJS,
        'libs/angular-ui-router/release/angular-ui-router.js')}"/>
      <apex:includeScript value="{!URLFOR($Resource.AngularJS,
        'libs/angular-sanitize/angular-sanitize.js')}"/>
      <apex:includeScript value="{!URLFOR($Resource.AngularJS,
        'libs/angular-touch/angular-touch.js')}"/>
      <apex:includeScript value="{!URLFOR($Resource.AngularJS,
        'libs/angular-route/angular-route.js')}"/>
 
      <!-- Mobile Opportunity Angular App (static resource) -->
      <apex:includeScript value="{!URLFOR($Resource.MobileOpportunity2,
        'app/controller.js')}"/>
      <apex:includeScript value="{!URLFOR($Resource.MobileOpportunity2,
        'app/app.js')}"/>
 
      <script>
          // set so javascript files will know where to look for static resources
          window.MOBILE_OPPORTUNITY_2_ROOT = "{!URLFOR($Resource.MobileOpportunity2, '')}";
 
          // encapsulate in an immediately invoked function
          (function (){
 
            var mobileOpportunity2Module = angular.module(
              'mobileOpportunity2-app');
 
            // inject functions into angular to connect javascript
            // controller with apex controller
            injectVFRemoting(
              'getOpportunities',
              '{!$RemoteAction.OpportunityController.getOpportunities}'
            );
            injectVFRemotingWithParam(
              'getOpportunityProducts',
              '{!$RemoteAction.OpportunityController.getOpportunityProducts}'
            );
 
            // inject a no argurment function
            function injectVFRemoting(jsFunction, vfFunction)
            {
              mobileOpportunity2Module.factory(jsFunction, ['$q', '$rootScope', function($q, $rootScope) {
                return function() {
                  var deferred = $q.defer();
 
                  Visualforce.remoting.Manager.invokeAction( vfFunction, function(result, event) {
                      $rootScope.$apply(function() {
                          if (event.status) {
                            deferred.resolve(result);
                          }
                          else {
                            deferred.reject(event);
                          }
                        }
                      )
                    },
                    { buffer: true, escape: false, timeout: 30000 }
                  );
                  return deferred.promise;
                }
              }]);
            }
 
            // inject a single param function
            function injectVFRemotingWithParam(jsFunction, vfFunction)
            {
              mobileOpportunity2Module.factory(jsFunction, ['$q', '$rootScope', function($q, $rootScope) {
                return function(param1) {
                  var deferred = $q.defer();
                  Visualforce.remoting.Manager.invokeAction(vfFunction, param1, function(result, event) {
                      $rootScope.$apply(function() {
                          if (event.status) {
                            deferred.resolve(result);
                          }
                          else {
                            deferred.reject(event);
                          }
                        }
                      )
                    },
                    { buffer: true, escape: false, timeout: 30000 }
                  );
                  return deferred.promise;
                }
              }]);
            }
          }());
      </script>
  </head>
  <body ng-app="mobileOpportunity2-app">
    <ui-view ng-animate="'view'"/>
  </body>
</html>
</apex:page>

The apex code is exactly the same as in Part One so that code has not been included in this blog.

Most of the web content has been moved from the Visualforce page into a static resource which contains app.js, controller.js, OpportunityTemplate.html, and OpportunityProductTemplate.html.

Here is the app.js:

 /*
  * This class defines the Mobile Opportunity Salesforce 1 Angular JS application.
  */

/*jslint node: true */
'use strict';

// encapsulate in an immediately invoked function
(function (){

    var mobileOpportunity2Module = angular.module('mobileOpportunity2-app', 
        ['ui.router',
         'ngSanitize',
         'ngTouch',
         'ngRoute',
         'mobileOpportunity2-controller']);

    mobileOpportunity2Module.
    config(function($stateProvider, $urlRouterProvider) {
        $urlRouterProvider.otherwise('/Opportunity');
        $stateProvider
            .state('Opportunity', {
                url: '/Opportunity',
                templateUrl: function()
                {
                    return MOBILE_OPPORTUNITY_2_ROOT +
                        '/app/views/OpportunityTemplate.html'
                },
                controller: 'OpportunityController'
            })
            .state('OpportunityProduct', {
                url: '/OpportunityProduct/:opportunityId',
                templateUrl: function()
                {
                    return MOBILE_OPPORTUNITY_2_ROOT +
                        '/app/views/OpportunityProductTemplate.html';
                },
                controller: 'OpportunityProductController'
            });
    });
}());

And, here is controller.js:

/*
 * This class defines the Angular JS controller.
 */

/*jslint node: true */
'use strict';

// encapsulate in an immediately invoked function
(function (){

    var mobileOpportunity2Module =
        angular.module('mobileOpportunity2-controller', []);

    mobileOpportunity2Module
        .controller('OpportunityController', ['$scope', 'getOpportunities',
            function($scope, getOpportunities)
            {
                getOpportunities().then( function(result)
                {
                    $scope.opportunities = result;
                })
            }])
        .controller('OpportunityProductController', ['$scope', '$stateParams', 'getOpportunityProducts',
            function($scope, $stateParams, getOpportunityProducts)
            {
                getOpportunityProducts($stateParams.opportunityId).then( function(result)
                {
                    $scope.opportunityProducts = result;
                })
            }]);
}());

And, here is OpportunityTemplate.html:

<header>
  <h1>Opportunities</h1>
</header>
<div class="app-content">
    <ul class="list-view with-swipe left-thumbs right-one-icons">
        <li ng-repeat="opportunity in opportunities track by $index">
            <div>
              <a href="#/OpportunityProduct/{{opportunity.Id}}" class="content">
                <h2 ng-bind-html="opportunity.Name"/>
                {{opportunity.Amount | currency}}
                <div class="list-view-icons">
                    <span class="icon-right-arrow"></span>
                </div>
              </a>
            </div>
        </li>
    </ul>
</div>

Finally, here is OpportunityProductTemplate.html:

<header>
    <div class="main-menu-button main-menu-button-left"><a class="left-arrow" href="#/Opportunity">&nbsp;</a></div>
    <h1>Opportunity Products</h1>
</header>
<div class="app-content">
    <ul class="list-view with-swipe left-thumbs right-one-icons">
        <li ng-repeat="opportunityProduct in opportunityProducts track by $index">
            <div>
              <h2 ng-bind-html="opportunityProduct.Name"/>
              {{opportunityProduct.TotalPrice | currency}}
            </div>
        </li>
    </ul>
</div>

I like this app ALOT because the code that runs in the browser is cleanly separated into a controller, the views, and a data model. The controllers main purpose is to populate the data model and the views main purpose is to display the data model. This pattern would allow the app to evolve with much less effort and complexity compared to our app from the previous blog.

I do not like this app because it took research, several iterations, and time to get this app to function.

Also, I do not like that my code lives inside a static resource as it makes development more cumbersome. With that said, the MavensMate team has done an outstanding job at making it as painless as possible to work with static resources but there is still pain.

Finally, I really do not like this app because I think I do not fully understand every aspect of how my code interacts with the Mobile Design Templates, with Angular, and with the other Javascript libraries.

If we could isolate everything – the app, the components, helper code, the styles, the documentation, etc – into a single component then it would be easier to fully understand the component and possible to deploy the component in any ORG. In fact, an admin could even do it.

The real fun begins in Part Three.

– JSON

Salesforce 1 Ride the Lightning – Part One

Tags

, , ,

Salesforce released “Salesforce 1 Lightning” at the Dreamforce 2014 conference which heavily leverages AuraJS.  This is the same technology that Salesforce used to build Salesforce 1.  You can learn all about Salesforce 1 Lightning in this blog.

In this blog series, I will share code snippets as I iterate to learn this exciting new technology.

In this blog, I will share code for a very simple starter Salesforce 1 example app – no lightning just yet.  The lightning will strike in Part Three.

Here is the Visualforce page which displays a list of Opportunities and the related Opportunity Products in Salesforce 1 using the Underscore Javascript Library and the Mobile Design Templates:

<!--
  -  This visualforce page defines all the pages for the 
  -  Salesforce 1 Mobile Opportunity example app, which 
  -  illustrates one way to implement a Salesforce 1 app 
  -  using the underscore javascript library.
  -
  -  @author Jason Hammerle
  -->

<apex:page docType="html-5.0"
           showHeader="false"
           sidebar="false"
           standardStylesheets="false"
           standardController="Opportunity"
           extensions="OpportunityController">

<head>
    <title>Mobile Opportunity 1</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, height=device-height, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0" />
    <meta name="apple-mobile-web-app-capable" content="yes" />

    <!-- Mobile Design Template libs (static resource) on Salesforce Server -->
    <apex:stylesheet value="{!URLFOR($Resource.Mobile_Design_Templates, 
        'Mobile-Design-Templates-master/common/css/app.min.css')}"/>
    <apex:includeScript value="{!URLFOR($Resource.Mobile_Design_Templates,
        'Mobile-Design-Templates-master/common/js/jQuery2.0.2.min.js')}"/>
    <apex:includeScript value="{!URLFOR($Resource.Mobile_Design_Templates,
        'Mobile-Design-Templates-master/common/js/jquery.touchwipe.min.js')}"/>
    <apex:includeScript value="{!URLFOR($Resource.Mobile_Design_Templates,
        'Mobile-Design-Templates-master/common/js/main.min.js')}"/>

    <!-- Underscore libs (static resource) on Salesforce Server -->
    <apex:includeScript value="{!$Resource.underscore_1_5_1}"/>
    <apex:includeScript value="{!$Resource.path_js}"/>

    <script type="text/html" id='opportunityView'>
        <div class="app-wrapper">
            <header>
                <h1>Opportunities</h1>
            </header>
            <div class="app-content">
                <ul id="cList" class="list-view with-swipe left-thumbs right-one-icons">
                    <% for(var i = 0; i < opportunity.length; i++){ %>
                        <li>
                            <div>
                                <a href="#/opportunity/<%= opportunity[i].Id %>" class="content">
                                    <h2><%= opportunity[i].Name %></h2>
                                    <%= opportunity[i].Amount %>
                                    <div class="list-view-icons">
                                        <span class="icon-right-arrow">&nbsp;</span>
                                    </div>
                                </a>
                            </div>
                        </li>
                    <% } %>
                </ul>
            </div>
        </div>
    </script>

    <script type="text/html" id='opportunityProductView'>
        <div class="app-wrapper">
            <header>
                <div class="main-menu-button main-menu-button-left"><a class="left-arrow" href="#/opportunity">&nbsp;</a></div>
                <h1>Opportunity Products</h1>
            </header>
            <div class="app-content">
                <ul id="cList" class="list-view with-swipe left-thumbs right-one-icons">
                    <% for(var i = 0; i < opportunityProducts.length; i++){ %>
                        <li>
                            <div>
                                <h2><%= opportunityProducts[i].Name %></h2>
                                <%= opportunityProducts[i].TotalPrice %>
                            </div>
                        </li>
                    <% } %>
                </ul>
            </div>
        </div>
    </script>

    <script type="text/javascript">

    // encapsulate in an immediately invoked function
    (function (){
        var compiledOpportunityTempl = _.template($("#opportunityView").html());
        var compiledOpportunityProductTempl = _.template($("#opportunityProductView").html());

        // init
        $(document).ready(function()
        {
            getOpportunities();
        });

        // get from apex controller extension
        function getOpportunities(){
            Visualforce.remoting.Manager.invokeAction(
                    '{!$RemoteAction.OpportunityController.getOpportunities}',
                    function(opportunityRecords, e) {
                        showOpportunities(opportunityRecords);},
                    {escape:false});
        }

        // show in view
        function showOpportunities(opportunityRecords)
        {
            $('#mainContainer').empty();
            $('#mainContainer').append(
                compiledOpportunityTempl({opportunity : opportunityRecords}));
            $(document).trigger('onTemplateReady');
        }

        // get from apex controller extension
        function getOpportunityProducts(opportunityId){
            Visualforce.remoting.Manager.invokeAction(
                    '{!$RemoteAction.OpportunityController.getOpportunityProducts}',
                    opportunityId,
                    function(productRecords, f) {
                        showOpportunityProducts(productRecords);
                    },
                    {escape:false});
        }

        // show in view
        function showOpportunityProducts(opportunityProductRecords)
        {
            $('#mainContainer').empty();
            $('#mainContainer').append(
                compiledOpportunityProductTempl({opportunityProducts : opportunityProductRecords}));
            $(document).trigger('onTemplateReady');
        }

        Path.map("#/opportunity").to(function(){
            getOpportunities();
        });

        Path.map("#/opportunity/:opportunityProductId").to(function(){
            getOpportunityProducts(this.params['opportunityProductId']);
        });

        Path.listen();
    }());

    </script>
</head>

<body>
    <div id="mainContainer">
    </div>
</body>

</apex:page>

And here is the Apex code:

/**
 * Provides the remote and aura actions to drive the mobile opportunity examples.
 *
 * @author Jason Hammerle
 */
public with sharing class OpportunityController
{
    private final sObject mysObject;

    // The extension constructor initializes the private member
    // variable mysObject by using the getRecord method from the standard
    // controller.
    public OpportunityController(ApexPages.StandardController stdController)
    {
        this.mysObject = (sObject)stdController.getRecord();
    }

    public String getRecordName()
    {
        return 'Hello ' + (String)mysObject.get('name') + ' (' + (Id)mysObject.get('Id') + ')';
    }

    // Get opportunities for client
    @RemoteAction
    public static List<Opportunity> getOpportunities()
    {
        List<Opportunity> opportunities =
        [SELECT     Id, Name, Amount, Probability, ExpectedRevenue
        FROM       Opportunity
        ORDER BY   ExpectedRevenue DESC, LeadSource DESC
        LIMIT      200];

        return opportunities;
    }

    // Get opportunity products for client
    @RemoteAction
    public static List<OpportunityLineItem> getOpportunityProducts(String opportunityId)
    {
        List<OpportunityLineItem> opportunityProducts =
        [SELECT     Id, Name, Quantity, TotalPrice, UnitPrice
        FROM       OpportunityLineItem
        WHERE      OpportunityId = :opportunityId
        ORDER BY   TotalPrice DESC
        LIMIT      200];

        return opportunityProducts;
    }
}

I like this app because it illustrates just how easy and straightforward it can be to build a Salesforce 1 app.

I do not like this app because the model, view, and controller on the client side are all combined in one great big monolithic hot mess – and it will just get worse as the app evolves. Wouldn’t it be nice if there were a way to separate concerns on the client side?

Also, I do not like this app because it has dependencies with other libraries. I bet you don’t like it either because it would take you *some* effort to get this app to work in your org. Wouldn’t it be nice if you could just plug in a self contained component into your org with no or very little work?

We will address these shortcomings in Part Two and Three – and then we will build out this example app by leveraging Heroku and other Salesforce technologies.

– JSON

lightning

How to pass the Advanced Salesforce Developer Exam (DEV501)

Tags

, , , ,

It is quite simple. You just have to understand Apex, Visualforce, Testing, Debugging, and the Salesforce Development Lifecycle inside and out. And know that when you pass you will be able to develop any Salesforce application that you dream up!

I have been working with Salesforce for two years but wrote my first line of Salesforce code just one year ago…at Dreamforce! So how, might you ask, did I pass this ridiculously difficult development exam with only one year of Salesforce coding experience?

I made Salesforce and Development training my top priority. Specifically I –

  1. Implemented a Salesforce 1 app the week after Dreamforce’13 by following Getting Started with Mobile Design Templates guides
  2. Attended DEV401 training in Dallas Texas – thanks to Andy Anderson for his awesome instruction. This guy really cares about his students’ success
  3. Attended DEV501 training in San Mateo – thanks to Dennis Goldwater who taught me how to leverage my Java knowledge in Salesforce. Dennis is a true developer in all regards – and a really smart guy.
  4. Memorized Universal Containers Schema!
  5. Memorized Salesforce Data Access Model
  6. Memorized Approval Workflow Process
  7. Memorized Workflow Process
  8. Practiced Declarative Programming in my Development Org
  9. Watched “Apex” premier training video
  10. Watched “Managing Development with Force.com” premier training video
  11. Watched “Visualforce Controllers” premier training video
  12. Practiced Apex and Visualforce using the Workbooks on DeveloperForce
  13. Read many blog articles from Sara Morgan, Andrew Fawcett, Jeff Douglas, Matt Lacey, Keir Bowden (aka Bob Buzzard)
  14. Worked through many awesome, helpful, indispensable Visualforce recipes in the Visualforce Development Cookbook which you can get here
  15. Watched “Force.com Design Patterns” on pluralsight.com
  16. Watched “Introduction to Visualforce” on pluralsight.com
  17. Watched “Force.com and Apex Fundamentals for Developers” on pluralsight.com
  18. Watched “Encapsulation and SOLID” on pluralsight.com
  19. Watched “SOLID Principles of Object Oriented Design” on pluralsight.com
  20. Developed production code over four three-week sprints with heavy emphasis on the metadata api’s by leveraging Andrew Fawcett’s Metadata Wrapper library
  21. Ported my Salesforce 1 app from JQuery to AngularJS which frigg’n rocks and deserves it’s own blog down the road
  22. (Oh, and I) failed DEV501 once before which is unfortunately required for most of us.

If you want to follow a similar study route but don’t have time to do all then I would propose at least the following: 2, 3, 8 9, 10, 11, 13, 14, 15, and 20. If “20” is not feasible given your job, then develop on your own time. Make up an app, develop it, bulkify it, test it, migrate it, version control it, etc. Who knows, “it” might be the next killer Salesforce App.

The day before the test, I suggest that you re-read the DEV501 workbook from the training class cover to cover.

The night before the test, I suggest you memorize two sheets worth of information where you struggle the most. I will share what I memorized but you may want to tailor this based on your strengths and weaknesses. Here are mine –

sheet2

sheet1

When you take the test, brain dump your two sheets of information on the paper provided, take a deep breath, and then press start. It worked for me and I hope it works for you.

Good Luck,

JSON

Evaluating a Salesforce formula from a custom Visualforce page

Tags

, , , ,

Salesforce will evaluate your formulas as part of a DML operation but often times you need to display the formula results to the end user *before* the save. In this blog, I will talk about this challenge with a simple example and show one way to solve it.

Let’s create a simple custom object called adder__c that has a number field named LHS__c, a number field named RHS__c, and a formula named SUM__c which simply adds the left-hand-side (LHS) and right-hand_side (RHS) numbers – pretty simple?

So here is how Salesforce renders the editor:

nativeAdder

And here is the detail page after the save (take note of the sum):

nativeAdderResults

This is great unless the user would like to see the SUM *before* pressing save. For this case, we can create a custom Visualforce page that calculates the sum in real-time. That is as soon as the LHS or RHS is entered or changes, then the SUM field is calculated. Here is a screenshot of that custom page:

customAdder

Now, this Visualforce page could replicate the formula logic, but I wouldn’t be writing a blog about that 🙂 Keep in mind that we are using simple math for explantation purposes but in most cases the Salesforce formula will get rather complex. So for these cases, we would want to evaluate the Salesforce formula in the CRM. And that is exactly what is happening in this custom Visualforce page, Apex class, and Apex trigger.

First, here is the Visualforce page which triggers a VFRemoting call anytime the LHS or RHS changes along with a callback to update the SUM:

<apex:page standardController="Adder__c"
           extensions="AdderExt"
           id="thePage">
    <apex:form id="theForm">
        <apex:pageBlock title="Custom Adder" mode="edit" id="thePageBlock">
            <apex:pageBlockButtons >
                <apex:commandButton value="Save" action="{!save}"/>
                <apex:commandButton value="Cancel" action="{!cancel}"/>
            </apex:pageBlockButtons>
            <apex:pageBlockSection id="theAdder" columns="1">
                <apex:inputField value="{!Adder__c.LHS__c}" id="LHS" onchange="remoteCallout()"/>
                <apex:inputField value="{!Adder__c.RHS__c}" id="RHS" onchange="remoteCallout()"/>
                <apex:outputField value="{!Adder__c.SUM__c}" id="SUM"/>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>

    <script>
        function remoteCallout()
        {
            LHS = defaultVal(document.getElementById('{!$Component.thePage.theForm.thePageBlock.theAdder.LHS}').value, 0);
            RHS = defaultVal(document.getElementById('{!$Component.thePage.theForm.thePageBlock.theAdder.RHS}').value, 0);

            Visualforce.remoting.Manager.invokeAction(
                '{!$RemoteAction.AdderExt.evaluateAdder}',
                LHS, RHS,
                function(adder) { remoteCallback(adder); },
                {escape:false}
            );
        }

        function remoteCallback(adder)
        {
            document.getElementById('{!$Component.thePage.theForm.thePageBlock.theAdder.SUM}').innerHTML = adder.SUM__c;
        }

        function defaultVal(testVal, defaultVal)
        {
            if (testVal == "")
                return defaultVal;
            return testVal;
        }
    </script>
</apex:page>

Second, here is the Apex controller extension which sets up a “fake save”, evaluates the adder, and returns the results to the Visualforce page:

public with sharing class AdderExt
{
    private final sObject mysObject;

    // boolean flag which informs our trigger to abort
    public static boolean fakeSave = false;

    // used to store evaluation results from trigger
    public static Adder__c evaluationResults = null;


    // The extension constructor initializes the private member
    // variable mysObject by using the getRecord method from the standard
    // controller.
    public AdderExt(ApexPages.StandardController stdController)
    {
        this.mysObject = (sObject)stdController.getRecord();
    }

    public String getRecordName()
    {
        return 'Hello ' + (String)mysObject.get('name') + ' (' + (Id)mysObject.get('Id') + ')';
    }

    @RemoteAction
    public static Adder__c evaluateAdder(Integer LHS, Integer RHS)
    {
        fakeSave = true;

        Adder__c evaluateMe = new Adder__c();
        evaluateMe.LHS__c = LHS;
        evaluateMe.RHS__c = RHS;

        try
        {
            insert evaluateMe;
        }
        catch(DmlException ignoreMe)
        {
            // expected exception as fake save throws to prevent DML
        }

        System.debug('evaluationResults='+evaluationResults);

        return evaluationResults;
    }
}

Lastly, here is the trigger which sets the evaluation results in the controller and aborts the DML:

trigger AbortInsert on Adder__c (before insert)
{
    System.debug('****************************************');

    List&amp;amp;lt;Adder__c&amp;amp;gt; adders = Trigger.new;
    for (Adder__c adder: adders)
    {
        if(AdderExt.fakeSave == true)
        {
            // AdderExt controls an editor that should only work on one row at a time
            System.assertEquals(adders.size(), 1);

            // Provide the evaluation results to the line item controller
            AdderExt.evaluationResults = adder;

            System.debug('adder='+adder);

            // Abort the save
            adder.addError('Throwing error to prevent DML for this formula only evaluation');
        }
    }
}

There you have it.

JSON

MavensMate and Summer’14 GOTCHA!!

Tags

, ,

If MavensMate fails to compile your apex classes in Summer’14 with the following error message…

Result: [OPERATION FAILED]: Resource Not Found. Response content: [{u’errorCode’: u’NOT_FOUND’, u’message’: u’Provided external ID field does not exist or is not accessible: ‘}]

…then you can resolve this by setting the MavensMate project mm_compile_with_tooling_api setting to false, e.g. click MavensMate > Setting > Project and then add or update the following:

“mm_compile_with_tooling_api” : false

Please respond to this blog post if you have a better solution.

Thanks,
json

All about Salesforce Usage Metrics

Tags

, , ,

Usage metrics was released as a pilot feature in Spring’14 and it ROCKS!!  After previewing the release notes for the first time, I thought to myself ho hum.  And then I enabled usage metrics, and then I installed the visualization app, and then I parsed the metrics into a custom object, and then I started writing reports, and THEN I realized how incredibly valuable this feature is to product managers, UX engineers, development teams, executives, and I’m sure many other personas – AND this is just version one.  OK – if i have your interest then read on and learn how to enable usage metrics, connect ORGs, quickly get Usage Metrics up and running with the visualization app, write apex to parse metrics, and start writing those oh so cool reports to really understand how your users are (or are not) using your app – plus gain valuable insights into the performance of your app!

The first step is to open an “AppExchange & Feature Requests” ticket with salesforce support and ask them to enable Usage Metrics in your app.  Something like,

1. Org ID: 000934FA2384
2. Name of the feature or limit: Usage Metrics
3. Duration that the requested change should remain in effect: forever and ever

If you have a Technical Evangelist then give him or her the heads up because the support guys will likely contact for approval.  This ORG is called the Reporting Org.

In a separate ORG or in the Reporting ORG, you will need to enable the environment hub.  In my case, I did this in the Reporting Org as we already have too many partner ORGs to manage.  You can find more Environment Hub details here.

Now, you need to connect your packaging ORG to the environment hub (which might also be your reporting org).  To do this, simply click on the environment hub tab, click connect organization, enter your cred’s for your packaging org, and voilà.  Now, you will start receiving metrics from your customers ORG’s (and from your ORG’s).  Here is a diagram to illustrate the ORG’s in play keeping in mind that the environment hub and reporting org might be one and the same:

Topology

 

You will get one metric data file per day (actually a row in the MetricsDataFile native salesforce object).  The best way to get up and running is to install the Usage Metrics Visualization App from App Exchange here.  This app can read the MetricsDataFile and create charts to show you the top 5 accessed visual force pages and other like high level metrics.  Here is a screenshot that shows the top five visual force page access from one org:

Analytics

 ( click the chart )

This might be enough for you, but if you want more then you might consider parsing the information from the MetricsDataFile into custom objects so that it is easy to write dashboards and reports.  You can do this with a batch or schedulable apex.  At the high level,

Batch

And at the more detailed level, here is a starter apex class to get you going which must be set to API v30 (or higher for those in the future):

global class parseUsageMetrics implements Database.Batchable
{
    public String query;

    global Database.QueryLocator start (Database.BatchableContext BC)
    {
        return Database.getQueryLocator(query);
    }

    global void execute (Database.BatchableContext BC, List fileList)
    {
        List<Daily_Customer_Metric__c> newMetrics =
            new List<Daily_Customer_Metric__c> ();

        for (MetricsDataFile file : fileList)
        {
            for (String dataEntry : (file.MetricsDataFile).toString().split('\n'))
            {
                // skip blank lines
                if(dataEntry.length() > 0)
                {
                    // Remove quotations in each field value
                    String[] metricsValues = dataEntry.remove('"').split(',');
                    String OrgId = metricsValues[0];
                    String OrgName = metricsValues[1];
                    String MetricName = metricsValues[4];
                    Integer MetricCount = Integer.valueOf(metricsValues[5]);

                    if(file.MetricsType == 'Visualforce')
                    {
                        Integer UniqueUsers = Integer.valueOf(metricsValues[6]);
                        Integer AvgLoadTime = Integer.valueOf(metricsValues[7]);
                    }

                    // insert into custom object(s) and WATCH OUT FOR GOVERNOR LIMITS!!
                }
            }
        }
    }

    global void finish (Database.BatchableContext BC)
    {
        // Do nothing
    }
}

This class can be launched with the following anonymous apex to get metrics from the last 10 days but keep in mind that you might not yet have 10 days of metrics:

parseUsageMetrics uMetric = new parseUsageMetrics();
uMetric.query =
    'SELECT MetricsDataFile, NamespacePrefix, MetricsStartDate, MetricsType, MetricsDataFileLength FROM MetricsDataFile WHERE MetricsStartDate = LAST_N_DAYS:10 ORDER BY MetricsStartDate';
database.executebatch(uMetric, 10);

Assuming the batch was successful then your custom object(s) should be populated with likely massive amounts of usage metric data.

Now, you can write reports and dashboards to slice and dice the data in any number of ways to get insights into exactly how your users are using your app with this uber-cool Usage Metrics feature.

What are you going to do with this information?

-JSON Hammerle

10 reasons Sublime + MavensMate kicks the panties off Eclipse as a Salesforce IDE

Tags

, , , , ,

OK, I was a long time Eclipse user (10+ years) until my colleagues visiting from Bolivia showed me the real deal. Now, I have only been working with Sublime + MavensMate for about three weeks but already I can come up with, at least, ten reasons why this duo kicks the panties off Eclipse as a Salesforce IDE. Here they are off the top of my head:

1. Sublime starts up much faster than Eclipse – no waiting
2. There is a command (Ctrl + Shift + P) for everything you want to do
3. The user experience for running unit tests and reviewing the results is way better
4. Ditto for running anonymous apex
5. Snippets make intellisense really intelligent and powerful
6. Packages are a snap to install
7. No jumping through hoops to connect your project to GIT and Salesforce
8. Multiple selections are cool and mass editing is better
9. Switching projects is instantaneous and working with multiple projects no prob
10. MavensMate has a built in Arcade to keep you busy while waiting for your unit tests to finish. How eff’ing cool is that?

I know there are many more reason that I have yet to find – and I know *YOU* probably have a few more reasons to add to the list as a reply to this blog.

– JSON