本部長は管理ができない

Salesforceに関わっているエンジニアの技術メモ。ときどきそれ以外。

一覧をソートしてみる

f:id:gren_dken:20140925132128j:plain
ガストロバル パロマ+1のハンバーグランチ。
どう食べれば良いか1分ほど悩んだ。


一覧のヘッダをクリックしたときに、クリックした項目でソートする処理を作ってみた。

f:id:gren_dken:20141108182852p:plain
姓項目を昇順でソートした状態


  • ソート結果はクエリを実行し取得
  • ヘッダの文字列部分はApexコンポーネントを使用
  • どの項目を、昇順or降順でソートしているかの状態を保持するクラスを作成
    • これを、項目のAPI参照名をキーにしたMapで管理



ページ
<apex:page controller="SortListController" action="{!doInit}">
    <apex:form >
        <apex:pageBlock title="取引先責任者一覧">
            <apex:pageBlockTable value="{!contacts}" var="cont" id="dataTable">
                <apex:column>
                    <apex:facet name="header">
                        <c:SortHeader headerLabel="{!$ObjectType.Contact.Fields.LastName.Label}"
                                      sortMethod="{!doSort}" sortColumn="LastName" sortIcon="{!sortStatusMap['LastName'].sortIcon}"
                                      rerenderId="dataTable" />
                    </apex:facet>
                    <apex:outputField value="{!cont.LastName}" />
                </apex:column>

                <apex:column>
                    <apex:facet name="header">
                        <c:SortHeader headerLabel="{!$ObjectType.Contact.Fields.FirstName.Label}"
                                      sortMethod="{!doSort}" sortColumn="FirstName" sortIcon="{!sortStatusMap['FirstName'].sortIcon}"
                                      rerenderId="dataTable" />
                    </apex:facet>
                    <apex:outputField value="{!cont.FirstName}" />
                </apex:column>

                <apex:column>
                    <apex:facet name="header">
                        <c:SortHeader headerLabel="{!$ObjectType.Contact.Fields.Title.Label}"
                                      sortMethod="{!doSort}" sortColumn="Title" sortIcon="{!sortStatusMap['Title'].sortIcon}"
                                      rerenderId="dataTable" />
                    </apex:facet>
                    <apex:outputField value="{!cont.Title}" />
                </apex:column>

                <apex:column>
                    <apex:facet name="header">
                        <c:SortHeader headerLabel="{!$ObjectType.Contact.Fields.Phone.Label}"
                                      sortMethod="{!doSort}" sortColumn="Phone" sortIcon="{!sortStatusMap['Phone'].sortIcon}"
                                      rerenderId="dataTable" />
                    </apex:facet>
                    <apex:outputField value="{!cont.Phone}" />
                </apex:column>

                <apex:column>
                    <apex:facet name="header">
                        <c:SortHeader headerLabel="{!$ObjectType.Contact.Fields.Email.Label}"
                                      sortMethod="{!doSort}" sortColumn="Email" sortIcon="{!sortStatusMap['Email'].sortIcon}"
                                      rerenderId="dataTable" />
                    </apex:facet>
                    <apex:outputField value="{!cont.Email}" />
                </apex:column>
            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>
</apex:page>



コンポーネント
<apex:component >
    <apex:attribute name="headerLabel" type="String" description="ヘッダに表示する文字列" />
    <apex:attribute name="sortMethod" type="ApexPages.Action" description="ヘッダ文字列押下時に実行するメソッド" required="true" />
    <apex:attribute name="sortColumn" type="String" description="ソート対象のカラム" required="true" />
    <apex:attribute name="sortIcon" type="String" description="項目の右側に表示するソート方向を示す文字" required="true" />
    <apex:attribute name="rerenderId" type="String" description="ヘッダ文字列押下時の再描画領域" />

    <apex:commandLink value="{!headerLabel}" action="{!sortMethod}" reRender="{!rerenderId}" >
        <apex:param value="{!sortColumn}" name="sortColumn" />
    </apex:commandLink>
    &nbsp;
    <apex:outputText value="{!sortIcon}" style="font-size: 80%; font-weight: normal;" />
