Visualforce y Lightning Experience

Después de un gran parón en mi blog debido a dreamOlé y otros eventos, hoy me gustaría publicar un post muy pero que muy pequeñín sobre una funcionalidad que nos trae la Summer ’18: visualizar una página Visualforce con estilo Lightning Experience.

Lo primero de todo, comencemos con la pregunta:

¿Trabajas en Lightning Experience?

Si, tu respuesta es NO … no eres el único, ya que una servidora sigue abriendo Classic UI casi siempre, pero chicos, hay que ser valientes y movernos a LEX !!!

Una de las razones por las que sigo en Classic es porque soy de la vieja escuela, y cuando tengo que montar una nueva UI me voy a Visualforce en vez de Lightning Components. Segundo error …. es hora de perder el miedo a Lightning Components y empezar a desarrollar UI de la forma que Salesforce nos aconseja.

¿Qué pasa si mi producto tiene ya cientos de páginas Visualforce? ¿Debo migrar todo a Lightning Components?

Teniendo en cuenta que el tiempo que puedes necesitar para ello puede ser elevado, y que desgraciadamente Lightning Components no ofrece toda la funcionalidad de Visualforce, yo migraría aquello que fuera sencillo. Por otro lado, si hay que hacer una nueva UI, revisad primero si Lightning Components os sirve.

¿Qué hacemos con el resto?

Salesforce responde a nuestras súplicas, y en mi caso, casi me echo a llorar cuando vi lo sencillo que era aplicar el estilo Lightning Experience a una página existente, de forma que pasamos de esto:

Captura de pantalla 2018-05-25 a las 11.44.33

A esto:

Captura de pantalla 2018-05-25 a las 11.45.42

¿Cómo lo hemos conseguido? Fácil.

Para ello necesitamos una Visualforce, que hemos rescatado de esta entrada, donde creábamos una vista alternativa a una cuenta, junto con su listado de oportunidades.

Una vez que tenemos nuestra página Visualforce, tenemos que asegurarnos que será visible en Lightning Experience, para lo que tenemos que marcar el checkbox “Available for Lightning Experience, Lightning Communities and the mobile app”

Captura de pantalla 2018-05-25 a las 11.43.51

Con esto conseguimos visualizar la página en Lightning Experience. Es decir, sin este checkbox, aunque usemos esta página para sobre-escribir la vista de un registro de Account, y la visualicemos en Classic UI, si nos movemos a LEX, visualizaremos la página Standard de Salesforce en LEX en vez de la nuestra.

Y por último (si un último paso!!) sólo tenemos que añadir lightningStyleSheets="true" al tag <apex:page> de forma que nuestra página quedaría así:

<apex:page standardController="Account" lightningStyleSheets="true">
 <apex:form >
  <apex:pageBlock title="Vista Cuenta">
   <apex:pageBlockButtons >
    <apex:commandButton value="Edit" action="{!edit}"/>
   </apex:pageBlockButtons>

   <apex:pageBlockSection title="Campos Cuenta" columns="2">
    <apex:outputField value="{!account.name}"/>
    <apex:outputField value="{!account.site}"/>
    <apex:outputField value="{!account.type}"/>
    <apex:outputField value="{!account.accountNumber}"/>
   </apex:pageBlockSection>
  </apex:pageBlock>
 </apex:form>
 <apex:relatedList list="Opportunities" />
</apex:page>

Si quieres saber más, échale un ojo a este link.

 

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.

 

Mi primera Visualforce II

En la última entrada en Español estuvimos aprendiendo como empezar con Apex y Visualforce. Mostramos una nueva página con algunos campos del objeto Cuenta (Account). Hoy veremos cómo pasar de campos de lectura a escritura.

Lo primero de todo, intentaremos cambiar los campos que teníamos en la pantalla anterior a modo escritura. Para ello lo único que tenemos que hacer es utilizar un tag diferente: apex:outputField

En este caso, la página se llamará accountedit ya que la vamos a utilizar para modificar la información que ofrece.

<!-- accountedit -->
<apex:page standardController="Account">
  <apex:form >
    <apex:pageBlock title="Vista Cuenta">
      <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>

Si escribimos una Url similar a esta:

https://<instancia>salesforce.com/apex/accountedit

vemos como ahora tenemos unos cuadros de texto donde añadir la información.

Captura de pantalla 2015-11-27 a las 18.43.01

De igual forma que en la anterior entrada, si queremos ver la información de una cuenta en concreto para modificarla, debemos simplemente añadir el Id único de la misma, en la url.

Captura de pantalla 2015-11-27 a las 18.49.54.png

Si haces una búsqueda en Google y pones algo del tipo “añadir campo de escritura en mi visualforce” aparecerá otro tag que también podrías usar: apex:outputText

¿Cual es la diferencia entre ambos tag si el resultado es el mismo? La principal es que outputField va a respetar el tipo de campo que quieras mostrar, mientras que outputText añade eso, un texto. Veamos un ejemplo que es bastante claro.

<apex:page standardController="Account">
  <apex:form >
    <apex:pageBlock title="Vista Cuenta">
      <apex:pageBlockSection title="Campos Cuenta">
        <apex:outputText value="{!account.type}"/>
        <apex:outputField value="{!account.type}"/>
      </apex:pageBlockSection>
    </apex:pageBlock>
  </apex:form>
</apex:page>

En el código de arriba vemos como queremos mostrar un campo de tipo picklist en nuestra página visualforce. Uno con outputField y el segundo es outputText. ¿Cual es el resultado?

Captura de pantalla 2015-11-27 a las 18.59.02.png

En campos de tipo lectura no podemos encontrarla ya que muestra simplemente el valor que hay almacenado en base de datos. Sin embargo en el caso de escritura, la diferencia es mayor

Captura de pantalla 2015-11-27 a las 18.59.50

Mientras que la en la primera opción muestra sólo un texto, el valor asociado a ese campo en el registro, en la segunda se puede desplegar el campo y visualizar todas las opciones que ofrece.

<apex:page standardController="Account">
  <apex:form >
    <apex:pageBlock title="Vista Cuenta">
      <apex:pageBlockSection title="Campos Cuenta">
        <apex:inputText value="{!account.type}"/>
        <apex:inputField value="{!account.type}"/>
      </apex:pageBlockSection>
    </apex:pageBlock>
  </apex:form>
</apex:page>

Esto no significa que no debamos usar outputText ó inputText, simplemente que en determinados casos, es preferible ouputField / inputField, ya que proporcionamos una mejor experiencia al usuario final.

¿Intentamos añadir más información a nuestras primeras páginas Visualforce? Algo que también nos puede interesar es mostrar información de los objetos relacionados. De igual forma que la página standard de Cuenta muestra las Oportunidades que están relacionadas con esa Account, ahora queremos mostrar esas Oportunidades en nuestra página Visualforce. Para ello utilizaremos el tag: apex:relatedList

En este caso, el tag deberemos ponerlo fuera del tag apex:form y simplemente añadir el nombre API del objeto. A partir de ese momento, Salesforce se encarga de todo el trabajo por nosotros.

 <apex:page standardController="Account">
   <apex:form >
     <apex:pageBlock title="Vista Cuenta">
       <apex:pageBlockSection title="Campos Cuenta" columns="2">
         <apex:outputField value="{!account.name}"/>
         <apex:outputField value="{!account.site}"/>
         <apex:outputField value="{!account.type}"/>
         <apex:outputField value="{!account.accountNumber}"/>
       </apex:pageBlockSection>
     </apex:pageBlock>
   </apex:form>
   <apex:relatedList list="Opportunities" />
</apex:page>

Captura de pantalla 2015-11-27 a las 19.07.09.png

En el caso de que queramos tener la opción de modificar alguno de los valores des esta lista relacionada, necesitaremos añadir código Apex y un Controlador. Pero esto es algo que veremos en la próxima entrada. Hoy continuaremos sólo con acciones que podamos llevar a cabo con el Controlador Standard de los objetos.

Uno de los últimos pasos que vamos a dar hoy será guardar en base de datos las modificaciones que hacemos en uno de los campos. Para ello, añadiremos un botón a nuestra página mediante el tag: apex:commandbutton Revisa este tag porque es muy interesante, ya que con sus atributos puedes indicar si mostrar el botón solo en la parte de arriba de la pantalla ó en ambos lados (por defecto los muestra en los dos).

<!-- accountedit -->
<apex:page standardController="Account">
  <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>

El atributo value nos permite indicar qué nombre le queremos poner al botón y el atributo action llamará al proceso que queremos ejecutar. Como seguimos trabajando con el Controlador Standard del objeto, simplemente debemos añadir “save” y Salesforce se encarga de realizar la acción de guardado por nosotros.

Captura de pantalla 2015-11-27 a las 19.13.21

Y después de modificar el valor de Account Number por 124 y darle al botón Save, accountview page muestra:

Captura de pantalla 2015-11-27 a las 19.16.51

¿Qué otras acciones nos permite Salesforce? En este link encontramos información sobre ellas, pero básicamente son:

  • Save
  • QuickSave
  • Edit
  • Delete
  • Cancel
  • List

 

Por ultimo, una ayuda para tener que evitar modificar el id de la cuenta cada vez que queremos mostrar un registro distinto. Si hemos creado estas páginas es porque queremos ofrecer esta vista al usuario final en vez de la standard. En ese caso, Salesforce nos permite sobre escribir estas vistas en el objeto.

Si vamos a:

Setup | Customize | Accounts | Buttons, Links and Actions

Modificaremos las vistas View y Edit

Captura de pantalla 2015-11-27 a las 19.20.23

En el caso de que queramos hacer lo mismo con un objeto creado por nostros, custom, en vez de Standard, solo tenemos que ir a

Setup | Create | Objects | <selecciona tu objecto> 

Y desde ahí baja a la sección Buttons,Links and Actions. La sobre escritura de la página se haría de forma similar a un objeto Standard.

En este post hemos aprendido:

  • Como crear una página editable
  • Diferencia entre inputField vs inputText y outputField vs outputText
  • Como añadir lista de un objeto relacionado a Cuenta
  • Como guarder modificaciones en la página de editado
  • Como evitar modificar la url con el id cada vez que quiero mostrar un registro

Y todo esto sin añadir una línea de código en Apex, solo Visualforce, ya que hemos usado el controlador Standard que los objetos nos ofrecen.