[Salesforce]関連リストのインライン編集をVFで作成する

オブジェクトの詳細で表示される関連リスト部分、要するにそのレコードを参照しているレコード、を一覧表示からインライン編集出来るようにしたかったので、インライン編集の基本的な部分から調べてみた。

参考

詳細レコード

該当のレコードの詳細を表示し、表示している項目をインライン編集出来るようにする。

作成しているVFページのURLにて、IDが指定されている + 指定されているstandardControllerのオブジェクトであれば、Visualforceだけで表示させることが可能。

https://Salesforce_instance/apex/myPage?id=001x000xxx3Jsxb

この場合、IDが001x000xxx3Jsxbの取引先(Account)が表示される。
apex:detailタグを使うと、このレコードの詳細がそのまま表示される。
その際に、属性inlineEdittrueで指定するとインライン編集が可能となる。

1
2
3
<apex:page standardController="Account">
        <apex:detail subject="{!account.Id}" relatedList="false" inlineEdit="true"/> 
</apex:page>

特定のレコードの詳細をそのまま表示したい時とかには便利。

一覧表示

詳細ではなく、一覧表示で複数件いっぺんに編集したい場合。 apex:pageの属性で、recordSetVarを指定してやれば、standardControllerで指定したオブジェクトのリストを表示出来る。

standardControllerとrecordSetVar – Qiita

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<apex:page standardController="Account" recordSetVar="records" id="thePage"> 
    <apex:form id="theForm"> 
        <apex:pageBlock id="thePageBlock"> 
            <apex:pageBlockTable value="{!records}" var="record" id="thePageBlockTable"> 
                <apex:column >
                    <apex:outputField value="{!record.Name}" id="AccountNameDOM" /> 
                    <apex:facet name="header">Name</apex:facet>
                </apex:column>
                <apex:column >
                    <apex:outputField value="{!record.Type}" id="AccountTypeDOM" /> 
                    <apex:facet name="header">Type</apex:facet>
                </apex:column>
                <apex:column >
                    <apex:outputField value="{!record.Industry}" 
                        id="AccountIndustryDOM" />  
                        <apex:facet name="header">Industry</apex:facet>
                </apex:column>
                <apex:inlineEditSupport event="ondblClick" 
                        showOnEdit="saveButton,cancelButton" hideOnEdit="editButton" /> 
            </apex:pageBlockTable> 
            <apex:pageBlockButtons > 
                <apex:commandButton value="Edit" action="{!save}" id="editButton" />
                <apex:commandButton value="Save" action="{!save}" id="saveButton" />
                <apex:commandButton value="Cancel" action="{!cancel}" id="cancelButton" />
            </apex:pageBlockButtons> 
        </apex:pageBlock> 
    </apex:form>
</apex:page>

コードは公式ドキュメントのママ。
formなど各タグ内のIDは別にあってもなくてもよさそう。

以下のように表示される。

apex:columnで囲まれてた部分が1つの項目になっており、<apex:facet name="header">タグの内容がそれぞれの項目の見出しとなる。

このままページを表示してインライン編集後、saveボタンをクリックすると確かに更新はされるがホームへ遷移してしまう。 (キャンセルをクリックしても同様)

これを解消するために、カスタムコントローラーを割り当ててみた。
recordSetVarを使っていると、割り当てたカスタムコントローラ側で取得したレコードリストを使用するために、StandardSetControllerを使う必要がある、とのこと。

SFDC:recordSetVarとextensions – tyoshikawa1106のブログ

これを、StandardControllerをコントローラ側で使っていると下記のようなエラーがでる。 エラーメッセージだけでは非常にわかりにくそうなので注意。

common.apex.runtime.bytecode.BytecodeApexObjectType cannot be cast to common.apex.runtime.impl.ApexType

で、カスタムコントローラ内で、ボタンを押された際のアクションを作成し、nullを返す事でページ遷移をしないようにする。
ただし、保存するsaveボタンはこれをしてしまうと保存されなくなってしまったので、キャンセルボタンだけにしておいた。

VF

1
<apex:page standardController="Account" recordSetVar="records" extensions="VfInlineEditSample" id="thePage"> 

apex (VfInlineEditSample.apxc)

1
2
3
4
5
6
7
8
9
public class VfInlineEditSample {
    public VfInlineEditSample(ApexPages.StandardSetController stdController){
        List<Account> lists = (List<Account>)stdController.getRecords();
    }
    
    public PageReference cancel(){
        return null;
    }
}

Classe StandardSetController

関連リスト

とあるレコードの関連リストを一覧表示からインライン編集したい場合。
上記の一覧表示と同じようにすればインライン編集出来るテーブルを作る事は可能。

IDはVFページのパラメータから取得する。

例)
VfInlineEditSamplePage.vfp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<apex:page standardController="Account" extensions="VfInlineEditSample" id="thePage"> 
    <apex:form id="theForm"> 
        <apex:pageBlock title="商談" >
            <apex:pageBlockTable value="{!opp_records}" var="opp">
                <apex:column >
                    <apex:outputField value="{!opp.Name}" /> 
                    <apex:facet name="header">商談名</apex:facet>
                </apex:column>
                <apex:column >
                    <apex:outputField value="{!opp.Amount}" /> 
                    <apex:facet name="header">金額</apex:facet>
                </apex:column>
                <apex:column >
                    <apex:outputField value="{!opp.StageName}" /> 
                    <apex:facet name="header">フェーズ</apex:facet>
                </apex:column>
                <apex:column >
                    <apex:outputField value="{!opp.NextStep}" /> 
                    <apex:facet name="header">次回アクション</apex:facet>
                </apex:column>
                <apex:inlineEditSupport event="ondblClick" />
            </apex:pageBlockTable> 
            <apex:pageBlockButtons > 
                <apex:commandButton value="Save" action="{!save}" />
                <apex:commandButton value="Cancel" action="{!cancel}" />
            </apex:pageBlockButtons> 
        </apex:pageBlock> 
    </apex:form>
