关于Stub API

Spring’17,Salesforce为开发者提供了一个强大的工具,Apex STUB API

看完官方文档提供的例子之后,感觉确实很美好,但又觉得没什么机会用。
不同于Java项目对框架和设计模式的极致追求。
我接手的Apex代码均为意识流散文诗式写法。根本没有抽象化,解耦等操作。

Stub Api是基于Java的Mocking Framework
mockito开发的。
使用Mocking Framework的前提是代码必须进行解耦,就是所谓的依赖注入(Dependency Injection)。
要求代码不能按业务直接写成流水账,而是将模块之间的强依赖解开。

比如,A类的构造强依赖与B类,如果没有B则没有A。

A a = new A();
//-----------------------------------
Class A {
    private B b;
    public A() {
        b = new B();
    }
    public String getName() {
        return b.getName();
    }
}

Class B {
   public String getName() {
       return "I'm the B";
   }
}

那么,问题来了,如果我有一个C类,返回“I’m the C”,我就要为C类新建一个A1类,在A1类中将A1与C进行强依赖。
如果需求越来越多,代码也随时越来越膨胀。
那么怎么改变这个局面呢?

B b = new B();
A a = new A(b);
-----------------------------------------------
Class A {
    private CustomObject co;
    public A(CustomObject cObj) {
        this.co = cObj;
    }
    public String getName() {
        reutrn co.Name;
    }
}

Class B extends CustomObject {
    public String getName() {
        return "I'm the B";
    }
}
abstract Class CustomObject {
    abstract public String getName();
}

这样一来,A和B的强依赖就解耦了,不再是每次new一个A,就必然得到B的内容,而是根据我传进去的B来决定A返回的内容。
如果想返回“I’m the C”,就不用再去新建一个A1类,想返回“I’m the D”,不用再去新建一个A2类。

只有依赖注入的写法才能进行方便的Mocking。

Stub Api可以做到的是,在解耦之后,为A类mock一个B类,C类。做到完美的单元测试(模块隔离)。
Whatever,用不上。至今还没见到过有控制反转思想的的Apex。
说句题外话,apex-enterprise-patterns(官方介绍文章)到现在还没推广开呢。

// 未完待续

Salesforce的StandardSetController使用Demo

先放Github地址
https://github.com/Kealthals/Salesforce-StandardSetControllerDemo.git

事情起因,是有人问我为什么StandardSetController无法保存Selected状态。
我觉得既然标准List View可以,那么使用StandardSetController应该也可以。
虽然这个Class我用的也不多,但还是动手写个一个小Demo验证了一下我的想法,虽然花了些小心思,但证明确实可行。
验证完毕,转念一想,既然已经动手了,就干脆把所有的methods都演示一遍吧。权当给自己留一个财富。

StandardController大家都比较熟悉,用来处理单条数据。模拟的是Create/Edit页面和Detail页面。
StandardSetController用的不多,因为模拟的是List View页面。标准List View页面已经很强大了,很少会遇到这种需求。
这个Demo也就是尽量做一个和标准List View类似的页面,时间有限,肯定做不到标准页面一般的好看和完美。
最低目标是将这个Class提供的method都使用一遍,除了一个不明所以的method之外,最低目标我觉得是完成了。
功能上,也提供了基本的
1. 罗列数据
2. 翻页
3. 选择数据(Selected)
4. 选择List View
5. 改变Page Size
6. 跳转指定页
7. Inline Edit(还不完美)
作为一个独立功能来讲,勉强达到了凑合着用的程度。
在功能实现的时候,又不断遇到了一些或新或旧的小问题,在大家的帮助下都找了比较好的小方案解决掉了这些小问题。
比如SFDC的autofocus,inputtext的输入类型限制等等。

继续阅读“Salesforce的StandardSetController使用Demo”

如何优雅的干掉VF的autofocus

How to remove VF input field’s autofocus?
最近在写StandardSetController Demo的时候遇到了一个逼死完美主义者的问题。
在VF加载之后,SFDC会非常Nice的,热心的,把第一个文本输入框设为焦点。
我推测sfdc的产品设计逻辑是下面这样的。
嗯。。。你设置一个输入框,代表着用户一定得输入点什么才能继续。
既然用户必须输入点什么的话,那么就必须用鼠标点一下这个输入框。
那么让我们来做点什么吧,帮用户先把输入框点上怎么样,节省用户的操作。
看呐,如果用户双手都在键盘上,他们连右手离开键盘的工夫都省下来了,用户一定会非常满意的。

