Como desarrollar un controlador personalizado (StandarController), para recuperar datos de objetos personalizados de cara a la generación de un informe con datos relacionados de varios objetos generado mediante una página de Visualforce.

Posiblemente esto sea de primero de desarrollador de Salesforce, y una tarea de lo más simple, pero si como yo, eres autodidacta, es posible que como a mi, te cueste un poco de trabajo conseguir realizar el proceso completo, ya que requiere de varios pasos, así que aquí dejo un ejemplo práctico de todo el proceso por si te puede servir de algo a ti, o por si en un futuro lo necesitara yo 😉

  1. Desarrollo del controlador StandarController en un Sandbox.
  2. Desarrollo de la página de Visualforce.
  3. Desarrollo de las clases de Test de código que permitan implementar el controlador en producción.
  4. Implementación del código en la organización de producción desde el Sandbox

 

El problema

En mi caso particular, tengo una serie de objetos personalizados relacionados con el objeto Contact, y lo que necesitaba era poder sacar un Informe en PDF con un resumen de los datos del contacto, con un extracto de la información contenida en los distintos objetos personalizados relacionados con cada uno de los contactos.

Para ello necesitaba un controlador personalizado que recuperara los datos necesarios para después consultarlos desde la página, así que empezaremos por el controlador.

 

Desarrollo del StandarController

Para desarrollar el StandarController, debemos crear un nuevo Sandbox en el que hacer todo el desarrollo y las pruebas que posteriormente implementaremos en producción.

En mi caso particular, creo la clase Apex CandidatoResumenController, que recogerá los datos de los objetos estandar Eventos y Tareas relacionadas con el candidato en cuestión por un lado, y de los objetos personalizados Cursos Realizados y Postulaciones.

A continuación muestro el código que recupera toda la información.

 

 

