Platform Encryption

Summer time, but I do not want to miss the opportunity so share something even if this post is sorter than usual.

A year ago I completed Platform Encryption Trailhead  but I have started working with it few months ago.

What is Platform Encryption and Why do we need it?

You can find more information here, but basically it would allow us to protect some data, so that, it would not be visible. However this does not mean that the data is stored in database with a mask “****”. It is going to be saved as usual, but if your user doesn’t have certain rights, it will not be visible, and he would have a mask “****” as view effect .

Ok, let’s explain a bit better. If I need a field encrypted, because it is a password, I will use the existing Text (encrypted) field type. If I need to have my data visible in my organization, but if this is stoles, for instance, it should not be visible, then, I need to use existing field types, but enable Platform Encryption on those that have sensible data.

How to enable Platform Encryption?

First of all, you need to have rights, to manage encrypted data. For that, the first thing to do is to create a Permission Set, under System Mode, select Manage Encryption Keys.  Remember to assign it to your user.

If you need to see also encrypted data, you need to add to the same Permission Set, or a new one, View Encrypted Data right.

Then, you need to go to Setup, under Administration Setup you can find Platform Encryption

Captura de pantalla 2017-08-11 a las 18.36.28

The very first time you also need to generate a Tenant Secret just clicking on the button.

Captura de pantalla 2017-08-14 a las 11.40.38

Captura de pantalla 2017-08-14 a las 11.44.24

 

The last step is to decide what you need to encrypt. Right now Salesforce allows you to encrypt certain fields on certain Standard Objects.

Leads fields are in beta

And you can also encrypt custom fields on Standard and Custom objects as long as their field type are: Email, Phone, Text, Text Area, Long Text Area, URL, Date or Date/Time.

But before encrypt any field you need to be aware that there are certain limitations. Maybe one of the most important one is that you cannot filter, order or group by an encrypted field, so that your code can fail once you enable this feature in your organization.

 

Reviewing the code

Now it is time to review your current code to check if there is something failing now. The easiest example of failure is this SOQL in your code.

Select Id, Name From Account Where Name = :accName;

But now it fails at compilation time. And if you turn Encryption off, it continues failing … so what should you do at this point?

If your next step is to move to dynamic SOQL, it’s a good try. Now it saves, but it would fail at run time.

You need to find another way to get the information. Salesforce suggests to use SOSL, and this is an easy solution, as we only need to move to this piece of code. So let’s go for it and solve this issue.

Account acc = new Account();Account acc = new Account();        
acc.Name = 'PE Blog Test';        
insert acc;         

List<List<Account>> accList = [FIND 'PE Blog Test' 
                               IN ALL FIELDS 
                               RETURNING Account(Id, Name)];        

System.debug('Testing: ' + accList);

Captura de pantalla 2017-08-14 a las 12.08.35

Reviewing Unit Tests

Ok, this section sound similar to the previous one. Yes, you are right, but I wanted to highlight it here, because I was not used to work with SOSL and I found something interesting to share.

If I have below method:

public static testMethod void testAcountsWithSOSL() 
{ 
   List<Account> accList = [Select Id from Account]; 
   System.assertEquals(0, accList.size(), 'Error: There should not be any account.');
   
   Account acc = new Account(Name = 'Test'); 
   insert acc;
   accList = [Select Id from Account]; 
   System.assertEquals(1, accList.size(), 'Error: There should be an account.'); 

   List<List<Account>> accList2 = [FIND 'Test' IN ALL FIELDS RETURNING Account];
   System.assertEquals(1, accList2.get(0).size(), 'Error: There should be an account. And result is = ' + accList2); 
}

And run the test, the result is the below one:

Captura de pantalla 2017-08-14 a las 12.14.31

Exactly, it fails because accList2 is empty although I was expecting to get an element.

Doing some researches, I found this article about unit test. So it seems they provide a Test method that allows you to insert the Id of the record you need to look for and get it back. So that, the solution would be this:

public static testMethod void testAcountsWithSOSLFollowingGuide() 
{ 
   List<Account> accList = [Select Id from Account]; 
   System.assertEquals(0, accList.size(), 'Error: There should not be any account.');

   Account acc = new Account(Name = 'Test'); 
   insert acc;
   accList = [Select Id from Account]; 
   System.assertEquals(1, accList.size(), 'Error: There should be an account.'); 

   Id [] fixedSearchResults= new Id[1]; 
   fixedSearchResults[0] = acc.Id;
   Test.setFixedSearchResults(fixedSearchResults); 
   List<List<SObject>> searchList = [FIND 'Test' IN ALL FIELDS RETURNING Account(Id, Name)]; 
   System.assertEquals(1, searchList.get(0).size(), 'Error: There should be an account. And result is = ' + searchList);    
}

Captura de pantalla 2017-08-14 a las 12.19.46

Ok, so doing in this way, you are saved. However, be aware that your tests have the SeeAllData=false and you do not expect to get data from your org, other wise, it would fail too.

I hope this post help you to be closer to understand what you need to do before enabling Platform Encryption in your org.

Picklist dinámicos en Lightning Components

Mis últimos post técnicos en Español eran bastante básicos. Explicaba cómo crear páginas visualforce, controllers, etc. Sin embargo, desde entonces, Lightning Experience y Lightning Components han cogido el protagonismo en Salesforce.

Así que en esta entrada voy a explicar como crear una lista desplegable y poder determinar su valor por defecto desde App Builder, de forma que cada usuario puede decidir qué valor le conviene.

Caso de Uso:

Como usuario, me gustaría tener un lightning component que me muestre los eventos de Salesforce a partir de una ciudad elegida y decidir el valor por defecto de esa lista desplegable.

Captura de pantalla 2017-08-02 a las 18.28.06

Pasos:

Lista desplegable con valores fijos

Comencemos creando un componente con determinadas ciudades pero sus valores son fijos. Para poder testearlo, lo mostraré en la pestaña Home de mi App Salesforce Events.

Para no alargar el post pondré una breve explicación de lo más importante en el código

Captura de pantalla 2017-08-03 a las 16.58.42

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" 
      implements="flexipage:availableForAllPageTypes" 
      access="global">
    
   <!-- Lista de ciudades sobre la que iterar -->
   <aura:attribute name="cities" type="List" />  
   
   <!-- Valor seleccionado para poder usarlo más adelante.-->
   <!-- Indicamos que el valor por defecto es "Madrid" mediante hardcode -->
   <aura:attribute name="selectedValue" type="String" default="Madrid"/> 
   
   <!-- handler donde llamamos al método que me devuelve los valores de la lista -->
   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />    
   
   <!-- Usando Lightning Design System, creamos la lista desplegable -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="{!v.selectedValue}">    
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration> 
   </lightning:select>
   
</aura:component>

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)    
   {
      //asigno a la variable cities de mi component los nombres de las ciudades
      component.set('v.cities',['London','Madrid','Paris','San Francisco']);
   }
})