对于大部分业务场景,这个贴心的小设计确实很有用。但有的时候会起反作用。
比如我的StandardSetController的Demo。
在List View画面,一般只有在数据列表最下方才有输入框,为了输入Page Size或者Page No。
但是不碰这几个输入框并不影响我使用List View。
不过,当默认Page Size非常大,画面超出一屏的时候,由于sfdc自动把第一个输入框设成焦点,导致画面滚动条直接被拖到了滚动条最下面。
还有当VF的第一个input项目是Date型的时候,sfdc的自动设置焦点机制,会触发datepicker,就像大晴天自动弹开的雨伞一样,造成另一场灾难。

研究了半天,虽然标准List View并没有这个问题,但还是没有找到比较官方的解决方法。
只好Hack一下了。

在VF中插入

   <script>
        function setFocusOnLoad() {}
    </script>

问题完美解决。

关于Salesforce的15位ID与18位ID

众所周知,Salesforce的Id有15位和18位两种。
18位ID的前15位与15位版本相同。
比如,有一条Account,其URL上的Id为0016F00002Dbbt5。
其中头三位001为Account的prefix(关于prefix,参照《salesforce的prefix和suffix》)
四到六位的6F0为Org Id的第4到6位。由于OrgId的唯一性,所以每个ID在整个SFDC世界都是唯一的。

同样是这条Account,使用工具取得的Id为18位的0016F00002Dbbt5QAB。
前15位与15位版本的ID相同,后3位则是根据前15位计算得来。

插个题外话,Org Id为00D开头的15或18位ID,第四位代表org所属的Instance。
比如说,我的OrgId为00D6F0XXXXXXXXX,那么此org所属的Instance是6对应的NA4。
第四位代码与Instance的对应关系表,请参照《Instance代码对照表
通过这个对照表,可以轻松的通过Org Id知道其所属的Instance。

包括某些官方文档在内,都将15位ID称为15位大小写敏感ID(the 15-character case-sensitive ID),18位ID称为18位大小写不敏感ID(the 18-character case-insensitive ID)。
那么问题来了,假如18位ID大小写不敏感,那是否就意味着
1. 0016F00002Dbbt5QAB
2. 0016F00002DBBT5QAB (全大写)
3. 0016f00002dbbt5qab (全小写)
代表同一条数据吗?
继续阅读“关于Salesforce的15位ID与18位ID”

Salesforce的Custom Permission

Salesforce的权限体系我相信大家已经很熟了。
数据级别权限,表级别权限,字段级别权限,功能权限,分别坐落在Profile/Permission Set和role/Sharing rule当中,还有一些是作为Feature License放在了User上。

可是对于自定义功能,SFDC并没有提供太好的控制方式。
逼的广大开发人员不得不开各种脑洞。
比如在人事系统里,HR和领导能看见员工评价,普通员工看不到员工评价,
这用Profile的FLS简简单单地就能控制了。
但是在所有人都能看到的VF上,假如有个员工评价Section不想给普通员工看的话,就不好办了。
在VF里可以判断当前User的Profile,判断一下当前用户的Profile在不在允许列表里不就行了嘛。
好家伙,一打听,500个Profile能看。这不行啊,还是看看谁不能看吧,呦呵,800个Profile。
开发人员就开脑洞了,我把能看的Profile的末尾加一个“VIP”,不能看的都没有。接着就动手把500多个Profile一个一个的改了名字。
然后在VF里判断User的Profile结尾是VIP的才显示。
妥了,实现,下班。
第二天,经理过来搂住你的肩旁,说,昨天那个功能实现的很好,今天还要再加一个Section,和昨天的差不多,不过普通员工Level里的组长可以也可以看。开发人员想了想,总不能再给Profile加一个后缀吧。就算这次可以,那以后再来一个怎么办。。。。。
后来,听说开发人员被120拉走了。

像这种自定义功能的权限控制,SFDC提供了Custom Permission,作为Profile与Permission Set向自定义功能领域的延伸。
说白了,就是Profile与Permission Set原本能控制的都是标准功能,Custom Permission就是一个插件/拓展包,赋予了Profile与Permission Set控制自定义功能的能力。

所以遇见上面的场景,我们只需要针对每一个Section建一个Custom Permission,
然后将这些Custom Permission挂到需要此权限的Profile或者Permission Set,
然后在VF中写上Custom Permission的判断。
如下方官方文档提供的例子一样

<apex:pageBlock rendered="{!$Permission.[Your Custom Permission API Name]}">
   <!-- Confidential Content Here -->
</apex:pageBlock>

在Apex中想用Custom Permission控制业务逻辑的话,
就要使用下面的方法,官方文档

if(FeatureManagement.checkPermission([Your Custom Permission API Name])) {
    // Your Codes Here
}

这样一来,想控制一个群体,就挂在Profile上,想控制个别人,就挂在Permission Set上。
为什么叫“挂在”呢,因为只有在Profile和Permission Set下面才有添加Custom Permission的按钮。

