[Visualforce] ページ読み込み時にapex:attributeの値をコントローラで使う方法

## TL;DR
`apex:attribute`の値をコントローラのコンストラクタで使用することはできません。
ページ読み込み時に`apex:attribute`の値をコントローラで使用する場合はgetter/setterまたはプロパティを使用します。

## はじめに
Visualforceコンポーネントに`apex:attribute`要素を追加すると、Visualforceから呼び出すときに属性に値を設定できます。
下記の例ではVisualforceコンポーネントにカスタムのlogoLinkUrl属性を定義しています。

```html:sample.page
<apex:page>
    <c:sampleHeader logoLinkUrl="https://www.google.com/"/>
</apex:page>
```

```html:sampleHeader.component
<apex:component>
    <apex:attribute name="logoLinkUrl" type="String" description="ヘッダーアイコンのリンク先" />

    <header>
        <a href="{!logoLinkUrl}" class="logo"><img src="/img/logo214.svg"/></a>
    </header>
</apex:component>
```

この`apex:attribute`は、assignTo属性によってコントローラの変数に値を代入[^1]することができます。
しかし、コンストラクタでこの値を使うと値が null となっていしまいます。
また、apex:component要素にはApexメソッドを呼び出すaction属性はありません。

## Visualforceページにアクセスしたときの実行順序について
Visualforceページにアクセスした時、下記の順で処理が行われます。
Visualforceコンポーネントのコンストラクタ(2)は、`apex:attribute`の`assignTo`(3)より先に実行されます。

1. Visualforceページのコントローラのコンストラクタ。
1. Visualforceコンポーネントのコンストラクタ。
1. Visualforceコンポーネントの`apex:attribute`のassignTo属性(setterメソッド)。
1. apex:pageコンポーネントのaction属性
1. Visualforceページ・Visualforceコンポーネントのすべての式、getterメソッド、 setterメソッド
1. (apex:formコンポーネントがある場合) ビューステートの作成
1. HTMLの返却および、クライアントサイドの処理(javascriptなど)

## Visualforceページ読み込み時にattributeの値をコントローラで使用する
attributeの値を使用するにはassignTo属性で指定したsetterを使用するか、getterで使用します。

「Visualforceページにアクセスしたときの実行順序について」でみたとおり、
`assignTo`により割り当てられている変数のsetterメソッドが呼び出され、
`assignTo`のあとにgetterメソッドが使用されるからです。

下記では、選択リスト項目をラジオボタンで表示するVisualforceコンポーネントで例をあげます。
![ATTRIBUTE002.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/d1a5d6c9-ca40-8eb2-e9ff-14b274663735.png)


### getterメソッドの例 (選択リスト項目をラジオボタンで表示)
getterメソッド、getアクセス機構をもつプロパティの実行は、apex:attributeのassignToよりあとに行われます。
なのでgetterメソッドを使用すれば、ページ読み込み時にapex:attributeの値を使用することができます。

```html:inputRadioDemo.page
<apex:page standardController="Account">
    <apex:form>
        <apex:pageBlock title="取引先" mode="edit">
            <apex:pageBlockButtons>
                <apex:commandButton action="{!quicksave}" value="保存" />
            </apex:pageBlockButtons>
            <apex:pageBlockSection title="ラジオボタンテスト" columns="1">
                <apex:outputField value="{!account.name}" />
                <apex:pageBlockSectionItem>
                    <apex:outputLabel value="区分" for="radio" />
                    <!-- ↓ ここでVisualforceコンポーネントを呼び出しています。 -->
                    <c:InputRadio value="{!Account.Type__c}" object="Account" field="Type__c" addNotSelect="true" id="radio"/>
                </apex:pageBlockSectionItem>
                <!-- ↓ 比較のための選択リストのinputFieldです。 -->
                <apex:inputField value="{!account.Industry}" />
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>
```