Lista desplegable con valores dinámicos

El siguiente paso es recuperar de la base de datos los valores de la ciudades que tenemos. Según la imagen de más arriba, de la lista desplegable desaparecería San Francisco.

Captura de pantalla 2017-08-03 a las 17.10.17

Para ello modificaría el Controlador JavaScript que llamaría a un Controlador en Apex dónde hacemos la búsqueda en base de datos.

SalesforceEventsController.cls

//Esta clase puede ser también global
public with sharing class SalesforceEventsController
{
   //Recuerda añadir la anotación @AuraEnabled 
   //para el que método sea visible en el componente
   @AuraEnabled public static List<String> getCityNames()
   {        
      List<String> cityNames = new List<String>();               
      
     //TODO: Recuerda SOC por lo que una SOQL debería hacerse en otra clase 
     //destinada a ello.
     for(City__c c : [Select Id, Name From City__c])        
      {            
         cityNames.add(c.Name);        
      }                
      return cityNames;    
   }
}

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)
   {        
      var action = component.get("c.getCityNames");
      
      //asigno a la variable cities de mi component 
      //los nombres de las ciudades que he recuperado del controlador apex
      action.setCallback(this, 
                         function(a){
                            component.set("v.cities", a.getReturnValue());
                         });
      $A.enqueueAction(action);    
   }
})

Lista desplegable con valores dinámicos y Tabla con información

El siguiente paso es mostrar los valores de los eventos de Salesforce a partir de una selección en la lista desplegable.

SalesforceEventsController.cls

@AuraEnabled  
public static List<SalesforceEvent__c> getEventsByCity(String cityName)
{
    City__c theCity = [Select Id From City__c Where Name = :cityName Limit 1];        
    Id theCityId = theCity.Id;                
    List<SalesforceEvent__c> sEvents = new List<SalesforceEvent__c>();        
    
    //TODO: Recuerda SOC por lo que una SOQL debería hacerse en otra clase 
    //destinada a ello.
    for(SalesforceEvent__c e : [Select Id,EventName__c,EventDate__c From SalesforceEvent__c Where City__c = :theCityId])        
    {            
        sEvents.add(e);        
    }        
    return sEvents;    
}

En este punto me di cuenta que el refresco iba a ser más complicado de lo que tenía pensado ya que tuve que crear un componente hijo para la parte de los Salesforce Events, y un evento para poder hacer el refresco. Os dejo el enlace que usé como referencia.

SalesforceEventsEvent.evt

<aura:event type="APPLICATION">
   <!-- A partir de una ciudad seleccionada, calculo los eventos -->
   <aura:attribute name="citySelected" type="String" />
</aura:event>

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" implements="flexipage:availableForAllPageTypes" access="global">
   <!-- Attributes set on Design Page-->    
   <aura:attribute access="global" name="headerText" type="String" /> 
   <aura:attribute access="global" name="defaultCity" type="String" />        
   
   <aura:attribute name="cities" type="List" />    
   <aura:attribute name="selectedValue" type="String" default="Madrid"/>    

   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />

   <!-- onchange no funciona sin un handler relacionado con value el tag option -->
   <aura:handler name="change" value="{!v.value}" action="{!c.cityChange}"/>        

   <!-- Usaremos onChange para llamar a la función js que refrescará la lista -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="selectedValue" onchange="{!c.cityChange}"> 
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration> 
   </lightning:select>        

   <h1>Salesforce Events:</h1>    
   <!-- Registro el evento y llamo al nuevo componente hijo para listar los eventos de Salesforce -->
   <aura:registerEvent name="appEvent" type="c:SalesforceEventsEvent"/>    
   <c:SalesforEventList />

</aura:component>

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)    
   { 
      var action = component.get("c.getCityNames"); 
      action.setCallback(this, function(a) 
      { 
         component.set("v.cities", a.getReturnValue());        
      });        
      $A.enqueueAction(action);    
   },        
   cityChange : function(component, event)    
   {        
      //Determino el nuevo valor por defecto que es el elegido
      component.set('v.selectedValue', component.find("mySelect").get("v.value"));        
      component.find("mySelect").get("v.value");
      
      //Paso al atributo citySelected del event el nuevo valor seleccionado
      var appEvent = $A.get("e.c:SalesforceEventsEvent");        
      appEvent.setParams({"citySelected" : component.get("v.selectedValue")});        
      appEvent.fire();    
   }
})

SalesforEventList.cmp

<aura:component controller="SalesforceEventsController" implements="flexipage:availableForAllPageTypes" access="global">

   <aura:handler event="c:SalesforceEventsEvent" action="{!c.loadEvents}"/>
   <aura:attribute name="cityToFilter" type="String" />    
   <aura:attribute name="sevents" type="SalesforceEvent__c[]" />         

   <aura:iteration items="{!v.sevents}" var="theEvent">        
      <li>{!theEvent.EventDate__c} - {!theEvent.EventName__c }</li>    
   </aura:iteration> 
</aura:component>

SalesforEventList.cmp

({ 
   loadEvents : function(component, event)    
   {        
      var citySentFromEvent = event.getParam("citySelected");
      component.set("v.cityToFilter", citySentFromEvent);            

      var action = component.get("c.getEventsByCity");        
      action.setParams({ cityName : component.get("v.cityToFilter") }); 
      action.setCallback(this, function(a) 
      { 
         component.set("v.sevents", a.getReturnValue());        
      });        
      $A.enqueueAction(action);                
   }
})

Determinar un valor por defecto desde App Builder

La idea aquí es dar la opción de poner el valor por defecto cómo parte de la configuración. Para ello usaremos la funcionalidad Dynamic PickList for Lightning Components.

Añadiremos 2 atributos a nuestro componente. Uno sera un texto con información y el segundo nuestra lista desplegable.

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" 
                implements="flexipage:availableForAllPageTypes" 
                access="global">
   
   <!-- Atributos para la Design Page--> 
   <!-- Este primer atributo no es requerido para la lista desplegable -->
   <aura:attribute access="global" name="headerText" type="String" /> 
   <!-- En vez de tener el valor por defecto con hardcode, usamos el atributo defaultCity -->
   <aura:attribute name="selectedValue" type="String" default="{!v.defaultCity}"/>

   <aura:attribute name="cities" type="List" />     
   <aura:attribute name="selectedValue" type="String"/>    
   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />    

   <!-- Utilizo como valor para value el atributo defaultCity -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="selectedValue">
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration>
   </lightning:select>
...
</aura:component>

A continuación debemos crear nuestro design para añadir los nuevos campos a la Design Page.

SalesforceEventsComponent.design