public with sharing class CandidatoResumenController {
  
    // Variables del controlador y el candidato seleccionado
    private ApexPages.StandardController m_controller;
    private Id idCandidato;
    // Variables para recoger las listas de objetos relacionados
    private List<Postulacion__c> listaPostulaciones;
    private List<Event> listaEventos;
    private List<Task> listaTareas;
    private List<Curso_Realizado__c> listaCursos;
    // Variables para saber si existen registros en los objetos relacionados
    private boolean hayPostulaciones = False;
    private boolean hayEventos = False;
    private boolean hayTareas = False;
    private boolean hayCursos = False;
    // Variables para recoger el número de registros de los objetos relacionados
    private integer iPostulaciones = 0;
    private integer iEventos = 0;
    private integer iTareas = 0;
    private integer iContrataciones = 0;
    private integer iCursos = 0;

    
    
    // Metodo principal que recogerá el candidato actual y llamara a las
    // funciones de busqueda de registros relacionados
    public CandidatoResumenController(ApexPages.StandardController controller){
        m_controller = controller;
        idCandidato = m_controller.getRecord().Id;
        listaEventos = calculaEventos(idCandidato);
        listaTareas = calculaTareas(idCandidato);
        listaPostulaciones = calculaPostulaciones(idCandidato);
        listaCursos = calculaCursos(IdCandidato);
    }
    
    /*
     * Funciones publicas utilizadas para devolver los distintos datos
     * recogidos, número de registros, etc
     */
        
    public List<Postulacion__c> getPostulaciones(){
        return listaPostulaciones;
    }
    
    public integer getNumeroEventos(){
        return iEventos;
    }
    
    public Boolean getHayEventos(){
        return hayEventos;
    }
    
    public integer getNumeroTareas(){
        return iTareas;
    }
    
    public Boolean getHayTareas(){
        return hayTareas;
    }
    
    public integer getNumeroPostulaciones(){
        return iPostulaciones;
    }
    
    public integer getNumeroCursos(){
        return iCursos;
    }
    
    public boolean getHayCursos(){
        return hayCursos;
    }
    
    public Boolean getHayPostulaciones(){
        return hayPostulaciones;
    }
    
    public integer getNumeroContrataciones(){
        return iContrataciones;
    }
    
    public List<Event> getEventos(){
        return listaEventos;
    }
    
    public List<Task> getTareas(){
        return listaTareas;
    }
    
    public List<Curso_Realizado__c> getCursosRealizados(){
        return listaCursos;
    }
    
    // Función para recuperar los datos de cursos realizados por el candidato/contacto
    private List<Curso_Realizado__c> calculaCursos(Id IdCandidato){
        List<Curso_Realizado__c> lCursos = [SELECT Name, Fecha_Matriculacion__c, Curso__r.Name, Curso__r.Descripcion__c, Estapa__c FROM Curso_Realizado__c
                                           WHERE Alumno__c= :IdCandidato ];
        
        if (lCursos.size() > 0){
            iCursos = lCursos.size();
            hayCursos = True;
        }
       
        return lCursos;
    }
    
    // Función para recuperar los eventos relativos al candidato seleccionado
    private List<Event> calculaEventos(Id idCandidato){
        List<Event> lEventos = [SELECT Id, Subject, ActivityDate, Type, Description  FROM Event WHERE WhoId= :idCandidato];
        if (lEventos.size() > 0){
            hayEventos = True;
            iEventos = lEventos.size();
        }        
        return lEventos;
    }
    
    // Funcion para recuperar las tareas relacionadas con el candidato seleccionado
    private List<Task> calculaTareas(Id idCandidato){
        List<Task> lTareas = [SELECT Id, Subject, Description, ActivityDate FROM Task WHERE WhoId= : idCandidato];
        if(lTareas.size() > 0){
            hayTareas = True;
            iEventos = lTareas.size();
        }
        return lTareas;
    }
    
    // Función para recuperar postulaciones realizadas con el candidato/contacto
    private List<Postulacion__c> calculaPostulaciones(Id idCandidato){
        List<Postulacion__c> lPostulaciones =  [SELECT Id, Name, Contratacion__c, Oferta__r.Name, Oferta__r.Puesto__c, Fecha_Postulacion__c,
                                                Feedback__c, Fecha_Contrato__c, Fecha_Fin__c
                                                FROM Postulacion__c
                                                WHERE Candidato__c= :idCandidato
                                                ORDER BY Fecha_Postulacion__c DESC ];
        
        
        for(integer i = 0; i < lPostulaciones.size(); i++){
            if (lPostulaciones[i].Contratacion__c){
                iContrataciones += 1;
            }
        }
        
        if(lPostulaciones.size() > 0){
            hayPostulaciones = True;
            iPostulaciones = lPostulaciones.size();
        }
        return lPostulaciones;
    }
}

 

Desarrollo Página de Visualforce

Una vez desarrollado el controlador, toca probar que recoge todos los datos que precisamos mostrar en nuestra página.

En mi caso particular, tengo la costumbre de realizar el desarrollo en paralelo para ir comprobando paso a paso que el código funciona, pero supongo que cada uno utiliza su propia metodología de desarrollo.

Algunos campos han sido alterados por cuestiones de privacidad

 

<!--
Inicializo la página con standarController de Contact y la extensión CandidatoResumenController creada anteriormente.
Hago el render como documento PDF, y establezco una serie de propiedades.
En cuanto a las propiedades, posiblemente no sean todas necesarias, pero las arrastro de otras páginas después de 
haber buscado información al respecto en diversos foros.
-->

<apex:page standardController="Contact" extensions="CandidatoResumenController" renderAs="PDF" standardStylesheets="false" applyHtmlTag="false" sidebar="false" showHeader="false" language="ES">

    <head>
 	<style type="text/css">
            <!--
            <apex:stylesheet value="{!URLFOR($Resource.Bulma, 'bulma/bulma/css/bulma.min.css')}"/>
            -->
            body, p, h1, h2, h3, h4, h5, h6 {font-family: Helvetica, Arial, 'Arial Unicode MS';}
            
            p {text-align: justify}
            
            h1 {font-size: 22px;}
            
            h3 {
            	font-size: 16:px;
            	font-weight: bold;
            }
            
            p {font-size: 13px;}
            
            .contenedor{
            	width: 100%;
            }
            
            .grupo{
            	background-color: #97BE0D;
            }
            
            tbody{
                font-size: 12px;
            }
            
            table {
            	width: 100%;
                padding: 6px 6px 6px 6px;
                font-size: 11px;
            }
            
            th, td {
            	padding: 6px 6px 6px 6px;
            }
            
            .separador{
            	width:100%;
            	heigth: 20px;
            }
            
            .salto{
            	page-break-after:always;
            }
            
            .cabecera{
            	background-color: #3AAADC;
            	font-size: 14px;
            	text-align: center;
            }
            
            .label{
            	background-color: #97BE0D;
            	width: 20%;
            }
            .campo{
                width: 30%;
            }
            
            .largo{
            	width: 80%;
            }
            
            .labelmin{
            	background-color: #97BE0D;
            	width: 15%;
            
            }
            
            .check{
            	color: #F29400;
            }
            
            .dir {text-transform: uppercase;}  
            
        </style>
    </head>   