</apex:component>



コントローラ
public with sharing class SortListController {
    /** ベースのSELECT文 */
    private String baseQuery = 'SELECT Id, FirstName, LastName, Title, Phone, Email FROM Contact ';

    /** 表示データ */
    public List<Contact> contacts {get; set;}

    /** カラムごとのソート状態 */
    public Map<String, SortStatus> sortStatusMap {get; set;}

    /**
     * コンストラクタ
     */
    public SortListController() {
        initSortStatus();
    }

    /**
     * 初期処理
     */
    public PageReference doInit() {
        this.contacts = Database.query(this.baseQuery);
        return null;
    }

    /**
     * ソート(ヘッダ押下時)
     */
    public PageReference doSort() {
        // ソート対象のカラムを取得
        String sortColumn = ApexPages.currentPage().getParameters().get('sortColumn');

        // ソート状態を変更
        changeSortStatus(sortColumn);

        // ソート条件を付加してデータ取得
        String query = this.baseQuery;
        query += 'ORDER BY ' + sortColumn + ' ' + this.sortStatusMap.get(sortColumn).sortWord;
        this.contacts = Database.query(query);

        return null;
    }

    /**
     * ソート状態の初期化
     */
    private void initSortStatus() {
        this.sortStatusMap = new Map<String, SortStatus> {
            'FirstName' => new SortStatus(),
            'LastName' => new SortStatus(),
            'Title' => new SortStatus(),
            'Phone' => new SortStatus(),
            'Email' => new SortStatus()
        };
    }

    /**
     * ソート状態の変更
     */
    private void changeSortStatus(String sortColumn){
        for (String key : this.sortStatusMap.keySet()) {
            if (key == sortColumn) {
                this.sortStatusMap.get(key).change();
            }
            else {
                this.sortStatusMap.get(key).init();
            }
        }
    }
}



ソート状態を保持するクラス
/**
 * ソート状態を保持するクラス
 */
public class SortStatus {
    /** ソート順 定義:ソートなし */
    public static Integer SORT_ORDER_NONE = 0;
    /** ソート順 定義:昇順ソート */
    public static Integer SORT_ORDER_ASC = 1;
    /** ソート順 定義:降順ソート */
    public static Integer SORT_ORDER_DESC = 2;

    /** ソート順 */
    public Integer sortOrder {get; set;}
    /** 一覧のヘッダに表示するソート方向を示す文字(↑↓など) */
    public String sortIcon {get; set;}
    /** SOQLのソート句 */
    public String sortWord {get; set;}

    /**
     * コンストラクタ
     */
    public SortStatus() {
        init();
    }

    /**
     * メンバ変数初期化
     */
    public void init() {
        this.sortOrder = SORT_ORDER_NONE;
        this.sortIcon = null;
        this.sortWord = null;
    }

    /**
     * ソート順の変更
     */
    public void change() {
        if (this.sortOrder == SORT_ORDER_NONE || this.sortOrder == SORT_ORDER_DESC) {
            this.sortOrder = SORT_ORDER_ASC;
            this.sortIcon = '↑';
            this.sortWord = 'ASC';
        }
        else if (this.sortOrder == SORT_ORDER_ASC) {
            this.sortOrder = SORT_ORDER_DESC;
            this.sortIcon = '↓';
            this.sortWord = 'DESC';
        }
        else {
            this.sortOrder = SORT_ORDER_NONE;
            this.sortIcon = null;
            this.sortWord = null;
        }
    }
}
実装時の驚き
  • apex:attributeにメソッドを渡せる
  • VisualforceでMapを使える

EclipseでコーディングしたApexコードが文字化け

Eclipse、Force.comIDEを新規インストールし、Apexクラスをコーディングしていたら動作がおかしい。

原因は文字列の記述。
Eclipseでは文字化けしていないが、Salesforceのエディタでは文字化けしている。
以下の「あいうえお」が文字化けする。

String tmp = 'あいうえお';


対応策は、eclipse.iniの「-vmargs」以降の行に下記のパラメータを追加。

-Dfile.encoding=utf-8

以上、メモ。

apex:inputTextでカレンダーと日付リンクを表示

f:id:gren_dken:20140523114919j:plain
ステーキくにのランチハンバーグ。美味い。



日付型、日付/時間型の入力の際に表示されるカレンダーとリンクは、Visualforceページだとapex:inputFieldの場合に表示される。

f:id:gren_dken:20140708211526p:plain


apex:inputTextでコントローラのDate型変数をvalueに設定してもカレンダーとリンクは表示されない。
日付型のinputFieldのHTMLを確認すると、カレンダーはDatePicker#pickDate、リンクはDatePicker#insertDateを使用している。

  • ページ
<apex:inputText id="fooDate" value="{!fooDate}" size="12"
    onfocus="javascript:DatePicker.pickDate(true, '{!$Component.fooDate}', false)" />
[ 
<apex:outputLink value="javascript:DatePicker.insertDate('{!fooDate}', '{!$Component.fooDate}', true);">
    {!fooDate}
</apex:outputLink>
 ]
<p style="display: none;"><apex:inputField value="{!obj.Date__c}"/></p>


  • コントローラ
// 今日日付をセット
this.fooDate = Datetime.now().format('yyyy/MM/dd', 'JST');


ただしカレンダーのHTMLは、ページ内に日付型か日付/時間型の入力フィールドがないとページにロードされないため、下記のように非表示でinputFieldを置く必要がある。

<p style="display: none;"><apex:inputField value="{!obj.Date__c}"/></p>

ちょっとしたバッチ

f:id:gren_dken:20140307221544j:plain
すた丼。
※本文とまったく関係ありません。



作業中に使っているファイルやフォルダに関するバッチ。
Windows用。
バッチファイルにD&Dして使っている。


現在日時のフォルダを作成
@echo off

rem 日付と時刻の取得
set currentDate=%DATE:/=%
set currentTime=%TIME::=%
set currentTime=%currentTime: =0%
set currentTime=%currentTime:~0,6%

mkdir "%~1\%currentDate%_%currentTime%"


今日日付に連番を付与したフォルダを作成
@echo off

set /A dirNo=0
set MAX_DIR_NO=9

set basePath=%~1\%DATE:/=%

:LOOP_START

rem フォルダ名生成
if %dirNo% equ 0 (
    set dirPath="%basePath%"
) else (
    set dirPath="%basePath%_0%dirNo%"
)

rem フォルダがなければ作成
if not exist %dirPath% (
    mkdir %dirPath%
    goto LOOP_END
)

rem インクリメント
if %dirNo%==%MAX_DIR_NO% (goto LOOP_END)
set /A dirNo=dirNo+1
goto LOOP_START

:LOOP_END


ファイルに現在日時を付与してコピー

複数ファイルにも対応。
「copy」を「move」にすればリネームになる。

@echo off

rem 日付と時刻の取得
set currentDate=%DATE:/=%
set currentTime=%TIME::=%
set currentTime=%currentTime: =0%
set currentTime=%currentTime:~0,6%

for %%i in (%*) do (
    copy "%%~i" "%%~dpni_%currentDate%_%currentTime%%%~xi"
)


フォルダに現在日時を付与してコピー
@echo off

rem 日付と時刻の取得
set currentDate=%DATE:/=%
set currentTime=%TIME::=%
set currentTime=%currentTime: =0%
set currentTime=%currentTime:~0,6%

rem コピー
xcopy "%~1" "%~1_%currentDate%_%currentTime%\" /E /H /K

コレクションの文字列化

f:id:gren_dken:20140328232050j:plain
一風堂 赤丸新味。
※本文と何ら関係ありません。


コレクションの中身を、ループを使わず展開出来ないか試したら、String#valueOfでさっくり出来た。
system#debugで出力される文字列と同じになる。


List
  • コード
List<String> tmpList = new List<String>{'Red', 'Yellow', 'Pink', 'Green', 'Purple'};
String strList = String.valueOf(tmpList);
system.debug('★List 1:' + tmpList);
system.debug('★List 2:' + strList);
  • 結果
★List 1:(Red, Yellow, Pink, Green, Purple)
★List 2:(Red, Yellow, Pink, Green, Purple)
Set
  • コード
Set<String> tmpSet = new Set<String>{'Red', 'Yellow', 'Pink', 'Green', 'Purple'};
String strSet = String.valueOf(tmpSet);
system.debug('★Set 1:' + tmpSet);
system.debug('★Set 2:' + strSet);
  • 結果
★Set 1:{Green, Pink, Purple, Red, Yellow}
★Set 2:{Green, Pink, Purple, Red, Yellow}
Map
  • コード
Map<Integer, String> tmpMap = new Map<Integer, String>{1 => 'Red', 2 => 'Yellow', 3 => 'Pink', 4 => 'Green', 5 => 'Purple'};
String strMap = String.valueOf(tmpMap);
system.debug('★Map 1:' + tmpMap);
system.debug('★Map 2:' + strMap);
  • 結果
★Map 1:{1=Red, 2=Yellow, 3=Pink, 4=Green, 5=Purple}
★Map 2:{1=Red, 2=Yellow, 3=Pink, 4=Green, 5=Purple}


コレクションのサイズが大きいと途中から省略されるので、出力サイズの検討が必要。

(~~Green, Purple, ...)

ファイルアップロード時のビューステートエラー対応

Apexでメモ&添付ファイルに135KB以上のファイルをアップロードした際に、ビューステートエラーになったので、対応方法をメモ。

public class uploadFileController{
    public Attachment attach {get; set;}

    public uploadFileController(){
        attach = new Attachment();
    }

    public void uploadFile(){
        attach.Parentid = 'XXXXXXX';

        try {
            insert attach;
            System.debug('アップロード完了');
        }
        catch (Exception e) {
            System.debug('アップロードエラー');
            System.debug(e.getMessage());
        }
        finally {
            // 135.0KB以上のファイルを指定した場合、nullにしないとビューステートエラーになる
            attach.Body = null;
        }

        return;
    }
}
  • insertが完了したら、Bodyをnullにする。
  • ドキュメントも同じと思われる(未実験)。

ページネーション

ApexとVisualforceでページネーション(ページング、ページ送り)を作ってみた。
※この記事を参考にさせていただきました。
StandardSetController | Official Blog of CloudClickware

表示するリンク
  • 最初のページ
  • 最後のページ
  • 前のページ
  • 次のページ
  • 現在ページを中心としたページ番号

ページ番号の表示数は定数で指定。

ページリンクのデザイン
<< 最初 | < 前 | 1 | 2 | 3 | 4 | 5 | 次 > | 最後 >>
コントローラ
public with sharing class paginationController{
    // 1ページあたりの表示件数
    private static final Integer ROW_PER_PAGE = 3;
    // 表示するリンクの数(現在ページを中心に前後に表示する数)
    private static final Integer NUMBER_OF_LINKS = 2;

    private ApexPages.StandardSetController setCon;

    // クリックしたページ番号
    public Integer clickPageNumber {get; set;}

    // コンストラクタ
    public paginationController(){
        setCon = new ApexPages.StandardSetController([SELECT Id, FirstName, LastName, Title, Phone, Email FROM Contact]);
        this.setCon.setPageSize(ROW_PER_PAGE);
    }

    // 指定のページへ移動
    public void doPageNumber(){
        this.setCon.setPageNumber(this.clickPageNumber);
    }

    // 前のページへ移動
    public void doPrevious(){
        this.setCon.previous();
    }

    // 次のページへ移動
    public void doNext(){
        this.setCon.next();
    }

    // 最初のページへ移動
    public void doFirst(){
        this.setCon.first();
    }

    // 最後のページへ移動
    public void doLast(){
        this.setCon.last();
    }

    // 前のページが存在するか
    public Boolean getHasPrevious(){
        return this.setCon.getHasPrevious();
    }

    // 次のページが存在するか
    public Boolean getHasNext(){
        return this.setCon.getHasNext();
    }

    // 最初のページか
    public Boolean getIsFirst(){
        return this.setCon.getPageNumber() == 1;
    }

    // 最後のページか
    public Boolean getIsLast(){
        return this.setCon.getPageNumber() == this.getTotalPages();
    }

    // 現在のページを取得
    public Integer getPageNumber(){
        return this.setCon.getPageNumber();
    }

    // ページ数を取得
    public Integer getTotalPages(){
        Decimal pages = Decimal.valueOf(this.setCon.getResultSize()) / Decimal.valueOf(this.setCon.getPageSize());
        return Integer.valueOf(pages.round(System.RoundingMode.CEILING));
    }

    // ページ番号のリンクを取得
    public List<Integer> getPageNumberLinks(){
        Integer startPageNumber = this.setCon.getPageNumber() - NUMBER_OF_LINKS;
        if(startPageNumber <= 0){
            startPageNumber = 1;
        }
        Integer endPageNumber = this.setCon.getPageNumber() + NUMBER_OF_LINKS;
        if(endPageNumber > this.getTotalPages()){
            endPageNumber = this.getTotalPages();
        }

        List<Integer> links = new List<Integer>();
        for(Integer i = startPageNumber; i <= endPageNumber; i++){
            links.add(i);
        }

        return links;
    }

    // 表示するレコードを取得
    public List<Contact> getContacts(){
        return (List<Contact>)setCon.getRecords();
    }
}
ページ
<apex:page controller="paginationController">
    <apex:form >
        <apex:pageBlock >
            <apex:pageBlockTable value="{!contacts}" var="cont">
                <apex:column value="{!cont.FirstName}"/>
                <apex:column value="{!cont.LastName}"/>
                <apex:column value="{!cont.Title}"/>
                <apex:column value="{!cont.Phone}"/>
                <apex:column value="{!cont.Email}"/>
            </apex:pageBlockTable>

            <br/>

            <!-- ページネーション -->
            <apex:commandLink value="<< 最初" rendered="{!!isFirst}" action="{!doFirst}" />
            <apex:outputLabel value="<< 最初" rendered="{!isFirst}" />
            <apex:outputLabel value=" | " />

            <apex:commandLink value="< 前" rendered="{!hasPrevious}" action="{!doPrevious}" />
            <apex:outputLabel value="< 前" rendered="{!!hasPrevious}" />
            <apex:outputLabel value=" | " />

            <apex:repeat value="{!pageNumberLinks}" var="links">
                <apex:commandLink value="{!links}" rendered="{!links != pageNumber}" action="{!doPageNumber}" >
                    <apex:param value="{!links}" name="clickPageNumber" assignTo="{!clickPageNumber}" />
                </apex:commandLink>
                <apex:outputLabel value="{!links}" rendered="{!links == pageNumber}" />
                <apex:outputLabel value=" | " />
            </apex:repeat>

            <apex:commandLink value="次 >" rendered="{!hasNext}" action="{!doNext}" />
            <apex:outputLabel value="次 >" rendered="{!!hasNext}" />
            <apex:outputLabel value=" | " />

            <apex:commandLink value="最後 >>" rendered="{!!isLast}" action="{!doLast}" />
            <apex:outputLabel value="最後 >>" rendered="{!isLast}" />
        </apex:pageBlock>
    </apex:form>
</apex:page>