<design:component label="Salesforce Events Cities">
   <design:attribute name="headerText" 
                     label="Header Text" 
                     description="Text that will appear in the header when the component is displayed" 
                     default="City" />    
   <!-- datasource nos ayuda a lincar 
        el atributo con una clase apex de dónde extraeremos 
        la información que necesitamos -->
   <design:attribute name="defaultCity" 
                     label="Default City PickList" 
                     datasource="apex://SalesforceEventsCityValuePickListDefault" />
</design:component>

Por último, debemos crear la clase apex lincada a nuestro atributo.

Aunque en la documentación aparece como global, si la hacemos pública funciona igualmente.

Esta clase extiende VisualEditor.DynamicPickList y aunque tiene cuatro métodos para sobre escribir, sólo son requeridos dos. Uno determina el valor por defecto en la lista desplegable, y el otro nos muestra los valores.

SalesforceEventsCityValuePickListDefault.cls

public class SalesforceEventsCityValuePickListDefault extends VisualEditor.DynamicPickList
{ 
   public override VisualEditor.DataRow getDefaultValue()    
   {        
      VisualEditor.DataRow defaultValue = new VisualEditor.DataRow('defaultLabel', 'defaultValue');        
      return defaultValue; 
   }        

   public override VisualEditor.DynamicPickListRows getValues()    
   { 
      VisualEditor.DataRow defaultRow = new VisualEditor.DataRow('defaultLabel', 'defaultValue'); 
      VisualEditor.DynamicPickListRows myRows = new VisualEditor.DynamicPickListRows(); 
      myRows.addRow(defaultRow);              

      List<String> cities = SalesforceEventsController.getCityNames();
      for(Integer i=0; i<cities.size(); i++)
      {
         VisualEditor.DataRow newRow = new VisualEditor.DataRow(cities.get(i), cities.get(i));
         myRows.addRow(newRow);
      }        
      return myRows; 
   }
}

Como muestro en la imagen de abajo, si abrimos el App Builder y seleccionamos nuestro Lightning Component, veremos los dos atributos que definimos en SalesforceEventsComponent.design siendo uno de ellos la lista desplegable con los valores que hemos recuperado de base de datos.

Captura de pantalla 2017-08-03 a las 20.03.46.png

Un último apunte antes de terminar el post. No estoy cubriendo error handling en mi código pero échale un vistazo a la ayuda de Salesforce para ello. Por ejemplo este y este.

Como has podido comprobar, es muy sencillo. Si eres nuevo en este tema, los pasos son fáciles de seguir. Si ya eres un experto, y has encontrado algo en el código que se puede mejorar. Dímelo y así otros podrán beneficiarse de tus conocimientos también.

Salesforce BigObjects

What is a BigObject?

Two years ago I started working with Big Objects, a Salesforce pilot that looked very interesting for me. And now that it seems that General Available time is closer, I would like to write about them.

Please, take into account that a Salesforce pilot could be discarded like Data Pipeline that it would not be GA at the end.

Big Objects is a new type of objects that Salesforce provides. Maybe the most important thing I can tell is that they do not count against the storage limit. Well, you could say that External Objects already do.

You can learn more about External Objects here.

Yes, you are right. But this time, data lives in your organisation instead of in an external storage and this means that you can work with them like with Custom / Standard objects, but if you go to Salesforce Storage screen, you would not find them.

Big Objects let you store and manage large amounts of data.

Use Case

But before starting with the explanation, let’s talk about the Use Case.

As a company that creates software, we have stories in order to develop new functionality. Before delivering them, we need to pass a code review, so that, we ensure the best to our end users.

Captura de pantalla 2017-07-07 a las 12.21.47

But how much storage should I use to keep code review records in our Production org? Should I remove them? If so, what about auditing?

BigObjects is your solution, so that, you can move old records to a new Code Review History BigObject record. Let’s focus for now on this new History object.

How to Create a BigObject?

Unfortunately you cannot do it in a declarative way. You need to define them via Metadata API and then do a deployment via cmd or workbench.

Before doing it, take into account:

  1. Indexes are required and in order to include them, you need to be on API 39.0 onwards.
  2. Once you deploy one, you cannot make any modification like amend a field type, change label, etc. However in a declarative way you can do small changes like the label and API name.
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
  <deploymentStatus>Deployed</deploymentStatus>
  <fields>
    <fullName>CanBeMerged__c</fullName>
      <label>Can Be Merged</label>
      <length>80</length>
      <type>Text</type>
    </fields> 
  <fields>
  <fullName>CodeReviewDate__c</fullName>
    <label>Code Review Date</label>
    <type>DateTime</type>
    <required>true</required>
  </fields>
  <fields>
    <fullName>Comments__c</fullName>
    <label>Comments</label>
    <length>255</length>
    <type>Text</type>
  </fields>
  <fields>
    <fullName>Employee__c</fullName>
    <label>Employee</label>
    <referenceTo>Employee__c</referenceTo>
    <relationshipName>Employees</relationshipName>
    <required>true</required>
    <type>Lookup</type>
  </fields>
  <fields>
    <fullName>Score__c</fullName>
    <label>Score</label>
    <precision>1</precision>
    <scale>0</scale>
    <type>Number</type>
  </fields>
  <fields>
    <fullName>Story__c</fullName>
    <label>Story__c</label>
    <referenceTo>Story__c</referenceTo>
    <relationshipName>Stories</relationshipName>
    <required>true</required>
    <type>Lookup</type>
  </fields>
  <indexes>
    <type>PRIMARY</type> 
    <fullName>Test1CodeReviewPK</fullName>
    <fields>
      <name>Story__c</name>
      <sortDirection>DESC</sortDirection> 
    </fields>
    <fields>
      <name>Employee__c</name>
      <sortDirection>DESC</sortDirection>
    </fields>
    <fields>
      <name>CodeReviewDate__c</name>
      <sortDirection>DESC</sortDirection>
    </fields>
  </indexes>
  <label>Test1 Code Review History</label>
  <pluralLabel>Test1 Code Reviews History</pluralLabel>
</CustomObject>

Once you deploy it into your organization, you would find something like this under BigObject entry:

I decided to name is as Test1 Code Review History because once it is created I cannot modify it, and I didn’t want to use Code Review History label for now.

Captura de pantalla 2017-07-14 a las 13.03.54.png

What can we highlight?

  1. The API name ends with __b instead of __c or __x like custom and external objects.
  2. We will not find Standard fields.
  3. We can create as many custom fields as we want but right now it only supports these field types: DateTime, Lookup, Number, Text and Long TextArea. What about the rest? Try to be creative. For instance a Checkbox can be converted into number or text.
  4. You will not find any other section like PageLayout or Buttons for now.
  5. You will not find Triggers section. It is not allowed for now.
  6. You will not be allowed to create a custom tab related to this object. That is related to previous point and the PageLayout. To visualise Big Object records, you need to create a visualforce page or a lightning component.
  7. You can determine its CRUD and FLS via Profile or Permission Sets during the deploy process or in a declarative way. However for now BigObjects can be only created and read.

