5

I'm having some problems when trying to make a number of classes and triggers to run on a dispatcher class. The components for this architecture are the following:

  1. One -TriggerDispatcher class.
  2. One trigger - MainAttachmentEntry
  3. One Apex class - AttachmentVisitReport

The first point for this architectural to work is the MainAttachmentEntry Code bellow:

trigger MainAttachmentEntry on Attachment (after delete, after insert, after update, before delete, before insert, before update)
{

TriggerDispatcher.entry(new  TriggerDispatcher.TriggerParameters(Trigger.isBefore, Trigger.isAfter, Trigger.isDelete, Trigger.isInsert, Trigger.isUpdate, Trigger.isExecuting, Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap));
}

This is packet that contains all trigger context variables, in this case except after undelete. This design is based on Adam Purkins dispatcher.

The MainAttachmentEntry trigger calls the TriggerDispatcher class and the entry() method.

Here is the TriggerDispatcher class:

public with sharing class TriggerDispatcher 
{

private static final String SACCOUNT = 'Account'; 
private static final String SCONTACT = 'Contact';
private static final String SLEAD = 'Lead';
private static final String SOPPORTUNITY = 'Opportunity';
private static final String SUSER = 'User'; 
private static final String SEVENT = 'Event'; 
private static final String SATTACHMENT = 'Attachment';
private static final String SFEEDITEM = 'FeedItem';

public static ITriggerEntry activeInstance = null; 
public static Map<Id, SObject> sObjectsToUpdate = new Map<Id, SObject>(); 



public interface ITriggerEntry
{
    void mainEntry(TriggerParameters tp);
    void inProgressEntry(TriggerParameters tp);     

}

public class TriggerParameters
{
    public String triggerObject {get; private set;}
    public Boolean isBefore {get; private set;}
    public Boolean isAfter {get; private set;}
    public Boolean isDelete {get; private set;}
    public Boolean isInsert {get; private set;}
    public Boolean isUpdate {get; private set;}
    public Boolean isExecuting {get; private set;}
    public List<SObject> oldList {get; private set;}
    public List<SObject> newList {get; set;}
    public Map<Id, SObject> oldMap {get; private set;}
    public Map<Id, SObject> newMap {get; set;}

    public TriggerParameters(Boolean ib, Boolean ia, Boolean id, Boolean ii, Boolean iu, Boolean ie,
        List<SObject> ol, List<SObject> nl, Map<Id, SObject> om, Map<Id, SObject> nm )
        {
            this.isBefore = ib;
            this.isAfter = ia;
            this.isDelete = id;
            this.isInsert = ii;
            this.isUpdate = iu;
            this.isExecuting = ie;
            this.oldList = ol; 
            this.newList = nl;
            this.oldMap = om;
            this.newMap = nm;
            this.triggerObject = getSObjType((this.oldList != null && this.oldList.size() > 0) ? 
            this.oldList[0] : this.newList[0]);
        } 

        private String getSObjType(SObject so)
        {
            String retVal; 
            if(so instanceof Account) retVal = SACCOUNT; 
            else if( so instanceof Contact) retVal = SCONTACT; 
            else if( so instanceof Lead) retVal = SLEAD; 
            else if( so instanceof Opportunity) retVal = SOPPORTUNITY;
            else if( so instanceof User) retVal =SUSER;
            else if( so instanceof Event) retVal = SEVENT; 
            else if( so instanceof Attachment) retVal = SATTACHMENT;
            else if( so instanceof FeedItem) retVal = SFEEDITEM; 
            return retVal; 

        }
}

//Central dispatch entry
public static void entry(TriggerParameters tp)
{
    if(activeInstance == null)
        processWork(tp);
        else
        activeInstance.inProgressEntry(tp);

}

//Order of execution is controlled

private static void processWork(TriggerParameters tp)
{
    if(tp.triggerObject == SATTACHMENT && tp.isAfter && tp.isInsert)
    {

      execute(new AttachmentVisitReport(), tp);

    }

      if(sObjectsToUpdate.size() >0)
            update sObjectsToUpdate.values();

}

private static void execute(ITriggerEntry ite, TriggerParameters tp)
{

    activeInstance = ite;
    activeInstance.mainEntry(tp);
    }
}

