Visualforceのレコード保存処理時、標準バリデーションエラーを回避して任意のactionを実行させる

# 目的・目標
* Visualforceのページメッセージを処理ごとに毎回クリアしたい
* 確認画面、完了画面のないVisualforceページで編集画面のみで`apex:pageMessages`を利用して視覚的に更新が完了・エラーした事を伝えたい
* 処理実行前にVisualforce画面の`apex:pageMessages`をクリアして任意の処理後に再度、完了・エラーを表示させたい

# Before(保存1の処理)
#### Visualforceコード
``` HTML
<apex:pageBlockButtons location="bottom">
  <apex:commandButton value="保存1" action="{!save}" reRender="container" status="spinnerStatus"/>
</apex:pageBlockButtons>
```
#### 動作画面
![Before1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331690/19601614-c79c-ca82-7aff-32204c013019.gif)

* 保存ボタンを押下したときロード画面が表示されているだけで更新されたのが認識しづらい

# Try(保存2の処理)
#### Visualforceコード
``` HTML
<apex:pageBlockButtons location="bottom">
  <apex:actionFunction name="save1B" action="{!clearVfMsg}" oncomplete="return false;" reRender="container" />
  <apex:commandButton value="保存2" onclick="save1B();" action="{!save}" reRender="container" status="spinnerStatus"/>
</apex:pageBlockButtons>
```
#### 動作画面
![Before2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331690/ab423bc0-e84c-c4ad-5ac1-b57e784d6bc0.gif)

### 改善
* `immediate="true"`を`apex:actionFunction`につけることで**保存ボタン押下時にデータ型、桁数(Salesforce標準)、必須項目(Salesforce標準)のバリデーションエラーが発生した場合にメッセージクリア処理が実行されないことを回避しました**

### 問題
* メッセージクリア前にロード画面が入ってしまう
* ボタンを連打すると`apex:pageMessages`がバグで表示されない場合がある(キャプション 4回目の保存ボタン押下時)

# After(保存3の処理)
#### Visualforceコード
``` HTML
<apex:pageBlockButtons location="bottom">
  <apex:actionFunction name="save1C" action="{!clearVfMsg}" oncomplete="save2C();" reRender="container" immediate="true"/>
  <apex:actionFunction name="save2C" action="{!save}" reRender="container" status="spinnerStatus"/>
  <apex:commandButton value="保存3" onclick="save1C(); return false;"/>
</apex:pageBlockButtons>
```
#### 動作画面
![After1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331690/9007c9f6-38cc-1ce0-5ca6-8e1ad94852d6.gif)

### 改善
* メッセージクリア後にロード画面が表示される
* ボタンを連打しても`apex:pageMessages`にバグが発生しない


# 学習したこと
* `immediate="true"`を`apex:actionFunction`につけることで**保存ボタン押下時にデータ型、桁数(Salesforce標準)、必須項目(Salesforce標準)のバリデーションエラーが発生する場合でも任意の処理を先に実行させる事ができる**
* `apex:actionFunction`の`oncomplete`を利用して処理をメソッドチェーンさせることができる
* メソッドチェーンさせることで任意の処理でロード画面などを実行させることができる


# サンプルコード
``` HTML:Visualforceページ
<apex:page controller="VfTestCtrl" title="取引先責任者 編集" lightningStylesheets="true">
  <style>
    .spinnerBg{
        width: 100%;
        height: 100%;
        position: absolute;
        background-color: #000;
        opacity: 0.2;
        z-index: 999999;
    }
    .spinner{
        width: 100%;
        height: 100%;
        position: absolute;
        background-image: url("/img/loading32.gif");
        background-size: 16px;
        background-repeat: no-repeat;
        background-attachment: fixed;
        background-position: center;
        z-index: 9999999;
        opacity: 1;
    }
  </style>

  <apex:actionStatus id="spinnerStatus">
      <apex:facet name="start">
          <div class="spinnerBg"/>
          <div class="spinner"/>
      </apex:facet>
  </apex:actionStatus>

  <apex:form>
    <apex:outputPanel id="container">
      <apex:pageBlock title="取引先責任者 編集">
        <div style="min-height: 120px">
          <apex:pageMessages/>
        </div>
        <apex:pageBlockSection columns="1">
          <apex:inputField value="{!con.TestNum__c}"/>
        </apex:pageBlockSection>
        <apex:pageBlockButtons location="bottom">
          <apex:commandButton value="保存1" action="{!save}" reRender="container" status="spinnerStatus"/>
        </apex:pageBlockButtons>
        <apex:pageBlockButtons location="bottom">
          <apex:actionFunction name="save1B" action="{!clearVfMsg}" oncomplete="return false;" reRender="container" />
          <apex:commandButton value="保存2" onclick="save1B();" action="{!save}" reRender="container" status="spinnerStatus"/>
        </apex:pageBlockButtons>
        <apex:pageBlockButtons location="bottom">
          <apex:actionFunction name="save1C" action="{!clearVfMsg}" oncomplete="save2C();" reRender="container" immediate="true"/>
          <apex:actionFunction name="save2C" action="{!save}" reRender="container" status="spinnerStatus"/>
          <apex:commandButton value="保存3" onclick="save1C(); return false;"/>
        </apex:pageBlockButtons>
      </apex:pageBlock>
    </apex:outputPanel>
  </apex:form>
</apex:page>
```




``` java:VFコントローラ
public without sharing class VfTestCtrl {

    public Contact con { get; set; }
    private Id conId;

    /** 画面表示メッセージ */
    private static final String CONTACT_NOT_EXIST_MSG       = '取引先責任者が存在しません。';
    private static final String CONTACT_UPDATE_COMPLETE_MSG = '取引先責任者の更新が完了しました。';


    public VfTestCtrl() {
        this.conId = ApexPages.currentPage().getParameters().get('Id');

        List<Contact> conList = queryContact(conId);
        if (conList.isEmpty()) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, CONTACT_NOT_EXIST_MSG));
            return;
        }
        this.con = conList[0];
    }


    /** VFメッセージクリア処理 */
    public void clearVfMsg() {
        ApexPages.getMessages().clear();
    }


    /** 保存処理 */
    public void save() {
        List<Contact> conList = queryContact(conId);
        if (conList.isEmpty()) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, CONTACT_NOT_EXIST_MSG));
            return;
        }

        update con;
        ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.CONFIRM, CONTACT_UPDATE_COMPLETE_MSG));
        return;
    }


    /** 対象の取引先責任者を取得 */
    private List<Contact> queryContact(Id contactId) {
        return [
            SELECT
                Id,
                TestNum__c
            FROM
                Contact
            WHERE
                Id = :contactId
        ];
    }
}

```