Captura de pantalla 2017-07-14 a las 13.44.36

How Can I Create Records?

There are different ways to create BigObject record, like using a csv file, use APIs like Bulk API or even Async SOQL, another Pilot I will talk about in a future entry.

In any case, here, I will focus on Apex.

As I mentioned above, you can only Read or Create records, and nowadays, you cannot use simple DML operations, there is a new one: database.insertImmediate(record)

Test1CodeReview__b cr = new Test1CodeReview__b();
cr.CanBeMerged__c = 'False';
cr.CodeReviewDate__c = System.today();
cr.Comments__c = 'I found a SOQL inside of a loop.';
cr.Employee__c = 'a6j24000000fxSL';
cr.Score__c = 0; 
cr.Story__c = 'a6k24000000k9bN';
database.insertImmediate(cr);

But something pretty cool that it was not available at the beginning, it is the fact we can use this method, to also update records. Actually it works like upsert. If I make a call where the record has a primary key that doesn’t exist, it would create a new record as we can see in the below image.

Captura de pantalla 2017-07-14 a las 14.28.04

However if the value already is in the platform, it will make a modification.

Test1CodeReview__b cr = new Test1CodeReview__b();
cr.CanBeMerged__c = 'False';
cr.CodeReviewDate__c = System.today();
cr.Comments__c = 'I found a SOQL inside of a loop.';
cr.Employee__c = 'a6j24000000fxSL';
cr.Score__c = 1; //Change to 1
cr.Story__c = 'a6k24000000k9bN';
database.insertImmediate(cr);

Captura de pantalla 2017-07-14 a las 14.35.49

If I come back to the original use case, I would like to move CodeReview__c records to Test1CodeReviewHistory__b records. In order to do that I only need to retrieve the record I need and create a new BigObject entry.

CodeReview__c cr = [Select Id, CanBeMerged__c, 
                           CodeReviewDate__c, Comments__c, 
                           Employee__c, Score__c, Story__c 
                    From CodeReview__c 
                    Limit 1];

String canBeMerged = cr.CanBeMerged__c == true ? 'True' : 'False';

Test1CodeReview__b crh = new Test1CodeReview__b();
crh.CanBeMerged__c = canBeMerged;
crh.CodeReviewDate__c = cr.CodeReviewDate__c;
crh.Comments__c = cr.Comments__c;
crh.Employee__c = cr.Employee__c;
crh.Score__c = cr.Score__c; 
crh.Story__c = cr.Story__c;
database.insertImmediate(crh);

How Can I Visualise Records?

As we have already mentioned, we cannot create a custom tab related to a BigObject and show all records. However we can query BigObjects, so what about if we create a custom page for that?

First of all we would need a controller. Really simple one. We have a method to retrieve all records and a get and set to show in a list what we retrieve.

public with sharing class CodeReviewController
{
   private static List<Test1CodeReview__b> codeReviewHistoryList; 
   
   public CodeReviewController()
   {
      setCodeReviewHistoryList(calculateCodeReviewHistoryList());
   } 

   public List<Test1CodeReview__b> calculateCodeReviewHistoryList()
   {
      return [SELECT Id, CanBeMerged__c, CodeReviewDate__c, 
                     Comments__c, Employee__c, Score__c, 
                     Story__c                
               FROM Test1CodeReview__b];
   }

   public static List<Test1CodeReview__b> getCodeReviewHistoryList()
   {
      return codeReviewHistoryList;
   }

   public static void setCodeReviewHistoryList(List<Test1CodeReview__b> value)
   {
      codeReviewHistoryList = value;
   }
}

Secondly, we have the visualforce page. Something to highlight is that right now, the standardController attribute doesn’t work, so we can only create a page with a customController. 

<apex:page showHeader="true" sidebar="true" controller="CodeReviewController">
   <apex:form>
      <apex:pageBlock title="Code Review History Records">
         <apex:pageBlockSection title="Code Review History" columns="1" collapsible="false">
	    <apex:pageBlockTable value="{!codeReviewHistoryList}" var="crHistory">
               <apex:column headerValue="Ready To Merge?">
	          <apex:outputField value="{!crHistory.CanBeMerged__c}"/>
               </apex:column>
	       <apex:column headerValue="Score">
	          <apex:outputField value="{!crHistory.Score__c}"/>
	       </apex:column>
               <apex:column headerValue="Comments">
	          <apex:outputField value="{!crHistory.Comments__c}"/>
               </apex:column>
               <apex:column headerValue="Story">
	          <apex:outputField value="{!crHistory.Story__c}"/>
               </apex:column>
               <apex:column headerValue="Reviewer">
                  <apex:outputField value="{!crHistory.Employee__c}"/>
               </apex:column>
               <apex:column headerValue="Code Review Date">
                  <apex:outputField value="{!crHistory.CodeReviewDate__c}"/>
               </apex:column>
            </apex:pageBlockTable>
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

And the result is this one that includes the record created from scratch and the one that is retrieved from CodeReview__c object.

Captura de pantalla 2017-07-17 a las 9.14.52

Summary

I would like to share this table that summarise BigObjects and compare with Custom Objects. But before, I would like to also highlight that this new object type can be included in a package. You would find under CustomObject section:

Captura de pantalla 2017-07-17 a las 9.16.06

Summary Table:

Feature Custom Object Big Object
Creation Manual
Metadata
Metadata
API name myObject__c myObject__b
Track Activities
Track Field History, etc.
Options Available Options No Available
Field Types All Text ; Date/Time ; Lookup
Number ; Long Text Area
Able to edit fields Yes Yes (with restrictions)
Able to delete fields Yes No
Triggers; Field Sets; etc Options Available Options No Available
Reports Yes No
How to Populate records All CSV file
API (Bulk, SOAP)
Apex
Async SOQL
Can I amend a record? Yes Yes (with restrictions)
Can I see data creating a Tab Yes No
For free? Yes No — Talk with Salesforce
Storage It count against storage It doesn’t count against storage

Maps and Visualforce

Since I was checking Force.com documentation in order to pass Platform Developer Transition exam I had the opportunity to learn more about some topics that I don’t usually use on my daily job.

One of those are Maps, and I’m not talking about Collections (Lists, Sets and Maps). I would like to talk about locations on this post.

Till some releases ago, the only way to add a location into your visualforce page, was via JavaScript. Let’s see an example. (Bellow use case is based on this Salesforce Trailhead)

I’m working on a healthcare app, and we would like to show a map on every doctor’s record, so that, the customer would be able to find the location easily.

In order to get it, I will create a visualforce page with the map and include it in the standard layout.

JavaScript Map Version

First thing is to add the listener:

 google.maps.event.addDomListener(window, 'load', initialize);