The entry() actually will pass the TriggerParametes tp and also make sure that this is the first time we are running those values within the TriggerParameters. If that is the case it will jump to another method, processWork(). There I can add the criteria for the using and if statement. In this case we are making sure that if the object passed on the TriggerParametes is of type Attachment, to execute a class AttachmentVisitReport.

Here is the code for AttachmentVisitReport. This class implements ITriggerEntry interface which it is inside the TriggerDispatcher class:

public with sharing class AttachmentVisitReport implements TriggerDispatcher.ITriggerEntry
{

public void inProgressEntry(TriggerDispatcher.TriggerParameters tp)
{
    //Logic .....
}

public void mainEntry(TriggerDispatcher.TriggerParameters tp)
{

    Map<Id, Attachment> newMap = (Map<Id, Attachment>) tp.newMap;
    Map<Id, Attachment> attMap = new Map<Id, Attachment>([SELECT ParentId FROM Attachment WHERE Parent.Type = 'Event' AND Id IN :newMap.keySet()]);
    Set<Id> eveIds = new Set<Id>(); //Event Ids
    Set<Id> accIds = new Set<Id>(); // Account Ids for Events  
    Set<Id> ownIds = new Set<Id>(); // Event OwnerIds 

    for(Attachment a: attMap.values())

        if(!attMap.isEmpty())

        eveIds.add(a.ParentId); 


    Map<Id, Event> eveMap = new Map<Id, Event>([SELECT OwnerId, WhatID FROM Event WHERE Id IN : eveIds]);

    for(Event e : eveMap.values())
    {
        if(!eveMap.isEmpty())

        accIds.add(e.WhatId); 
        ownIds.add(e.OwnerId); 


    List<EntitySubscription> entityListAcct = [SELECT id, ParentId, SubscriberId FROM EntitySubscription WHERE ParentId IN :accIds];
    List<EntitySubscription> entityListOwn = [SELECT id, ParentId, SubscriberId FROM EntitySubscription WHERE ParentId IN :ownIds];

    for( EntitySubscription en :entityListAcct)
    {
        if(!entityListAcct.isEmpty())
        {
        FeedItem feedItemAcct = new FeedItem(
        Body='Update done to Visit Report. Attachment inserted or updated',
        ParentId = en.ParentId);

        insert feedItemAcct;  


        TriggerDispatcher.sObjectsToUpdate.put(feedItemAcct.id , feedItemAcct);
         }
       }
     }
   }
 }

The intention of this code is to add a Feed for those following the Owner of events if those contain an attachment. It also tries to do the same for Accounts followers if there is an Event related to an Account. Actually he logic is:

If an Event has WhatId = Account and an Attachment is inserted in the Event, a new feed should be send to the Event owner followers and to the Account followers.

I tried different things but is not inserting the Feed. On this code I'm just trying to do this which new attachments and only for Account followers. This section:

for( EntitySubscription en :entityListAcct)
    {
        if(!entityListAcct.isEmpty())
        {
        FeedItem feedItemAcct = new FeedItem(
        Body='Update done to Visit Report. Attachment inserted or updated',
        ParentId = en.ParentId);

        insert feedItemAcct;  


        TriggerDispatcher.sObjectsToUpdate.put(feedItemAcct.id , feedItemAcct);

I tried to change the SObjectToUpdate to do a insert rather than update ... but is not inserting the Feed. So, can anyone give some directions on how to make this work? Thanks.

1
  • all the isEmpty() tests are pointless
    – cropredy
    Commented Feb 22, 2016 at 4:24

2 Answers 2

3

I'm not sure I can help you fix your code, but maybe you'll be interested in this design pattern that accomplishes pretty much the same thing. I'm not sure who the original author is, but it's included in the MavensMate sublime text plugin as a class template.

First, add this universal "TriggerHandler" utility class to your code base:

//This is a utitliy class that can be used to help manage triggers.  
// For each object, create 1 "Master" Trigger that can handle each event.  Then for each peice of funcitonality,
// create a class and implement "TriggerHandler.HandlerInterface".  Write all the trigger logic in the "void handle()"
// method just as you would directly in a trigger.  The one difference is that Trigger.new and Trigger.old must be explicitly cast to the concrete SOBJECT
// EX:
//  * for(System__c sys : (List<System__c>) trigger.new){}
//  * Account old = (Account) trigger.old[i];
//In the master trigger:
// 1: Instante a new Trigger Handler Obj: TriggerHandler handler = new TriggerHandler();
// 2: Bind any logic to the desired events: handler.bind(TriggerHandler.Evt.beforeupdate, new MyHandlerClass());
//   Make sure that the event bound is executed by the trigger!!!
// 3: call manage: handler.manage();
public class TriggerHandler 
{
    /**
     *   Enum representing each of before/after CRUD events on Sobjects
     */
    public enum Evt 
    {
        afterdelete, afterinsert, afterundelete,
        afterupdate, beforedelete, beforeinsert, beforeupdate   
    }

    /*
     *   Simplistic handler to implement on any of the event. It doesn't require or enforces any pattern except the
     *   method name to be "handle()".
     */
    public interface HandlerInterface
    {
        void handle();          
    } 

    // Internal mapping of handlers
    Map<String, List<HandlerInterface>> eventHandlerMapping = new Map<String, List<HandlerInterface>>();

    /**
     *   Core API to bind handlers with events
     */
    public TriggerHandler bind(Evt event, HandlerInterface eh)
    {
        List<HandlerInterface> handlers = eventHandlerMapping.get(event.name());
        if (handlers == null) 
        {
            handlers = new List<HandlerInterface>();
            eventHandlerMapping.put(event.name(), handlers);
        }
        handlers.add(eh);
        return this;
    }

    /**
     *   Invokes correct handlers as per the context of Trigger and available registered handlers
     */
    public void manage()
    {
        Evt ev = null;
        if(Trigger.isInsert && Trigger.isBefore)
        {
            ev = Evt.beforeinsert;
        }
        else if(Trigger.isInsert && Trigger.isAfter)
        {
            ev = Evt.afterinsert;
        }
        else if(Trigger.isUpdate && Trigger.isBefore)
        {
            ev = Evt.beforeupdate;
        }
        else if(Trigger.isUpdate && Trigger.isAfter)
        {
            ev = Evt.afterupdate;
        }
        else if(Trigger.isDelete && Trigger.isBefore)
        {
            ev = Evt.beforedelete;
        }
        else if(Trigger.isDelete && Trigger.isAfter)
        {
            ev = Evt.afterdelete;
        }
        else if(Trigger.isundelete)
        {
            ev = Evt.afterundelete;             
        }

        List<HandlerInterface> handlers = eventHandlerMapping.get(ev.name());

        if (handlers != null && ! handlers.isEmpty()) 
        {
            for (HandlerInterface h : handlers) 
            {
                h.handle();
            }
        }
    }

}

For each operation your want to run, create implement the handler interface:

public with sharing class MyAttachmentOperation1 implements TriggerHandler.HandlerInterface {
    public void handle(){
       for(Attachment attach : (List<Attachment>) Trigger.new){
       //do something...
       }
    }
}

Finally create your master trigger for attachment:

//Master trigger for all events on attachment.
// Each individual action should implement TriggerHandler.HandlerInterface
// If binding a new event, make sure that it is uncommented from the trigger def
//See comments in TriggerHandler for more details on the pattern
trigger AttachmentMasterTrigger on Attachment (
    before update,
    after insert,
    before insert
    //after update, 
    //before delete, 
    //after delete, 
    //after undelete
    ) {

    //add action for project license date trigger handler
    TriggerHandler th = new TriggerHandler();
    th.bind(TriggerHandler.Evt.beforeinsert, new MyAttachmentOperation1());
    th.bind(TriggerHandler.Evt.beforeupdate, new MyAttachmentOperation2());
    th.bind(TriggerHandler.Evt.afterinsert, new MyAttachmentOperation3());

    //execute bindings
    th.manage();

}
3
  • Thanks for your input NSjonas. I have been working on this design for a few days and I really need to understand why is not inserting the Feeds. There are a number of patterns that can be utilize to handle trigges but I really want to push this one. Thanks again for your feedback Commented May 30, 2015 at 8:54
  • @NSjonas - When I try to save the Trigger it gives me the following error - Variable does not exist: TriggerHandler.Evt Am I missing something? Commented Aug 31, 2018 at 18:19
  • @AdityaPatil 1: make sure that TriggerHandler.cls successfully compiled 2: Make sure that you trigger is correctly referencing one of the enum values. EG: TriggerHandler.Evt.beforeinsert
    – NSjonas
    Commented Aug 31, 2018 at 19:50
1

I find the following pattern much simpler...not sure if it helps you but I hope you find it interesting. I like the following pattern because:

  • It is really easy to answer a question like "What happens to a Lead record before insert?" at one glance.
  • It fits in well with the Selector library, which can do a lot of the work for you in filtering records.
  • Just my opinion, but it is a lot tidier.

    • No String variables are needed to represent SObjectType.
    • No Enum is needed to represent trigger event state.
    • Each event is represented concretely by one method.
    • Each handler deals with only one SObjectType.
  • I also like its separation of concerns.

    • Trigger knows when to act
    • Handler knows what actions to perform
    • Service knows how to perform actions

Trigger

trigger Lead on Lead (before insert, after insert, before update, after update)
{
    LeadTriggerHandler handle = new LeadTriggerHandler(trigger.new, trigger.oldMap);

    if (trigger.isBefore)
    {
        if (trigger.isInsert) handle.beforeInsert();
        if (trigger.isUpdate) handle.beforeUpdate();
    }
    if (trigger.isAfter)
    {
        if (trigger.isInsert) handle.afterInsert();
        if (trigger.isUpdate) handle.afterUpdate();
    }
}

Handler

public with sharing class LeadTriggerHandler
{
    @TestVisible static Boolean bypassTrigger;

    final List<Lead> newRecords;
    final Map<Id, Lead> oldMap;
    public LeadTriggerHandler(List<Lead> newRecords, Map<Id, Lead> oldMap)
    {
        this.newRecords = newRecords;
        this.oldMap = oldMap;
    }

    public void beforeInsert()
    {
        if (bypassTrigger) return;
        LeadServices.setSomeFields(newRecords);
    }
    public void afterInsert()
    {
        if (bypassTrigger) return;
        LeadServices.updateSomeRelatedRecords(newRecords);
    }

    public void beforeUpdate()
    {
        if (bypassTrigger) return;
        LeadServices.setSomeFields(newRecords);
    }
    public void afterUpdate()
    {
        if (bypassTrigger) return;
        LeadServices.updateSomeRelatedRecords(newRecords);
    }

    // etc
}

Service

public with sharing class LeadServices
{
    public static void setSomeFields(List<Lead> leads)
    {
        // do stuff
    }
    public static void updateSomeRelatedRecords(List<Lead> leads)
    {
        // do stuff
    }
}
3
  • Adrian -- if you have not done so already, check out Fawcett's Force.com Enterprise Architecture for a broader set of patterns (his Domain Pattern addresses triggers)
    – cropredy
    Commented Feb 22, 2016 at 4:25
  • @crop1645 Probably won't pony up for it unless I start studying for the TA exam, but I'll keep it in mind. Thanks! I spent a lot of time crafting the best trigger handler pattern for me and I won't be easy to sway to another pattern. :)
    – Adrian Larson
    Commented Feb 22, 2016 at 4:29
  • I know the switching cost argument and on an existing org it is high. But reading the book is well worth it as it made clear to me that I could do better.
    – cropredy
    Commented Feb 22, 2016 at 4:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .