惊!List型Custom Setting惨遭SFDC抛弃,上位者居然是……

一夜之间,List型Custom Setting惨遭抛弃,SFDC发布冷血声明。
上位者从默默无闻变成人尽皆知。
SFDC开发者不禁议论纷纷。
到底是人性的扭曲,还是道德的沦丧。
接下来请看————————————谁杀死了List型Custom Setting?

继续阅读“惊!List型Custom Setting惨遭SFDC抛弃,上位者居然是……”

关于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还是学好英语吧。

不小心Commit账户密码到本地仓库,并且push到了remote仓库怎么办

今天我就做了这个傻事。
不过还好是私有仓库,没有酿成泄密事件。

那么这种情况该怎么处理呢?
首先,把账户密码改掉,再Commit一次是肯定不行的。
看worktree,账户密码确实是没了,但是在Commit记录里仍然能看到的。

别急,Stackoverflow上我找到了一个简单粗暴的做法,直接抹掉全部历史!
https://stackoverflow.com/questions/13716658/how-to-delete-all-commit-history-in-github

首先你需要一个git命令行工具,Eclipse自带的git插件是玩具。

1.创建一个独立(孤儿)的分支,并Checkout

git checkout –orphan latest_branch

2.将目前的文件全部add到此分支

git add -A

3.全部Commit到孤儿分支

git commit -am “commit message”

4.删除带有历史信息的Master分支

git branch -D master

5.将没有任何历史的孤儿分支重命名为Master

git branch -m master

6.强制推送到Remote仓库

git push -f origin master

代价就是所有历史都没有了。
当然,我们有更精细的方式,可以剔除包含敏感信息的commit

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一发。