that call an initialize function

 var map;
 
 function initialize() {
     var mapOptions = {
         center: new google.maps.LatLng(43.2616465, -2.9393368),
         zoom: 15
     };
     map = new google.maps.Map(document.getElementById("map-canvas"), 
                               mapOptions);
     loadDoctor();
 }

Where we define map properties, like latitude, longitude and zoom.

But this function call another, loadDoctor 

 function loadDoctor()
 {
     Visualforce.remoting.Manager.invokeAction(
         '{!$RemoteAction.DoctorsController.findTheDoctor}',
         '{!Id}',
         function(result, event)
         {
             if (event.status) {
                 var name = result.SurgeryName__c;
                 var lat = result.Location__Latitude__s;
                 var lng = result.Location__Longitude__s;
                 addMarker(name, lat, lng);
             } else {
                 alert(event.message);
             }
        },
        {escape: true}
     );
 }

Above function uses a RemoteAction method, so we know that a Controller is included on this page. This communication with the controller just returns information about the Doctor’s records like his place name and the Location. Maybe you are wondering what below line means.

Location__Latitude__s  and  Location__Longitude__s

Location is a Geolocation field, and, in the end, it is a Compound field, so in order to see the information, we need to decouple into its “elements”, Latitude and Longitude.

Finally, on above method, we have a call to another function that will add the marker on the google map

 function addMarker(name, lat, lng)
 {
     var marker = new google.maps.Marker
           ({
               position: new google.maps.LatLng(lat, lng),
               map: map,
               title: name
           });
 }

With this code, the result is this one:

Captura de pantalla 2016-07-30 a las 22.17.58

Apex Map Version

Is there a different way to get the same result? Yes !! Since Spring ’15 we have a new tag available on visualforce pages: apex:map and with this piece of code:

<apex:map width="100%" height="100%" mapType="roadmap" zoomLevel="15"
          center="43.2616465, -2.9393368">
    <apex:mapMarker title="{!Doctor__c.SurgeryName__c}" 
                    position="{!Doctor__c.Location__Latitude__s},
                              {!Doctor__c.Location__Longitude__s}">
    </apex:mapMarker>
</apex:map>

we can get exactly what we had on the javascript code as we can see on below image that compares both maps. Left side uses apex:maps whereas on the right side the map is created with javascripts.

Captura de pantalla 2016-07-30 a las 22.24.42

More about apex:map

Attributes

  • Height and Width could be defined as pixels or percentage
  • Center could be used with Location data, Address and also with JSON format. From my point of view, it would be easier to create a method in the controller with the data
public Map<String,Double> getDoctorsCenter()
{
    Map<String,Double> mapCenter = new Map<String,Double>
                                   {
                                      'latitude' => 43.2616465,
                                      'longitude' => -2.9393368
                                    };
    return mapCenter;
}

and use the method in the visualforce page.

<apex:map width="100%" height="100%" mapType="roadmap" 
          zoomLevel="15"
          center="{!doctorsCenter}">

Above example set geolocation with hardcode but if we have this information on data base, the code could be safer and don’t fail if there is any change on this data.

MapMarker

  • Similar as before, position attribute could be defined based on Address, JSON or using the Geolocation field. On above example, we directly used the compound sub-fields, but it would be also fine to get this information in the controller, create the map and call the method in the page as we show on center attribute.
  • If your idea is to show all doctors around your city, instead of adding an mapMarker per record, remember to use apex:repeat tag
<apex:map width="100%" height="100%" mapType="roadmap" zoomLevel="15"
          center="43.2616465, -2.9393368">
    <apex:repeat value="{!locations}" var="pos">
        <apex:mapMarker position="{!pos}"/>
    </apex:repeat>
</apex:map>
  • Another mapMarker attribute is icon that help us to show a different image than the red bubble that google offer to us. A good way to show it is using a static resource and make a call to it.
<apex:mapMarker title="{!Doctor__c.SurgeryName__c}" 
                position="{!Doctor__c.Location__Latitude__s},
                          {!Doctor__c.Location__Longitude__s}"
                icon="{!URLFOR($Resource.MapMarkers, 'myIcon.png')">
</apex:mapMarker>

MapInfoWindows

  • This is another tag to show even more information. So for instance, below code show the doctor’s name and phone if we click on the marker icon
<apex:map width="100%" height="100%" mapType="roadmap" zoomLevel="15"
          center="43.2616465, -2.9393368">
    <apex:mapMarker title="{!Doctor__c.SurgeryName__c}" 
                    position="{!Doctor__c.Location__Latitude__s},
                              {!Doctor__c.Location__Longitude__s}">
        <apex:mapInfoWindow >
            <apex:outputPanel layout="block" style="font-weight: bold;">
                <apex:outputText >{!Doctor__c.SurgeryName__c}
                </apex:outputText>
            </apex:outputPanel>
            <apex:outputPanel>
                <apex:outputText >{!Doctor__c.Phone__c}
                </apex:outputText>
            </apex:outputPanel>
        </apex:mapInfoWindow>
    </apex:mapMarker>
 </apex:map>

 

Captura de pantalla 2016-08-02 a las 17.58.24.png

JavaScript vs apex:map

  1. Unfortunately, we cannot create a page with map tag in a Developer Edition, however JavaScript is available on Developer editions. But if you want to give it a try, create your own Trial edition.
  2. As JavaScript is executed on the client side, the map is loaded quicker than the one that use apex:map.
  3. On the other side, Salesforce takes care of any apex tag so any change on code versions, or browser, etc. that could make the code fail, Salesforce will deal with it.
  4. JavaScripts needs a method in the controller in order to get Location information dynamically and if we want to show the map inside a Standard layout, we must define the RemoteAction method as global that makes you define the class as global as well. This is not important till you add the code inside a package. As a reminder, global classes are available to everybody that install your package and maybe this is not your desire.
  5. RemoteAction is not able to get parameters from url. So I cannot do something like
Id currentDoctorId = controller.getId()

and then use it on the RemoteAction method. However,  in our case, we can use directly the Id of the record on the visualforce page as a workaround:

Visualforce.remoting.Manager.invokeAction(
         '{!$RemoteAction.DoctorsController.findTheDoctor}',
         '{!Id}',
         function(result, event){....});

Resources

  1. Apex:map Salesforce documentation
  2. Github Repository with code used on above examples

Mi primera Visualforce IV – Controladores II

¡Cuánto tiempo! Pues si, en Enero fue mi último post en Español y 11  meses después vuelvo a escribir una entrada en nuestra lengua materna. Y es que últimamente he estado un pelín ocupada con mi bebe de 7 meses.

Esta será la última entrada con el título Mi primera Visualforce ya que terminaremos de cubrir conocimientos básicos. Pero no significa que no seguiré escribiendo en Español. Seguid atentos porque publicaré cosas.

¡Pero vamos al lío! Todos los objetos tienen asociados un controlador estándar que le proporciona funcionalidad (Échale un vistazo a esta entrada). Además, podemos crear controladores personalizados para extender y dar más potencia a nuestra página.

Además los controladores pueden tener un único elemento como argumento, como expliqué en mi última entrada, ó a una lista de registros.

Controlador asociado a varios registros

 

Controlador Estándar

Como recordarás, para usar el controlador estándar, sólo tienes que usar el atributo standardController del tag apex:page

<apex:page standardController="Account">
</apex:page>

Entonces, ¿cuál es la diferencia? Como supondrás, necesitaré una variable para poder almacenar todas las cuentas, y después iterar sobre ella. Para ello, apex:page ofrece otro atributo recordsetVar cuyo valor será la lista de registros. Una vez que la tengamos, sólo tenemos que mostrar los campos que deseemos de nuestra cuenta. En el código de más abajo, usaremos el tag apex:repeat aunque podríamos haber usado también apex:datatable como en posts anteriores.

<apex:page standardController="Account" recordsetVar="records">
   <apex:form >
      <apex:pageBlock title="Listado de Cuentas">
         <apex:pageBlockSection columns="1">
            <apex:repeat value="{!records}" var="acc">
               <apex:outputField value="{!acc.Name}"/>
            </apex:repeat>
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

Siendo este el resultado

Captura de pantalla 2016-11-07 a las 18.52.19.png

Controlador Personalizado

¿Y cómo puedo conseguir lo mismo pero con un controlador creador por mi mismo? Sólo tienes que seguir estos pasos:

Crear un Controlador en Apex

Una clase en Apex sencilla. Pero ten en cuenta lo que explicamos en el último post sobre nomenclatura, sharings, etc. Lo único que debemos tener en cuenta es el constructor. Esta vez, en vez de utilizar ApexPages.StandardController, usaremos la clase ApexPages.StandardSetController que nos permitirá recuperar la lista de registros seleccionados (por ejemplo) con el método getRecords de forma similar que el controlador de único registro nos ofrecía el método getRecord. Pero además, en este caso, tenemos métodos para facilitar la paginación cómo getHasNext ó getHasPrevious.

public with sharing class AccountListController
{
   public AccountListController(ApexPages.StandardSetController controller)
   {
      //TODO añadir código
   }
}

Añadir método para recuperar la lista de cuentas

Muy sencillo. Añadimos la variable registroCuentas con sus métodos get y set. Por otro lado, añadimos un método que recuperará las listas que tenemos en base de datos por medio de una SOQL calcularCuentas. Y por último, en el constructor, rellenamos la variable privada con la información.

public with sharing class AccountListController
{
   private List<Account> registroCuentas;
 
   public AccountListController(ApexPages.StandardSetController controller)
   {
      setRegistroCuentas(calcularCuentas());
   }
 
   public List<Account> calcularCuentas()
   {
      try
      {
         return [Select Id, Name From Account];
      }
      catch(Exception ex)
      {
         ApexPages.addMessages(ex);
         return null;
      }
   }
 
   public List<Account> getRegistroCuentas()
   {
      return registroCuentas;
   }
 
   public void setRegistroCuentas(List<Account> value)
   {
      registroCuentas = value;
   }
}

Modificar la página para usar el Controlador

En este último paso sólo tenemos que añadir el atributo extensions cuyo valor es el nombre del controlador personalizado que hemos creado. Además, debemos usar nuestro método como valor en el atributo recordSetVar.

<apex:page standardController="Account" 
           extensions="AccountListController" 
           recordsetVar="registroCuentas">
   <apex:form >
      <apex:pageBlock title="Listado de Cuentas Personalizado">
         <apex:pageBlockSection columns="1">
            <apex:repeat value="{!registroCuentas}" var="acc">
               <apex:outputField value="{!acc.Name}"/>
            </apex:repeat>
         </apex:pageBlockSection>
      </apex:pageBlock>
   </apex:form>
</apex:page>

captura-de-pantalla-2016-11-07-a-las-19-23-30

Como puedes ver, el look & feel de la página es igual que la página que usa el controlador estándar.

A partir de ahí, eres tu quien pone los límites en una página.

Alternativa

Por último, si nuestro caso de uso es simplemente mostrar la lista de Cuentas, pero no nos piden nada más (por ejemplo mostrar sólo unos campos), podemos usar el tag apex:enhancedList y no reinventar la rueda. Con esta línea de código, podemos mostrar todas las cuentas de nuestra organización junto con sus enlaces de editar, borrar, seleccionar, etc y sin ningún esfuerzo.

<apex:page >
   <apex:enhancedList type="Account" 
                      height="300" 
                      rowsPerPage="10" 
                      id="AccountList" />
</apex:page>

 

captura-de-pantalla-2016-11-08-a-las-17-45-17

Esta ha sido una entrada sencilla en la que hemos aprendido

  • Mostrar una lista de Cuentas por medio del controlador Standard
  • Mostrar una lista de Cuentas via un controlador personalizado
  • Mostrar una lista de Cuentas usando el tag apex:enhancedList 

Nos vemos en el próximo post.

Mi primera Visualforce III – Controladores

En este post vamos a continuar desde dónde lo dejamos. En la última entrada en Español hablábamos de cómo crear una página visualforce utilizando únicamente un Controlador Standard. Si todavía no has tenido la oportunidad de leerla sería interesante que lo hicieras antes de continuar. Aquí tienes el link.

Controlador Personalizado

Hasta ahora sólo hemos usado el controlador Standard asociado al objeto que usábamos en la página, Account. Y con él hemos conseguido hacer muchas cosas. Entonces, ¿por qué necesito crear uno? Con este tipo de controladores vamos a extender la funcionalidad, de forma que nuestra página puede llegar a ser más potente si cabe.

Tenemos dos tipos de controladores personalizados:

  • Asociados a una lista de registros de un objeto
  • Asociados a un único registro de un objeto

Dejaremos la lista de registros para un blog futuro y hoy vamos a centrarnos en qué hacer con un único registro.

Único Registro

Estos controladores se usarán cuando quiero realizar una acción asociada a un registro en concreto.

Para empezar sólo tengo que crear una clase Apex

Setup | Develop | Apex Classes

Y hacer click en el botón New.

A continuación sólo tienes que escribir estas líneas de código y guardar:

public with sharing class AccountController
{
    public AccountController(ApexPages.StandardController controller){}
}