<!--
    Aquí declaro todas las variables que voy a recoger del Controlador personalizado
    para utilizarlas posteriormente donde sea necesario
-->
    <apex:variable value="{!NumeroEventos}" var="ne"/>
    <apex:variable value="{!NumeroTareas}" var="nt"/>
    <apex:variable value="{!NumeroPostulaciones}" var="np"/>
    <apex:variable value="{!NumeroCursos}" var="ncc"/>
    <apex:variable value="{!NumeroContrataciones}" var="nc"/>
    
    <apex:variable value="{!hayPostulaciones}" var="hayPos"/>
    <apex:variable value="{!hayTareas}" var="hayTar"/>
    <apex:variable value="{!hayEventos}" var="hayEve"/>
    <apex:variable value="{!hayCursos}" var="hayCur"/>
    
    
<!--
Creo una cabecera con el título del Informe generado,
el nombre del candidato o contacto y la fecha de generación
-->
    <div>
        <table>
            <tr>
                <td style="font-size: 20px">Informe Histórico:<br/><b>{!Contact.Name}</b><br/>
                    <span style="font-size: 14px">
                    Fecha:&nbsp; 
                    <apex:outputText value="{0,date,dd/MM/yyyy}"><apex:param value="{!Now()}"/></apex:outputText>
                    </span>
                </td>
                <td style="width:40%; text-align: right"><apex:image url="{!URLFOR($Resource.Recursos, 'recursos/logos/logo500.png')}" width="300" height="auto" alt="Logotipo"/></td>
            </tr>
        </table>
        <hr/>
    </div>


<!--
    La primera página del informe recoge datos del objeto estandar Contact
    mostrando un resumen de todos los datos del candidato