那么,想创建一个Custom Permission,在哪创建呢?
第一种方式是官方文档常写的 —– 点击Setup,然后左上方Quick Find,输入Custom Permissions。
第二种方式是按照本博客的风格 —– Setup -> Build -> Develop -> Custom Permissions。
Custom Permission本身需要定义的内容倒是不多,就一个Label一个API Name是必须的,其他都可有可无。

虽然Custom Permission只是Profile与Permission Set的功能拓展,
但由于在VF中官方提供了方便的检查方式($Permission),倒是给我们提供了另外一个用途,
正所谓,有心栽花花不开,无心插柳柳成荫。

继续阅读“Salesforce的Custom Permission”

URL Hacking之动态report条件

上回书说道(《salesforce的field-id与url-hack》),URL Hacking可以用来跳转到指定的页面与设定新建页面的default值。

除此之外,URL Hacking还可以用来改变report的条件。
比如说,有这样的业务场景————客户想知道当前访问的Lead都被哪些特定种类Campaign纳入为Campaign Member。
有的同学说了,这个简单啊,不就是Releted List嘛,我知道。
但是,由于Releted List无法进行过滤筛选,所以所有种类的Campaign信息都混在了一起,客户不矫情的话也许勉强凑合着用了。

除了做VF页面之外,还可以使用URL Hacking来实现。
首先,在Lead的详细画面上放一个custom link,点击之后打开一个Campaign Member的report。
然后设置好report的Show范围为“All Campaigns”,如果是其他report,还要设定好时间范围。
只要实现每次点击custom link跳转到report时候,都能动态的设置campaign的record type与当前lead的Id,就可以实现。

// 接下来是重点
继续阅读“URL Hacking之动态report条件”

本博客引用Salesforce官方文档的说明

最近有一些同学说我写的文章骗人。
弄得我一脸黑人问号。

深入交流了一下才知道,是说我提供的所有官方链接都打不开。
无论点开哪个链接都是404。

这里我不得不澄清一下,
我所引用的所有链接都是基于英文版的sfdc文档。
由于并不是所有的官方英文文档都有对应的中文版,
所以,浏览器默认语言是非英语的同学,很容易会碰到404错误。

看到404没关系,如下图,将语言改成英语就可以了。

最后,还是想说一句,毕竟sfdc是英语母语者开发的,想好好做sfdc还是学好英语吧。

Salesforce的Add Campaign Member

// 更新 2018-10-12
该Issue已经被解决。

// 最下有更新 2018-05-08

最近发现了一个好玩的现象。
作为Salesforce Marketing核心功能之一,标准功能Campaign的出镜率非常高。

这次的问题就出在向Campaign添加Campaign Member上。

除了使用DataLoader与Data Import Wizard等数据导入工具以外,Salesforce在标准画面上提供了三个添加Campaign Member的渠道。
第一种为最为常见的方式,通过Campaign详细画面下方Campaign Member关联列表上的Manage Members按钮进行添加。

第二种方式,通过Contact/Lead的ListView画面上的Add to Campaign按钮。

第三种方式,通过Report上的Add to Campaign按钮。

不过以上三种方式,只支持向单个Campaign添加Members。

问题是这样的,
假如我有一个Campaign叫做TestCampaign,有500个Contact叫做TestContact000~Test499。
如果没有任何意外发生,三种方式的行为是一致的,结果均是成功将500条Contact做成了Campaign Member。

接下来我们做实验。
首先,在Contact上添加一个Text字段,叫testValidation__c。
然后在Campaign Member上添加一个Validation Rule,拦截条件为Contact.testValidation__c等于“E”。
之后将TestContact000的testValidation__c设置为”E”。

分别使用三种方式,将TestContact001~500添加到TestCampaign。

结果,行为出现了偏差。
对于第二种和第三种方式,

由于有一条“错误”数据,导致整个导入Campaign Member动作失败。

而最常见的第一种方式,却出现了不同的结果。

首先,错误信息与其他两种方式一致,并且没有进行画面跳转。


但是,如果我们进入Existing Members画面,就会赫然发现,里面居然进去了99条。

观察数据编号,发现进去的数据为TestContact001到TestContact099。就是说,除了“错误”的TestContact000,前100条的剩余数据都成功做成,100条以后的全都被放弃了。

这样的话,我猜想Add Member功能的机制是这样的———————选完数据,开始执行之后,后台会将数据按顺序分为100条一个批次,加入某个批次出现了错误数据,则其余数据插入,处理终止。

为了证实这个猜想,我又实验了将TestContact101设定为“错误”数据的情况,果然如我所料,进入了199条数据,为TestContact000~TestContact100及TestContact102~TestContact199。

对于同样一个功能,不同入口却出现了不同的行为模式与处理方式,实在是无法理解。
关于100条一个批次的问题,我猜应该是一个Salesforce的历史遗留问题,因为曾经Trigger虽然最多一起处理200条数据,但曾经后台还是按100一批处理的,导致有一些代码写法会莫名的只对前100条生效。

也许,又是一个Bug。

// 2018-05-08 Update

SFDC承认了这是一个Bug。Agent还贴心的提了一个Known Issue。
https://success.salesforce.com/issues_view?id=a1p3A000000nXgTQAU
遇到相同问题的同学请mark一发。

关于example.com

每当刷完Sandbox,Salesforce为了防止从Sandbox发出的邮件使用户混肴,
所以将所有User的邮箱的@替换为=,然后加上@example.com
比如,User A的邮箱为[email protected],则变换为[email protected]

如果向这种邮箱发出邮件,邮件会发到哪去呢?

有同学说了,这个邮箱肯定不存在的,所以谁也收不到。
这可未必。如果了解SMTP服务器的话,就知道有种catch all账号,可以收到所有发往此邮件服务器的邮件,无论收件地址是否存在。
所以用公司邮箱乱发东西的同学们注意了,千万别用公司邮箱乱说话呦。

所以我们就来看看这个example.com
首先用浏览器访问,还真的能直接访问,白白的背景上,正中间方方正正的写着

Example Domain

This domain is established to be used for illustrative examples in documents. You may use this domain in examples without prior coordination or asking for permission.

More information…

大概意思是说,这个域名就是给你在文档里做说明示例用的。随便用,不用经过我同意。
呦!谁这么大方?

顺手Whois了一下这个example.com
注册商是RESERVED-Internet Assigned Numbers Authority

这个Internet Assigned Numbers Authority是哪路神仙?
原来是大名鼎鼎的IANA(互联网号码分配局)。
什么?没听过,那么ICANN总听过吧,
Internet Corporation for Assigned Names and Numbers,互联网名称与数字地址分配机构。
如果购买域名的话,每年都要向ICANN交管理费的。
这个IANA就是ICANN的下属机构。
这就是说,这个域名居然还是官方所有的。

随后,点了一下页面上的More information…
了解到了整个事情的始末。

原来根据RFC2606
一级域名
.test
.example
.invalid
.localhost
二级域名
example.com
example.net
example.org
都为特殊目的所保留。

RFC6761则规定了具体的做法。

// 小插曲,很Interesting的是,RFC2606的作者是IBM雇员 Donald E. Eastlake 3rd与Aliza R. Panitz, 而RFC6761的作者为Apple Inc.的雇员Stuart Cheshire与Apple Inc.的雇员Marc Krochmal

网页中明确的说明了Certain domains are set aside, and nominally registered to “IANA”, for specific policy or technical purposes.
甚至提供了IDN的保留域名。

就像好莱坞电影里以555区号开头的电话一样,互联网也保留了这种安全域名用来举例子。

根据RFC6761中的提到的,

IANA currently maintains a web server providing a web page explaining the purpose of example domains.

所以只有http请求才会被受理以展示那个说明页面。
你的邮件无法找到example.com的MX记录,
虽然可以落到A记录,顺利的拿到IP,但IANA并没有建立监听25端口的SMTP服务,
所以,邮件发送会失败。

Salesforce关于Update Records with Inactive Owners权限

// 老鸟直接去下面看重点

Salesforce作为平台,无论是Standard Object还是Custome Object,都提供了默认的系统字段。
这些字段普通用户无法直接更新与编辑,只有系统才可以修改,以此作为审计信息,保证数据的可靠性。

但是,由于并不是每个使用Salesforce的组织都刚刚步入信息化。
所以,从Legacy System迁移到我们全球最先进的CRM系统——Salesforce的时候,必然会涉及到数据迁移。
有的同学说了,数据迁移简单啊,把数据从原来的系统抽出来,然后插入到Salesforce里。
我们知道,如果直接导出,再插入的后果是导致创建日期等一众字段都会变成数据导入那天。
那么,如果有功能是基于创建先后顺序的时候该怎么办,五年前的数据将会比昨天创建的数据更靠前。

为了应对这种情况,Salesforce提供了Set Audit Fields upon Record Creation与Update Records with Inactive Owners两个权限。
开启的方法为:
1. Setup -> Build -> Customize -> User Interface -> Enable “Set Audit Fields upon Record Creation” and “Update Records with Inactive Owners” User Permissions 勾选
2. 创建一个PermissionSet将Set Audit Fields upon Record Creation与Update Records with Inactive Owners分别勾选。将此PermissionSet分配给需要的User。
3. 在Custom Profile里将Set Audit Fields upon Record Creation与Update Records with Inactive Owners分别勾选。
这里需要注意的是,System Admin是标准Profile,所以只能使用PermissionSet。

Set Audit Fields upon Record Creation顾名思义,就是在Insert数据的时候,可以设定Audit Fields。一次操作成型,不能再Update。
继续阅读“Salesforce关于Update Records with Inactive Owners权限”