Antes de continuar ¿qué podemos destacar del código anterior?

  1. Una clase puede ser:
    1. private: la clase es sólo conocida de forma local, es decir por el código de esa clase.
    2. public: la clase es visible en toda la aplicación.
    3. global: la clase es visible por cualquier código Apex en cualquier sitio, es decir, si empaquetamos la clase, ésta será visible y accesible para cualquier usuario que haya instalado el paquete. Mi recomendación por ahora es que tu clase sea pública de forma que así también puedes proteger la propiedad intelectual de tu clase si decides comercializarla en un futuro.
  2. With Sharing nos permite indicar permisos de acceso. Échale un vistazo a este blog en inglés de Andy Fawcett dónde explica el significado de estás palabras
  3. Palabra Controller en el nombre de la clase. Es una opción personal para saber que esa clase es un Controlador, pero si tu prefieres darle otro nombre, ¡adelante! sólo una recomendación. Mantén una misma convención en cuanto a nombres se refiere a lo largo de todo tu desarrollo, así que si creas otro controlador, dale un nombre similar
  4. Un constructor con un único argumento del tipo ApexPages.StandardController que proporciona una serie de métodos para poder recoger información del registro asociado a ese controlador

Una vez que tenemos el controller creado, vamos a asociarlo a una página. Por ejemplo, vamos a extender la funcionalidad de la página accountedit que creamos en el post anterior. Para ello sólo tenemos que añadir el atributo extensions a la definición de la página.

<apex:page standardController="Account" extensions="AccountController">
</apex:page> 

Y ahora, ¿tenemos que hacer algún otro cambio? No en principio podemos mantener el mismo código en la página aunque lo asociemos a un controlador custom.


<!-- accountedit -->
<apex:page standardController="Account" extensions="AccountController">
  <apex:form >
    <apex:pageBlock title="Vista Cuenta">
      <apex:pageBlockButtons >
        <apex:commandButton action="{!save}" value="Save"/>
      </apex:pageBlockButtons>
      <apex:pageBlockSection title="Campos Cuenta">
        <apex:inputField value="{!account.name}"/>
        <apex:inputField value="{!account.site}"/>
        <apex:inputField value="{!account.type}"/>
        <apex:inputField value="{!account.accountNumber}"/>
      </apex:pageBlockSection>
    </apex:pageBlock>
  </apex:form>
</apex:page>

Como ya aprendimos en el post anterior, si sobre escribimos el botón Edit del objeto Cuenta para mostrar nuestra página, el resultado será el siguiente:

Captura de pantalla 2016-01-18 a las 16.31.57

Entonces, ¿cual es la diferencia?

Por ahora ninguna.

Pero vamos a seguir añadiendo funcionalidad. En la página view del anterior post, mostrábamos además una lista relacionada con las Oportunidades asociadas a la cuenta seleccionada. Vamos a hacer lo mismo en nuestra página pero esta vez vamos a usar el tag  apex:BlockTable.  Este tag tiene como atributo value que usaremos para llamar al método que nos devuelve la lista de registros sobre la que vamos a iterar en la tabla. Y el atributo var que será la variable que usaremos en cada iteración.

<apex:pageBlockTable title="Opportunities" value="{!opportunityList}" var="opp">
</apex:pageBlockTable>

¿Y qué es opportunityList? Ahí es cuando entra en acción nuestro nuevo controlador dónde tendremos un método que via una consulta a base de datos (en Salesforce se llaman SOQL) obtendremos la lista de Oportunidades asociadas a la cuenta Account1.

Una vez que tenga esta información, usaremos los métodos get / set que al final serán los que el atributo value llamará.


public with sharing class AccountController
{
    private Id m_accountId;
    private List<Opportunity> m_opportunities;
    private ApexPages.StandardController m_controller;

    public AccountController(ApexPages.StandardController controller)
    {
        m_controller = controller;
        m_accountId = m_controller.getRecord().Id;
        m_opportunities = calculateOpportunities(m_accountId);
    }
   
    public List<Opportunity> getOpportunityList()
    {
        return m_opportunities;
    }

    public void setOpportunityList(List<Opportunity> value)
    {
        m_opportunities = value;
    }

    private List<Opportunity> calculateOpportunities(Id accId)
    {
        //Remember Separation of Concerns
        //Move to a Selector class
        List<Opportunity> oppList = [Select Id, Name From Opportunity Where AccountId = :accId];

        return oppList;
    }
}

Cómo hemos mencionado antes del código, tenemos una variable privada m_opportunities que será utilizada por el get / set. Y si te fijas, estos métodos tienen el nombre que hemos usado en nuestra página visualforce.

Además, tenemos un método calculateOpportunities donde vamos a hacer la consulta a base de datos, la SOQL. Recuerda que aunque en este ejemplo estoy haciendo una SOQL en un controlador, debemos mantener el SOC. Para más información echa un vistazo aquí.

Y este método será llamado en el constructor del controlador y lo usaremos para modificar la variable privada m_opportunities de forma que una vez que se llame al controlador, la lista de Oportunidades se rellenará automáticamente.

Otra cosa a destacar, es el filtro de nuestra consulta. Buscamos las Oportunidades cuyo campo Account tenga el mismo valor que la cuenta que tengo en mi página. Si recuerdas, ya hemos comentado que StandardController ofrece muchos métodos, entre ellos getRecord() que nos devolverá el registro que estamos visualizando en el momento de llamar a nuestro controlador.

El último paso antes de visualizar el resultado, es añadir alguna columna a nuestra tabla. Por ahora sólo añadiremos el Nombre de la oportunidad.


<apex:pageBlock title="Related Lists">
  <apex:pageBlockTable title="Opportunities" value="{!opportunityList}" var="opp" >
    <apex:column headerValue="Name">
      <apex:outputText value="{!opp.Name}"/>
    </apex:column>
  </apex:pageBlockTable>
</apex:pageBlock>

Sabiendo todo esto, ¿cual es el resultado?

Captura de pantalla 2016-01-21 a las 19.51.57.png

¿Pero y si queremos añadir un campo que no está en el objeto Oportunidad y mostrarlo en la tabla? Por ejemplo, me gustaría visualizar un checkbox que me indicara si la Oportunidad tiene estado Closed Won.

Al ser un campo calculado, la lista que devolveremos no será directamente del tipo Oportunidad. Lo que vamos a hacer es crearnos una clase interna con dos variables, por un lado el registro de la oportunidad, y por otro este nuevo campo que vamos a calcular. Una vez que lo tengamos, la lista que usaremos en la página será del tipo de la clase interna.

Por otro lado, en el método que calcula las Oportunidades asociadas a la Cuenta, haremos los cálculos pertinentes para rellenar el nuevo campo que crearemos.

Así quedaría el controlador:


public with sharing class AccountController
{
    private Id m_accountId;
    private List<OpportunityInfo> m_opportunities;
    private ApexPages.StandardController m_controller;

    public AccountController(ApexPages.StandardController controller)
    {
        m_controller = controller;
        m_accountId = controller.getRecord().Id;
        m_opportunities = calculateOpportunities(m_accountId);
    }

    public List<OpportunityInfo> getOpportunityList()
    {
        return m_opportunities;
    }

    public void setOpportunityList(List<OpportunityInfo> value)
    {
        m_opportunities = value;
    }

    private List<OpportunityInfo> calculateOpportunities(Id accId)
    {
        Boolean isClosed;
        OpportunityInfo oppInfo;
        List<OpportunityInfo> oppInfoList = new List<OpportunityInfo>();

        //Remember Separation of Concerns
        //Move to a Selector class
        for(Opportunity opp : [Select Id, Name,
                                 StageName, Amount,
                                 CloseDate
                               From Opportunity
                               Where AccountId = :accId])
        {
            isClosed = opp.StageName == 'Closed Won' ? true : false;
            oppInfo = new OpportunityInfo(opp, isClosed);
            oppInfoList.add(oppInfo);
        }
        return oppInfoList;
     }

     //Inner class
     private class OpportunityInfo
     {
        public Opportunity OpportunityRecord {get; private set;}
        public Boolean ToClose {get; set;}

        private OpportunityInfo(Opportunity oppValue, Boolean toCloseValue)
        {
            OpportunityRecord = oppValue;
            ToClose = toCloseValue;
        }
      }
}

Y en la página visualforce, la tabla tendrá que hacer uso de la clase interna


<apex:pageBlock title="Related Lists">
   <apex:pageBlockTable title="Opportunities" value="{!opportunityList}" var="opp" >
      <apex:column headerValue="Name">
         <apex:outputField value="{!opp.OpportunityRecord.Name}"/>
      </apex:column>
      <apex:column headerValue="is Closed?">
         <apex:inputCheckbox value="{!opp.ToClose}" disabled="{!opp.ToClose}"/>
      </apex:column>
      <apex:column headerValue="Stage">
         <apex:outputField value="{!opp.OpportunityRecord.StageName}"/>
      </apex:column>
      <apex:column headerValue="Amount">
         <apex:outputField value="{!opp.OpportunityRecord.Amount}"/>
      </apex:column>
      <apex:column headerValue="CloseDate">
         <apex:outputField value="{!opp.OpportunityRecord.CloseDate}"/>
      </apex:column>
   </apex:pageBlockTable>
</apex:pageBlock>

Siendo el resultado:

Captura de pantalla 2016-01-21 a las 20.11.08.png

En este ejemplo todos los campos de la tabla son de sólo lectura. El único que es input es el campo personalizado que hemos creado a partir de la clase interna. Pero si te fijas, en todos los registros el checkbox es de tipo lectura menos en el último en que el campo es de escritura porque el Stage no es Closed Won. Sin embargo el tag que hemos usado en todos los casos es apex:inputCheckbox. ¿Por qué no usamos output? Porque este tag sólo da la opción de escritura. Sin embargo ofrece un atributo para deshabilitarlo y es el que hemos usado, disabled.

Por último, nos podríamos preguntar por el botón Save. Si mantenemos en nuestra página visualforce la misma línea para el botón save que en el post anterior, la funcionalidad será la misma. Usará el controlador Standard para hacer la modificación en base de datos.

Sin embargo podemos modificar esa funcionalidad para extenderla. Para ello tendremos que crear un método en nuestro controlador dónde añadiremos la funcionalidad que necesitemos y devolveremos la página hacia donde quiero navegar después de la modificación. En nuestro caso la versión sólo lectura de nuestra cuenta.

public PageReference saveUpdates()
{
    //TODO: Acciones para salvar
    return new PageReference('/' + m_accountId);
} 

Y hacer una llamada a ese método en nuestra página.

<apex:pageBlockButtons >
    <apex:commandButton action="{!saveUpdates}" value="Save"/>
</apex:pageBlockButtons> 

Como resumen, en esta entrada hemos aprendido:

  • Cómo crear un controlador asociado a un único registro
  • Cómo asociarlo a una visualforce
  • Cómo mostrar campos en nuestra página visualforce
  • Cómo crear un tabla con valores que vienen de base de datos
  • Cómo añadir a la tabla un campo que no existe en base de datos
  • Cómo modificar el botón Standard Save

Nos vemos en el próximo post.

 

How to attach a file to Opportunity Lines

Some days ago, I received an email with a simple question. “How can I get some attachments related to my object?”

I know what you are thinking. The answer is easy. Actually we have 2 options, and both with #clicksnotcode:

  1. Add Attachment and Notes related list to your object.
  2. Enable Chatter on our object.

But her question didn’t end there. Actually she needed these files attached to a certain Standard object, Opportunity Product or Opportunity Line Item one. Ok, in that case above options do not work because we find some restrictions on this object.

Having this information, the only solution that came to my mind was to have a custom object as container where we can have the list of Attachments that we need to link to Opportunity Line.

  • New Custom Object – Attachment Container

Bellow example shows the easiest object that you can create with only Standard fields, and no custom field at all. But you can customize as much as you want.

  • Add Note and Attachments: 

Double check that Note and Attachments related list is in the page layout, so after creating a record, you can attach a new file.

  • New lookup field:

Now we would need to create a lookup field on Opportunity Line Item Standard object, so that, we can link each record to a single Attachment Container record, and its list of files.

Captura de pantalla 2015-06-06 a las 23.22.54

Having these objects structure, we would be able to use Apex code an access to all files like they were attached to Opportunity Lines directly.

We can only find a single issue. How can we avoid that the same Attachment Container record could be related to a single Opportunity record?

  • Create an Opportunity Line trigger:

Via a trigger, we could make sure that 2 Opportunity Lines will not try to use the same Attachment Container record, and be sure that files would be ONLY related to a single Opportunity Line record. In this way we can enforce a relationship of 1 to1 using lookup fields.

I would also like to raise the fact that we are doing validations on after actions. Why? What about if the end user decide to create its own trigger and modify the record during the before? Having our validations at the end, we will be sure that we would check final values. Please, find more information about validations on triggers on this blog entry.

trigger OpportunityLineTrigger on OpportunityLineItem (after insert, 
                                                       after update)
{
    //Retrieve attachments that already exist
    Map<Id, Id> currentAttachmentAndOppLines = new Map<Id, Id>();
    for(OpportunityLineItem currentOppLine : [Select Id, Attachment_Container__c
                                              From OpportunityLineItem])
    {
         currentAttachmentAndOppLines.put(currentOppLine.Attachment_Container__c,
                                          currentOppLine.Id);
    }

    if(Trigger.isAfter)
    {
        for(OpportunityLineItem oppLine : Trigger.new)
        {
            Id attachmentContainerId = oppLine.Attachment_Container__c;
            if(currentAttachmentAndOppLines.containsKey(attachmentContainerId))
            {
                if(Trigger.isUpdate)
                {
                    Id oppLineId = oppLine.Id;
                    if(currentAttachmentAndOppLines.get(attachmentContainerId) != oppLineId)
                    {
                        oppLine.addError('This Attachment Container is already in used.');
                    }
                }
            }
        }
     }
}

Do you have any other idea? Don’t be shy and share. I’m looking forward to knowing it !!