-->    

    <div class="contenedor salto">
        <table class="table">
                <tr>
            		<td class="cabecera" colspan="4">
                        Datos personales del candidato
                	</td>
                </tr>

            	<tr>
                    <td class="label">Nombre</td>
                    <td class="campo">{!Contact.FirstName}</td>
                    <td class="label">Apellidos</td>
                    <td class="campo">{!Contact.LastName}</td>
                </tr>
                <tr>
                	<td class="label">Fecha Nacimiento</td>
                	<td class="campo">
                        <apex:outputField value="{!Contact.Fecha_Nacimiento__c}"/>
                    </td>
                	<td class="label">Alta Empil</td>                    
                	<td class="campo">
                        <apex:outputField value="{!Contact.Fecha_Alta__c}"/>
                    </td>
                </tr>
                <tr>
                	<td class="label">Tipo XX</td>
                	<td class="campo">{!Contact.Tipo_XX__c}</td>
                	<td class="label">Porcentaje</td>                    
                	<td class="campo">{!Contact.Tipo_XXX__c}</td>                    
                </tr>
                <tr>
                	<td class="label">Origen</td>
                	<td class="campo">{!Contact.Tipo_ZZ__c}</td>
                	<td class="label">Fecha</td>                    
                	<td class="campo">{!Contact.Tipo_ZZZ__c}</td>                    
                </tr>
                <tr>
                	<td class="label">Puesto</td>
                	<td class="campo check"><apex:outputField value="{!Contact.Puesto__c}"/></td>
                	<td class="label">mmmmmmmmm</td>                    
                	<td class="campo check"><apex:outputField value="{!Contact.mmm__c}"/></td>                    
                </tr>
                <tr>
                	<td class="label">Contrato</td>
                	<td class="campo"><apex:outputField value="{!Contact.Contrato__c}"/></td>
                	<td class="label">Afiliado</td>                    
                	<td class="campo"><apex:outputField value="{!Contact.Afiliado__c}"/></td>                    
                </tr>                
                <tr>
                	<td class="label">Nivel Estudios</td>
                	<td class="campo"><apex:outputField value="{!Contact.Nivel_Estudios__c}"/></td>
                	<td class="label">Coche Propio</td>                    
                	<td class="campo"><apex:outputField value="{!Contact.Coche_propio__c}"/></td>                    
                </tr>
                <tr>
                	<td class="label">Jornada preferible</td>
                	<td class="campo"><apex:outputField value="{!Contact.Jornada_Preferible__c}"/></td>
                	<td class="label">Horario disponible</td>                    
                	<td class="campo"><apex:outputField value="{!Contact.Horario_disponible__c}"/></td>                    
                </tr>
                <tr>
                	<td class="label">Permisos conduccion</td>
                	<td class="campo"><apex:outputField value="{!Contact.Permisos_conduccion__c}"/></td>
                	<td class="label">Otros carnets</td>                    
                	<td class="campo"><apex:outputField value="{!Contact.Otros_carnets__c}"/></td>                    
                </tr>            
                <tr>
                	<td class="label">Idiomas</td>
                	<td class="campo"><apex:outputField value="{!Contact.Idiomas__c}"/></td>
                	<td class="label">Informática</td>                    
                	<td class="campo"><apex:outputField value="{!Contact.Informatica__c}"/></td>                    
                </tr>            
                <tr>
                	<td class="label">Postulaciones</td>
                	<td class="campo">{!np}</td>
                	<td class="label">Contrataciones</td>                    
                	<td class="campo">{!nc}</td>                    
            	</tr>
            	<tr>
                    <td class="label">Categorias experiencia</td>
                    <td class="campo" colspan="3"><apex:outputField value="{!Contact.Categorias_Experiencia__c}"/></td>
            	</tr>
            	<tr>
                    <td class="label">Observaciones</td>
                    <td class="campo" colspan="3"><apex:outputField value="{!Contact.Observaciones__c}"/></td>
            	</tr>
        </table>
    </div>

<!--
    En una página a continuación muestro las postulaciones realizadas para dicho candidato
    Utilizando la variable hayPos, la página se renderiza únicamente si existen registros
-->    
    
    <apex:outputText rendered="{!hayPos}">
        <div class="contenedor salto">
            <table>
                    <tr>
                        <th class="cabecera" colspan="4">
                            Postulaciones
                        </th>
                    </tr>
                    
                    <apex:repeat value="{!Postulaciones}" var="pos">
                    <tr>
                        <td class="label">Postulación</td>
                        <td class="campo">{!pos.Name}</td>
                        <td class="label">Oferta</td>                    
                        <td class="campo">{!pos.Oferta__r.Name}</td>                    
                    </tr>
                    <tr>
                        <td class="label">Fecha</td>
                        <td class="campo"><apex:outputField value="{!pos.Fecha_Postulacion__c}"/></td>
                        <td class="label">Contratacion</td>                    
                        <td class="campo"><apex:outputField value="{!pos.Contratacion__c}"/> </td>                    
                    </tr>
                    <apex:outputText rendered="{!pos.Contratacion__c}">
                    <tr>
                        <td class="label">Fecha Contrato</td>
                        <td class="campo"><apex:outputField value="{!pos.Fecha_Contrato__c}"/> </td>
                        <td class="label">Fecha Fin</td>                    
                        <td class="campo"><apex:outputField value="{!pos.Fecha_Fin__c}"/> </td>                    
                    </tr>                    
                    </apex:outputText>    
                    <tr>
                        <td class="label">Puesto</td>
                        <td class="campo" colspan="3">{!pos.Oferta__r.Puesto__c}</td>
                    </tr>
                    <tr>
                        <td class="label">Feedback</td>
                        <td class="campo" colspan="3">{!pos.Feedback__c}</td>
                    </tr>
                    <hr/>
                    </apex:repeat>
            </table>
        </div>
    </apex:outputText>
    


<!--
    En otra página a continuación muestro los cursos realizados por el candidato
    Utilizando la variable hayCur, la página se renderiza únicamente si existen registros
--> 
    
    
    <apex:outputText rendered="{!hayCur}">
        <div class="contenedor salto">
            <table>
                
            
                <tr>
                	<td class="cabecera" colspan="4">Cursos realizados</td>
                </tr>                
                <apex:repeat value="{!CursosRealizados}" var="curso">
                <tr>
                	<td class="label">Curso</td>
                	<td class="campo">{!curso.Name}</td>
                	<td class="label">Fecha Matriculación</td>                    
                	<td class="campo"><apex:outputField value="{!curso.Fecha_Matriculacion__c}"/></td>                    
                </tr>                
                <tr>
                	<td class="label">Nombre</td>                    
                	<td class="campo">{!curso.Curso__r.Descripcion__c}</td>
                    <td class="label">Estado</td>
                    <td class="campo">{!curso.Estapa__c}</td>
                </tr>                
                </apex:repeat>
            </table>            
        </div>
    </apex:outputText>
    


<!--
    Por último, se añaden dos páginas para mostrar una relación de Eventos
    y tareas relacionadas con el candidato en cuestión
-->    
    
    

    <apex:outputText rendered="{!hayEve}">
        <div class="contenedor salto">
            <table>
                <tr>
                    <td class="cabecera" colspan="4">Relación Tareas / Eventos</td>
                </tr>

                <apex:repeat value="{!Eventos}" var="even">
                    <tr>
                        <td class="label">Asunto</td>
                        <td class="campo" colspan="3">{!even.Subject}</td>
                    </tr>
                    <tr>
                        <td class="label">Fecha</td>
                        <td class="campo"><apex:outputField value="{!even.ActivityDate}"/></td>
                        <td class="label">Tipo</td>
                        <td class="campo">{!even.Type}</td>
                    </tr>
                    <tr>
                        <td class="label">Descripción</td>
                        <td class="campo" colspan="3">{!even.description}</td>
                    </tr>
                    <hr/>
                </apex:repeat>
            </table>
        </div>
    </apex:outputText>
    
    
    
    
    <apex:outputText rendered="{!hayTar}">
        <div class="contenedor">
            <table>
                <tr>
                    <td class="cabecera" colspan="4">Relación Actividades</td>
                </tr>

                <apex:repeat value="{!Tareas}" var="task">
                    <tr>
                        <td class="label">Fecha</td>
                        <td class="campo largo" colspan="3"><apex:outputField value="{!task.ActivityDate}"/></td>
                    </tr>
                    <tr>
                        <td class="label">Asunto</td>
                        <td class="campo largo" colspan="3">{!task.Subject}</td>
                    </tr>
                    <tr>
                        <td class="label">Descripción</td>
                        <td class="campo largo" colspan="3">{!task.Description}</td>
                    </tr>
                    <hr/>
                
                </apex:repeat>
            </table>
        </div>
    </apex:outputText>
    
</apex:page>

 

Clases Test para verificación del código

La primera vez que intenté hacer algo de esto, lo primero que me sorprendió es que no se puede desarrollar en la organización de producción, así que toca crearse un  Sandbox de desarrollo.

Lo segundo es que una vez desarrollado y testado, no había manera de copiar y pegar el código de alguna manera en la organización de producción, así que hay que aprender a utilizar los conjuntos de cambios para hacer la implementación.

Pero no quedó ahí la cosa, lo tercero y gracias a los dioses último, es que tampoco permite implementar el conjunto de cambios, si no se realiza un test de al menos el 75% del código si no me equivoco.

Así que aquí va la clase test que realizaría los test de código de la clase del controlador.

 

// Creamos una clase pública que será la responsable de hacer las llamadas necesarias
// para testar el correcto funcionamiento de la mayor cantidad posible de código.

// Se utiliza el siguiente "decorador" @isTest para la clase

