A quick note to warn anyone that is planning to use the Salesforce Marketing Cloud and the affect on your data storage costs.
If you are setting up Marketing cloud watch out the affect it can have on data storage costs if you use Marketing Cloud connect. Marketing Cloud connect allows you to use data from Sales and Service cloud, however it also can store huge amounts of data on the Sales and Service cloud when you run your campaigns. You should look at configuring the Marketing cloud to only store aggregated email data in the Sales and Service cloud otherwise you may cause the customer to hit their data limits.
Steve Fouracre Blog
Email me at sfouracre@selfevolvingsoftware.com
My App
Salesforce Ideas
LinkedIn
Salesforce Profile
Search
Friday 11 August 2017
Monday 26 December 2016
A Great Example Of What Can Be Made on Salesforce For Estate Agents
Tuesday 31 May 2016
Useful Summer 16 Features
Cloning Sandboxes
You
can now create a sandbox by cloning an existing sandbox rather than using
your production org as your source. Save time by populating any type of
sandbox with a previously chosen set of data and metadata. (Sandbox
templates, which serve a similar purpose, are available only for Full or
Partial Copy Sandboxes.) Sandbox-to-sandbox cloning facilitates iterative
development, allowing you to freeze development in one sandbox and pick up
where you left off in a new one. This feature is available in both Lightning
Experience and Salesforce Classic.
|
Simplify Development of Asynchronous Callouts by
Using Named Credentials
You
can now use named credentials in long-running asynchronous callouts from a
Visualforce page’s controller. A named credential specifies the URL of a
callout endpoint and its required authentication parameters in one
definition. Salesforce manages all authentication for callouts that specify a
named credential as the callout endpoint so that your Apex code doesn’t have
to. You can also skip remote site settings, which are otherwise required for
callouts to external sites, for the site defined in the named credential.
This feature is available in both Lightning Experience and Salesforce Classic.
|
Get a Map of Populated SObject Fields
We’ve
added a method on the Apex SObject class that makes it more efficient to
iterate over fields that have been populated in memory. Previously, iterating
over just the populated the fields of an SObject involved some complicated
programming. For example, if you queried an SObject using SOQL, it wasn’t
easy to tell which fields were returned. In Summer ’16, we’ve introduced a
new method on the SObject class that returns a map of populated field names
and their corresponding values:
Account
a = new Account();
a.name
= 'TestMapAccount1';
insert
a;
a
= [select Id,Name from Account where id=:a.Id];
Map
fieldsToValue = a.getPopulatedFieldsAsMap();
for
(String fieldName : fieldsToValue.keySet()){
System.debug('field name is ' + fieldName
+ ', value is '
+ fieldsToValue.get(fieldName));
}
Admittedly
the example by Salesforce above isn’t amazing but if the SOQL was a dynamic
SOQL I can see a lot of benefit in this
You
can also perform without a SOQL
Account
a = new Account();
a.name
= 'TestMapAccount1';
insert
a;
Map
fieldsToValue = a.getPopulatedFieldsAsMap();
for
(String fieldName : fieldsToValue.keySet()){
System.debug('field name is ' + fieldName
+ ', value is ' +
fieldsToValue.get(fieldName));
}
|
EmailTemplate Functions
RenderEmailTemplateBodyResult
Class
getMergedBody() Returns the rendered body
text with merge field references replaced with the corresponding record data.
System.Messaging Class
renderEmailTemplate(whoId,
whatId, bodies)
Returns
an array of RenderEmailTemplateBodyResult objects, each of which corresponds
to an element in the
supplied
array of text bodies. Each RenderEmailTemplateBodyResult provides a success
or failure indication
along
with either an error code or the rendered text.
renderStoredEmailTemplate(templateId,
whoId, whatId)
Renders
a text, custom, HTML, or Visualforce email template that exists in the
database into an instance of
Messaging.SingleEmailMessage.
|
$Resource Global Value Provider And Lightning
The
$Resource global value provider lets you reference images, style sheets, and
JavaScript code you’ve uploaded in static resources. Using $Resource lets you
reference assets by name, without worrying about the gory details of URLs or
file paths. You can use $Resource in Lightning components markup and within
JavaScript controller and helper code. To reference a specific resource in
component markup, use $Resource.resourceName within an expression. Include
CSS style sheets or JavaScript libraries into a component using the tag. To
obtain a reference to a static resource in JavaScript code, use $A.get('$Resource.resourceName').
|
Saturday 21 May 2016
Get a Map of Populated SObject Fields
We’ve added a method on the Apex SObject class that makes it more
efficient to iterate over fields that have been populated in memory.
Previously, iterating over just the populated the fields of an SObject involved
some complicated programming. For example, if you queried an SObject using
SOQL, it wasn’t easy to tell which fields were returned. In Summer ’16, we’ve
introduced a new method on the SObject class that returns a map of populated
field names and their corresponding values:
Account a = new Account();
a.name = 'TestMapAccount1';
insert a;
a = [select Id,Name from Account where
id=:a.Id];
Map<String, Account>
fieldsToValue = a.getPopulatedFieldsAsMap();
for (String fieldName :
fieldsToValue.keySet()){
System.debug('field name is ' +
fieldName + ', value is ' +
fieldsToValue.get(fieldName));
}
|
Admittedly the example by Salesforce
above isn’t amazing but if the SOQL was a dynamic SOQL I can see a lot of
benefit in this
You can also perform without a SOQL
ld names and their corresponding values:
Account a = new Account();
a.name = 'TestMapAccount1';
insert a;
Map<String, Account>
fieldsToValue = a.getPopulatedFieldsAsMap();
for (String fieldName : fieldsToValue.keySet()){
System.debug('field name is ' +
fieldName + ', value is ' +
fieldsToValue.get(fieldName));
}
|
Sunday 20 March 2016
Salesforce PageBlockSections Styling Problems
When you want to render displays of parts of a PageBlockSection because PageBlockSections and PageBlockSectionItems do not take the attribute Rendered you have to use Outputpanels to turn on and off the display of the PageBlockSection.
The problem with this is that the Outputpanel interferes with the styling of the PageBlockSection and the PageBlockSection does not display properly.
This can be seen in this example
( you will need to create a controller class with the visualforce page ):
The aim of this visualforce page is to simply display a button and when the button is pressed the entire Outputpanel is displayed.
The problem with this is that the Outputpanel interferes with the styling of the PageBlockSection and the PageBlockSection does not display properly.
This can be seen in this example
( you will need to create a controller class with the visualforce page ):
The aim of this visualforce page is to simply display a button and when the button is pressed the entire Outputpanel is displayed.
<apex:form>
<apex:pageBlock >
<apex:outputPanel id="Results">
<apex:pageBlockSection title=""
columns="1" collapsible="false">
<apex:outputPanel rendered="{!displaySelectedFrame}" >
<apex:pageBlockSectionItem >
<apex:outputPanel >
<apex:outputLabel value="A Label"
/>
<h1>{! frameworkName}</h1>
</apex:outputPanel>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
<apex:outputPanel >
<apex:outputLabel value="Description"
/>
{! description}
</apex:outputPanel>
</apex:pageBlockSectionItem>
</apex:outputPanel>
<apex:pageBlockSectionItem >
<apex:commandButton
value="Display Section" action="{!doSomething}"
rerender="Results" />
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
</apex:outputPanel>
</apex:pageBlock>
</apex:form>
|
If you move the rendered to inside the PageBlockSectionItems it solves the styling issue but the entire section of the intended rendered area no longer collapses the PageBlockSectionItems so when the page starts there is a large white space created by the PageBlockSectionItems and so the styling is still not correct. This is shown in the following:
<apex:form>
<apex:pageBlock >
<apex:outputPanel id="Results">
<apex:pageBlockSection title=""
columns="1" collapsible="false">
<apex:pageBlockSectionItem >
<apex:outputPanel rendered="{!displaySelectedFrame}">
<apex:outputLabel value="A Label"
/>
<h1>{! frameworkName}</h1>
</apex:outputPanel>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
<apex:outputPanel rendered="{!displaySelectedFrame}"
>
<apex:outputLabel value="Description"
/>
{! description}
</apex:outputPanel>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
<apex:commandButton
value="Display Section" action="{!doSomething}" rerender="Results" />
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
</apex:outputPanel>
</apex:pageBlock>
</apex:form>
|
To solve both situations use a PageBlockSection inside the Outputpanel that renders the section that we are wanting to turn on/off the display. By having 2 PageBlockSections the problem is solved.
<apex:form>
<apex:pageBlock >
<apex:outputPanel id="Results">
<apex:pageBlockSection columns="1"
collapsible="false">
<apex:outputPanel layout="block"
rendered="{!displaySelectedFrame}" >
<apex:pageBlockSection columns="1"
collapsible="false">
<apex:pageBlockSectionItem >
<apex:outputPanel >
<h1><apex:outputLabel value="A Label"
/></h1><br/>
{!
frameworkName}
</apex:outputPanel>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
<apex:outputPanel >
<h1><apex:outputLabel value="Description"
/></h1><br/>
{!
description}
</apex:outputPanel>
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
</apex:outputPanel>
<apex:pageBlockSectionItem >
<apex:commandButton
value="Display Section" action="{!doSomething}"
rerender="Results" />
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
</apex:outputPanel>
</apex:pageBlock>
</apex:form>
|
Saturday 5 March 2016
Trigger Pattern Framework - session 2
In my last blog I introduced my Trigger Pattern Framework, now I will cover Trigger Control, which is one of the main improvements to other well documented frameworks.
DMLs are very processor intensive
and conserving this precious resource in a multitenant environment is ever more
so important.
So I built into my framework a
capability using a Custom Setting to activate / deactivate per trigger for any
User, Profile, or the entire Organization; or with a separate Custom Setting to
be able to activate / deactivate ALL triggers for any User, Profile, or the
entire Organization. This was great in situations where companies require
migrating data from 1 system to another and you want to safeguard that no
unwanted actions occur through the execution of code in the trigger.
However there are also many
situations such as in unit tests where we create test data, to avoid running
through all the code in triggers which is not necessary unless we are testing
specifically the triggers to disable the triggers using the Custom Settings
above will require running a DML and since we are trying to avoid DMLs because
they are expensive to run we need another mechanism to bypass the code in the
trigger, therefore we introduce a static variable to do this work. Here is a
section in the TriggerFactory class that controls the execution using these
methods and also the code from the calling classes that are used in this part
of the framework.
boolean notriggerSetting;
boolean
noTriggersPerObject;
try{
notriggerSetting = TriggerController.globalTriggerControlSetting();
noTriggersPerObject =
TriggerController.globalTriggerPerObjectControlSetting(objType);
}
catch
(Exception ex){system.debug('error in trigger controller ' + ex); }
Type soType = Type.forName(objType);
if (!notriggerSetting &&
!noTriggersPerObject &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_ALL)) {
if (Trigger.isBefore){
if
(Trigger.isUpdate && !TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_UPDATE)){
handler.beforeUpdate(Trigger.oldmap,
Trigger.newmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_UPDATE), true);
}
else if
(Trigger.isDelete &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_DELETE)){
handler.beforeDelete(Trigger.oldmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_DELETE), true);
}
else if
(Trigger.isInsert &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_INSERT)){
handler.beforeInsert(Trigger.newmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_INSERT), true);
}
else if
(Trigger.isUnDelete &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_UNDELETE)){
handler.beforeUnDelete(Trigger.oldmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_UNDELETE), true);
}
}
else{
if
(Trigger.isUpdate &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_UPDATE)){
handler.afterUpdate(Trigger.oldmap,
Trigger.newmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_UPDATE), true);
}
else if
(Trigger.isDelete && !TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_DELETE)){
handler.afterDelete(Trigger.oldmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_DELETE), true);
}
else if
(Trigger.isInsert &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_INSERT)){
handler.afterInsert(Trigger.newmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_INSERT), true);
}
else if
(Trigger.isUnDelete &&
!TriggerController.getTriggerControlValue(soType,
TriggerController.TRIGGER_UNDELETE)){
handler.afterUnDelete(Trigger.oldmap,
Trigger.newmap);
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(soType, TriggerController.TRIGGER_UNDELETE), true);
}
}
}
|
public class
TriggerController {
public static map<TriggerControlKeyValue,
boolean> triggerDisableMap = new map<TriggerControlKeyValue,
boolean>();
public static map<TriggerControlKeyValue,
boolean> triggerSuccessMap = new map<TriggerControlKeyValue,
boolean>();
public static final String TRIGGER_ALL = 'ALL';
public static final String TRIGGER_INSERT =
'INSERT';
public static final String TRIGGER_UPDATE =
'UPDATE';
public static final String TRIGGER_DELETE =
'DELETE';
public static final String TRIGGER_UNDELETE =
'UNDELETE';
public static Boolean
getTriggerControlValue(System.Type objType, String triggerType){
TriggerControlKeyValue tkv = new
TriggerControlKeyValue(objType ,triggerType);
Boolean triggerDisable = false;
if (triggerDisableMap != null
&& triggerDisableMap.containskey(tkv))
triggerDisable =
triggerDisableMap.get(tkv);
return triggerDisable;
}
public static void setTriggerControlValue(System.Type
objType, String triggerType, Boolean triggerDisable){
TriggerControlKeyValue tkv = new
TriggerControlKeyValue(objType ,triggerType);
for (TriggerControlKeyValue eachtk
: triggerDisableMap.keyset()){
if (eachtk == tkv){
tkv
= eachtk;
break;
}
}
triggerDisableMap.put(tkv,
triggerDisable);
}
public static Boolean
getTriggerSuccessValue(System.Type objType, String triggerType){
TriggerControlKeyValue tkv = new
TriggerControlKeyValue(objType ,triggerType);
Boolean triggerSuccess = false;
for (TriggerControlKeyValue eachtk
: triggerSuccessMap.keyset()){
if (eachtk == tkv){
triggerSuccess
= triggerSuccessMap.get(eachtk);
break;
}
}
return triggerSuccess;
}
public static boolean globalTriggerControlSetting(){
return (((Triggers_Off__c.getOrgDefaults() != null)
? Triggers_Off__c.getOrgDefaults().value__c : false) ||
Triggers_Off__c.getInstance(UserInfo.getUserId()).value__c ||
Triggers_Off__c.getInstance(UserInfo.getProfileId()).value__c) ;
}
public static boolean
globalTriggerPerObjectControlSetting(String obj){
if (obj != null && obj != '') {
if (!obj.endswith('__c')) obj +=
'__c';
boolean s = false;
if
(Trigger_Per_Object__c.getOrgDefaults() != null) s = (boolean)Trigger_Per_Object__c.getOrgDefaults().get(obj);
boolean t = false;
if
(Trigger_Per_Object__c.getInstance(UserInfo.getUserId()) != null) t =
(boolean)Trigger_Per_Object__c.getInstance(UserInfo.getUserId()).get(obj);
boolean u = false;
if (Trigger_Per_Object__c.getInstance(UserInfo.getProfileId())
!= null) u =
(boolean)Trigger_Per_Object__c.getInstance(UserInfo.getProfileId()).get(obj);
if (s == null) s = false;
if (t == null) t = false;
if (u == null) u = false;
return (s || t ||
u);
}else
return false;
}
}
|
public class
TriggerControlKeyValue {
public system.type
objectType;
public string triggerType;
public TriggerControlKeyValue(system.type
thisObjectType, string thisTriggerType) {
objectType = thisObjectType;
triggerType = thisTriggerType;
}
public boolean equals(object obj){
if (obj instanceof
TriggerControlKeyValue){
TriggerControlKeyValue
t = (TriggerControlKeyValue)obj;
return
(objectType.equals(t.objectType) && triggerType.equals(t.triggerType));
}
return false;
}
public integer hashCode(){
return system.hashCode(objectType)
* system.hashCode(triggerType);
}
}
|
We also need to be able to unit
test the framework to test the trigger control has been built correctly and remains
operational. To test this part of the framework we don’t want to test the
outcomes from running each individual part of the trigger as the outcomes will
be different per trigger, instead we just need to test that the code passed
through the track of code we expect. For this purpose, another map is used.
@istest
public class
TriggerControllerTest {
public static TestDataCreation td = new
TestDataCreation();
public static Account acc;
public static Triggers_Off__c trig;
public static Trigger_Per_Object__c trigPerObject;
static{
acc = td.insertAccount(null);
}
static testMethod void AccountTriggerGlobalCSTest()
{
//test global CS on/off
trig = td.insertTriggersOff(null);
Test.startTest();
//record should be
inserted
//system.assert([Select
id From Account where Name=:defaultCusName].size() == 1);
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_INSERT)
== true);
//should change
acc.Name =
'ChangeCusName';
update acc;
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE)
== true);
//reset
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE),
false);
trig.value__c =
true;
update trig;
//should not change
acc.Name =
'DefaultCusName';
update acc;
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE)
== false);
//disable insert
test.stopTest();
}
static testMethod void
AccountTriggerPerObjectCSStaticTest() {
//test trigger control using Per
Object CS
trigPerObject =
td.insertTriggersPerObject(null);
Test.startTest();
//reset
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT),
true);
//record should be
inserted but shouldnt set
Account_Insert_Succeeded
trigPerObject.Account__c
= true;
update trigPerObject;
//reset
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT),
false);
system.assert([Select
id From Account].size() == 1);
Account acc2 =
td.insertAccount(null);
system.assert([Select
id From Account].size() == 2);
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_INSERT)
== false);
test.stopTest();
}
static testMethod void AccountTriggerGlobalStaticTest()
{
//test trigger control using static
variables
Test.startTest();
//disable update
TriggerController.triggerDisableMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_ALL), true);
acc.Name =
'ChangeCusName';
update acc;
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE)
== false);
//reset
TriggerController.triggerDisableMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_ALL), false);
TriggerController.triggerDisableMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE),
true);
acc.Name =
'DefaultCusName';
update acc;
//should not change
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE)
== false);
//update should run
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE),
false);
TriggerController.triggerDisableMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE),
false);
acc.Name =
'DefaultCusName';
update acc;
system.debug('##
TriggerController ' + TriggerController.triggerSuccessMap);
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE)
== true);
//test insert
trigger code off
TriggerController.triggerSuccessMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT),
false);
TriggerController.triggerDisableMap.put(new
TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT),
true);
system.assert([Select
id From Account].size() == 1);
Account acc2 =
td.insertAccount(null);
system.assert([Select
id From Account].size() == 2);
//should not change
system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_INSERT)
== false);
test.stopTest();
}
}
|
Subscribe to:
Posts (Atom)