```html:inputRadio.commponent
<!-- 選択リスト項目 ラジオボタン表示 -->
<apex:component controller="InputRadioCtrl">
    <apex:attribute name="value" type="String" assignTo="{!selectedValue}" description="" required="true"/>
    <apex:attribute name="addNotSelect" type="Boolean" assignTo="{!isAddNotSelect}" description="選択肢に未選択を表示する" default="false"/>

    <!-- ↓ ここで指定したSObjectField(選択リスト)の選択リスト値をコントローラで取得し、 apex:selectOptions で使用します。 -->
    <apex:attribute name="object" type="String" assignTo="{!objectStr}" description="SObject名をAPI参照名で指定" required="true"/>
    <apex:attribute name="field" type="String" assignTo="{!fieldStr}" description="選択リストの項目名をAPI参照名で指定" required="true"/>

    <apex:selectRadio value="{!value}" disabled="false" rendered="{!options.size!=0}" id="radio">
        <!-- ↓ apex:attributeの値をもとにコントローラで取得した値を使用しています -->
        <apex:selectOptions value="{!options}"/>
    </apex:selectRadio>
</apex:component>

```

```java:InputRadioCtrl.cls
public with sharing class InputRadioCtrl {
    public String  selectedValue  { get; set; }
    public Boolean isAddNotSelect { get; set; }
    public String  objectStr      { get; set; }
    public String  fieldStr       { get; set; }

    // ↓ このgetterメソッドで、Visualforceページからapex:attributeを通して設定した値を使用して、選択肢を返しています。
    /** 選択肢の取得 */
    public List<SelectOption> getOptions() {
        Schema.SObjectField field = toSObjectField(objectStr, fieldStr);
        List<SelectOption> soList = new List<SelectOption>();
        if (field.getDescribe().isUpdateable()) {
            soList = getPickListOptions(field);
            if (String.isNotBlank(selectedValue) && !containsSelectOption(soList, selectedValue))  {
                // 無効な値が設定されているときは表示させる
                soList.add(new SelectOption(selectedValue, selectedValue));
            }
            if (isAddNotSelect) {
                soList.add(new SelectOption('', '未選択'));
            }
        }
        return soList;
    }

    /** SObjectField変換メソッド */
    private Schema.SObjectField toSObjectField(String objectName, String fieldName) {
        Schema.DescribeSObjectResult dsr = Schema.describeSObjects(new List<String>{objectName})[0];
        return dsr.fields.getMap().get(fieldName);
    }

    /** 選択リスト取得メソッド */
    private List<SelectOption> getPickListOptions(Schema.SObjectField field) {
        List<SelectOption> opList = new List<SelectOption>();
        for (Schema.PicklistEntry pe : field.getDescribe().getPickListValues()) {
            if (!pe.isActive()) { continue; }
            opList.add(new SelectOption(pe.getValue(),pe.getLabel()));
        }
        return opList;
    }

    /** 値が選択肢に含まれているか */
    private Boolean containsSelectOption(List<SelectOption> pOptions, String pValue) {
        List<String> values = new List<String>();
        for (SelectOption so : pOptions) {
            values.add(so.getValue());
        }
        return values.contains(pValue);
    }
}
```

### setterメソッドの例 (選択リスト項目をラジオボタンで表示)
assignToで指定している変数のsetterメソッド、あるいはsetをもつプロパティを使用することで、ページ読み込み時に`apex:attribute`の値を使用できます。
下記ではsetをもつプロパティの例です。
getterメソッドの例とおなじ機能のものをsetをもつプロパティを使用して書き換えています。


```html:inputRadioDemo.page
<apex:page standardController="Account">
    <apex:form>
        <apex:pageBlock title="取引先" mode="edit">
            <apex:pageBlockButtons>
                <apex:commandButton action="{!quicksave}" value="保存" />
            </apex:pageBlockButtons>
            <apex:pageBlockSection title="ラジオボタンテスト" columns="1">
                <apex:outputField value="{!account.name}" />
                <apex:pageBlockSectionItem>
                    <apex:outputLabel value="区分" for="radio" />
                    <!-- ↓ ここでVisualforceコンポーネントを呼び出しています。 -->
                    <c:InputRadio value="{!Account.Type__c}" field="Account.Type__c" addNotSelect="true" id="radio"/>
                </apex:pageBlockSectionItem>
                <!-- ↓ 比較のための選択リストのinputFieldです。 -->
                <apex:inputField value="{!account.Industry}" />
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>
```

```html:inputRadio.commponent
<!-- 選択リスト項目 ラジオボタン表示 -->
<apex:component controller="InputRadioCtrl">
    <apex:attribute name="value" type="String" assignTo="{!selectedValue}" description="" required="true"/>
    <apex:attribute name="addNotSelect" type="Boolean" assignTo="{!isAddNotSelect}" description="選択肢に未選択を表示する" default="false"/>

    <!-- ↓ ここで指定したSObjectField(選択リスト)の選択リスト値をコントローラで取得し、 apex:selectOptions で使用します。 -->
    <apex:attribute name="field" type="String" assignTo="{!fieldStr}" description="項目を「ObjectApiName.FieldApiName」の形式で指定" required="true"/>

    <apex:selectRadio value="{!value}" disabled="false" rendered="{!options.size!=0}" id="radio">
        <!-- ↓ apex:attributeの値をもとにコントローラで取得した値を使用しています -->
        <apex:selectOptions value="{!options}"/>
    </apex:selectRadio>
</apex:component>

```

```java:InputRadioCtrl.cls
public with sharing class InputRadioCtrl {
    public String             selectedValue  { get; set; }
    public Boolean            isAddNotSelect { get; set; }
    public List<SelectOption> options        { get; private set; }

    // ↓ assignToで代入されるプロパティです。
    // ここのsetアクセス機構で、設定された値(value)をつかって他の変数に値を設定しています。
    public String             fieldStr       { get; set {
        fieldStr = value;
        Schema.SObjectField field = toSObjectField(value);
        setSelectOption(field);
    }}

    /** 選択肢の設定 */
    private void setSelectOption(Schema.SObjectField field) {
        List<SelectOption> soList = new List<SelectOption>();
        if (field.getDescribe().isUpdateable()) {
            soList = getPickListOptions(field);
            if (String.isNotBlank(selectedValue) && !containsSelectOption(soList, selectedValue))  {
                // 無効な値が設定されているときは表示させる
                soList.add(new SelectOption(selectedValue, selectedValue));
            }
            if (isAddNotSelect) {
                soList.add(new SelectOption('', '未選択'));
            }
        }
        this.options = soList;
    }

    /** SObjectField変換メソッド */
    private Schema.SObjectField toSObjectField(String value) {
        List<String> objAndField = value.split('\\.');
        String objectName = objAndField[0];
        String fieldName  = objAndField[1];

        Schema.DescribeSObjectResult dsr = Schema.describeSObjects(new List<String>{objectName})[0];
        return dsr.fields.getMap().get(fieldName);
    }

    /** 選択リスト取得メソッド */
    private List<SelectOption> getPickListOptions(Schema.SObjectField field) {
        List<SelectOption> opList = new List<SelectOption>();
        for (Schema.PicklistEntry pe : field.getDescribe().getPickListValues()) {
            if (!pe.isActive()) { continue; }
            opList.add(new SelectOption(pe.getValue(),pe.getLabel()));
        }
        return opList;
    }

    /** 値が選択肢に含まれているか */
    private Boolean containsSelectOption(List<SelectOption> pOptions, String pValue) {
        List<String> values = new List<String>();
        for (SelectOption so : pOptions) {
            values.add(so.getValue());
        }
        return values.contains(pValue);
    }
}
```

## 参考
- [Visualforce ページ内の実行順序 | Visualforce 開発者ガイド](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_controller_lifecycle.htm)
- [Apexの実行順序 - TECH BLOG | 株式会社テラスカイ](https://www.terrasky.co.jp/blog/2013/130710_001295.php)
- [[salesforce]Visualforceページ表示処理の実行順序 – deferloader](https://deferloader.blog.uhuru.co.jp/1218/)
- [apex:attribute | Visualforce 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_compref_attribute.htm)


<!-- 脚注 -->
[^1]: Visualforceコンポーネントからコントローラに apex:attribute の value を渡す。その逆はできません。