@isTest
public class TestCandidatosController {

    // Contiene un único método también "decorado" en el que debemos asegurarnos
    // ejecutar todas las líneas de código posible del controlador.
    @isTest static void TestCandidatosController(){


        // Primero creo un candidato de prueba con el que realizar las pruebas
        Contact candi = new Contact();
        candi.Salutation = 'Don';
        candi.FirstName = 'Juanito';
        candi.LastName = 'Caminante';
        candi.Estado__c = 'Alta';
        insert candi;

        // Después y en mi caso, creo una oferta, dado que las postulaciones
        // que necesito recuperar están vinculadas a una oferta        
        Oferta__c ofer = new Oferta__c();
        ofer.Cuenta__c = '0013X00003bQOgeQAG';
        ofer.Puesto__c = 'Puesto de pruebas';
        ofer.Categoria_Profesional__c = 'DIR';
        ofer.Provincia_Puesto__c = '28';
        ofer.Tipo_Contrato__c = 'F';
        ofer.Periodo_Duracion__c = 'MES';
        insert ofer;
        
        // y por ultimo creo una postulacion relacionada con la oferta anterior
        Postulacion__c pos = new Postulacion__c();
        pos.Oferta__c = ofer.id;
        pos.Candidato__c = candi.Id;
        insert pos;
        
        // Comenzamos con el código que probará el controlador y con el test en si mismo.
        Test.startTest();

        // Creo un StandarController, pasándole el id del candidato
        // también se crea un controlador personalizado de la clase 
        // que queremos testar.
        ApexPages.StandardController sc = new ApexPages.StandardController(candi);
        CandidatoResumenController rc = new CandidatoResumenController(sc);
        
        // ahora creamos la página sobre la que se testaría el código
        // pasándole a su vez el id del candidato creado para testar la página
        PageReference pageRef = Page.HistorialCandidato;
        pageRef.getParameters().put('Id', String.valueOf(candi.Id));
        Test.setCurrentPage(pageRef);
        

        // Por último voy haciendo llamadas a todos los métodos públicos
        // del controlador, para asegurar que se ejecutan todas las líneas
        // posibles del código
        integer numeroEventos = rc.getNumeroEventos();
        boolean hayEventos = rc.getHayEventos();
        integer numeroTareas = rc.getNumeroTareas();
        boolean hayTareas = rc.getHayTareas();
        integer numeroPostulaciones = rc.getNumeroPostulaciones();
        boolean hayPostulaciones = rc.getHayPostulaciones();
        integer numeroCursos = rc.getNumeroCursos();
        boolean hayCursos = rc.getHayCursos();
        integer numeroContrataciones = rc.getNumeroContrataciones();
        
        List<Event> eventos = rc.getEventos();
        List<Task> tareas = rc.getTareas();
        List<Postulacion__c> postulaciones = rc.getPostulaciones();
        List<Curso_realizado__c> curso = rc.getCursosRealizados();
        
        // Finaliza el test
        Test.stopTest();
        
        
    }
}

 

Implementación en producción

Una vez desarrollado todo el código anterior en el Sandbox, estamos preparados para realizar la implementación en la organización de producción mediante Conjuntos de cambios. Para ello, será necesario configurar los Ajustes de implementación, lo que es relativamente sencillo e intuitivo.

Una vez configurados los ajustes citados, podemos crear los Conjuntos de cambios.

El primer paso es crear un Conjunto de cambios salientes en el Sandbox de desarrollo, al cual añadiremos el controlador personalizado, la página de Visualforce y la clase Test. Una vez añadido los cargaremos para su implementación.

El segundo paso consiste en crear un Conjunto de cambios entrantes en la organización de producción, que nos mostrará el Conjunto creado en desarrollo.

En el proceso de validación del código, introduciremos el nombre de nuestra clase Test (TestCandidatosController) y lanzaremos la validación. Si el proceso de validación se ejecuta correctamente, sin errores y cubriendo el porcentaje de código exigido, podremos proceder a su implementación de forma automática, en caso contrario, tendremos que hacer las correcciones oportunas hasta pasar la validación para poder realizar la implementación.