</apex:page>

VfInlineEditSample.apxc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class VfInlineEditSample {
    
    public List<Opportunity> opp_records{get; set;}
    
    public VfInlineEditSample(ApexPages.StandardController stdController){
        Account acc = (Account)stdController.getRecord();
        Id aid = acc.id;
        Account record = [SELECT id, name, (SELECT id,name,StageName,NextStep,Amount FROM Opportunities) FROM Account WHERE id=:aid];
        opp_records = record.Opportunities;
    }
    
    public PageReference cancel(){
        return null;
    }
}

以下のようになる。

このままだとSaveをクリックしても保存はされない。
なので、cancelと同じようにカスタムコントローラ内でアクションを受け取り、編集した内容はVFへ引き渡している、 opp_recordsに入っているので、それをupdateすれば更新される。

apexクラスに以下を追加。

1
2
3
4
public PageReference save(){
    update opp_records;
    return null;
}

しかしこうすると、編集されたされてないにかかわらず、関連リストに並んでいるオブジェクト(この場合は商談)のレコード全てが更新されてしまう。
ガバナ制限は、1万レコードまで大丈夫なので、問題になることは少ないと思うが、最終更新日付が全て更新されてしまうのが都合が悪いかもしれない。

Apex ガバナ制限

更新対象を選ぶ

ワークフローであれば、ISCHANGEDを使えば、その項目が変更されたかどうかをチェック出来るが、apexではそのメソッドはない。
(機能追加が要望があがってたりする(トリガの機能としてだけど) IsChanged function in Apex)

なので、更新される項目を一つずつ比較して更新があるかどうかを確認する泥臭い方法を取ってみる。
(他にいいアイデアがあれば教えて欲しい…)

修正後のapex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class VfInlineEditSample {
    
    public List<Opportunity> opp_records{get; set;}
    private List<Opportunity> old_records;
    
    public VfInlineEditSample(ApexPages.StandardController stdController){
        Account acc = (Account)stdController.getRecord();
        Id aid = acc.id;
        Account record = [SELECT id, name, (SELECT id,name,StageName,NextStep,Amount FROM Opportunities) FROM Account WHERE id=:aid];
        opp_records = record.Opportunities;
        old_records = opp_records.deepClone();
    }
    
    public PageReference cancel(){
        return null;
    }
    
    public PageReference save(){
        List<Opportunity> update_lists = new List<Opportunity>();
        for(Integer i = 0; i< opp_records.size(); i++){
            if(opp_records[i].name != old_records[i].name){
                update_lists.add(opp_records[i]);
                continue;
            }else if(opp_records[i].StageName != old_records[i].StageName){
                update_lists.add(opp_records[i]);
                continue;
            }else if(opp_records[i].NextStep != old_records[i].NextStep){
                update_lists.add(opp_records[i]);
                continue;
            }else if(opp_records[i].Amount != old_records[i].Amount){
                update_lists.add(opp_records[i]);
                continue;
            }
        }
        
        if(update_lists.size() > 0){
            update update_lists;
        }
        
        return null;
    }
}

リストをコピーする際に、deepCloneを使わないとcloneでは浅いコピーとなってしまい、参照しているものが同じになってしまうので注意。 (一方の値を更新するともう一方も同じ値になってしまう)

汎用的にする

上記だと、表示する項目を増やす度に比較の条件文を追加しないといけない。
なので、全項目から取得出来るものだけを比較するようにしてみた。

以下、関数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public List<sObject> isChanged(String object_name, List<sObject> new_records, List<sObject> old_records){
    sObject obj = (sObject)Type.forName(object_name).newInstance();
    Schema.DescribeSObjectResult descR = obj.getsObjectType().getDescribe();
    Set<String> fields = descR.fields.getMap().keySet();
    Map<String, Schema.SObjectField> fmap = descR.fields.getMap();
    
    List<sObject> update_records = new List<sObject>();
    for(Integer i = 0; i< new_records.size(); i++){
        for(String field : fields){
            try{
                Schema.SObjectField f = fmap.get(field);
                Schema.DescribeFieldResult fr = f.getDescribe();
                if(!fr.isUpdateable()) continue; //更新出来ない項目は飛ばす
                //比較
                if(new_records[i].get(field) != old_records[i].get(field)){
                    update_records.add(new_records[i]);
                    continue;
                }
            }catch(SObjectException e){
                //取得していない項目
            }
        }
    }
    
    return update_records;
}
  • getDescribe()で指定したオブジェクトの全項目名を取得出来る
  • 取得した項目名を使ってオブジェクトからget()で取得。例外が発生したものはselectで取得していない項目、となる
  • 更新可能かどうかは、Describeで取得出来る項目の情報内を見れば判定可能
    isUpdateableがtrueなら更新が出来る項目となるので、これがtrueのもののみ比較している

使用の際は、第一引数にオブジェクトの参照名、第二引数に更新後のレコードリスト、第三引数に更新前のレコードリストを指定してやる。

1
List<Opportunity> update_lists = isChanged('Opportunity', opp_records, old_records);

これで項目が増えても安心。

参考

   このエントリーをはてなブックマークに追加