【UI5・CAP】OData V4でドラフト対応したアプリを作る （2）UI5で一覧画面を作成

##はじめに
この記事は、[OData V4でドラフト対応したアプリを作る ](https://qiita.com/tami/items/30a75e454f4a5fb68fb8)シリーズの2回目です。
今回は、フリースタイルのUI5で以下のような画面を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/002d64b4-563d-1735-1bc5-382ac992a5aa.png)

ソースコードは以下に格納しています。
https://github.com/miyasuta/odata-v4-freestyle/tree/master/app/demo.bookingapp

##ステップ
1. プロジェクトを生成
1. CAPのODataにアクセス
1. 一覧表示用のビューを作成
1. 削除処理

###1. プロジェクトを生成
[generator-easy-ui5](https://www.npmjs.com/package/generator-easy-ui5)を使用してUI5プロジェクトを作成します。/appフォルダに移動して以下のコマンドを実行します。

```
yo easy-ui5
```
generatorの選択で、"generator-ui5-project"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/82b1044c-8228-ffad-16f9-2497ce3cc0a6.png)
以下のように質問に回答します。今回はローカルのみで実行するので、プラットフォームの選択で"Static webserver"を指定しました。ここはデプロイ先の環境に合わせて指定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/acc3c69e-7c39-942a-71d4-65e65069a3b7.png)
プロジェクトが生成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/db4f532e-08d4-1560-045e-21d559c8016e.png)

###2. CAPのODataにアクセス
####2.1. モデルを追加
以下のコマンドでmanifest.jsonにモデル（ODataサービス）を追加します。
コマンドのリファレンス：https://github.com/ui5-community/generator-ui5-project/

```
yo easy-ui5 project newmodel
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/687d67fe-3b97-9838-f34e-cbae7b7631d8.png)
モデルのタイプでOData v4を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/bdd5723f-907a-bf1a-80cc-8f25c1fe665c.png)
わかりやすさのために、データソース名などを以下のように変更しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c714312d-6e09-f875-07de-9aa0f266fff2.png)


####2.2. simpleproxyをインストール
ローカルで起動しているCAPのODataにアクセスするには、[ui5-middleware-simpleproxy](https://www.npmjs.com/package/ui5-middleware-simpleproxy)というミドルウェアを使います。1.で生成したプロジェクトのディレクトリに移動し、simpleproxyをインストールします。

```
npm install ui5-middleware-simpleproxy --save-dev
```
package.jsonのui5.dependenciesにも追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b113aef3-bf83-5e5f-b631-e7bac52c62c5.png)

ui5.yamlに以下の設定を追加します。これで、/bookings/で始まるリクエストが`http://localhost:4004/ordering/`へ転送されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/41b25c77-8754-a747-eb01-29dbc1e4368e.png)
`removeETag: true`のオプションについては、参考にした[GitHubのプロジェクト](https://github.com/SAP-samples/ui5-cap-event-app/blob/main/docs/documentation.md#form-ui)の設定に合わせています。このオプションをつけないと、データ取得時に以下のエラーになってしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/47f058b4-def9-db1b-5460-e34626173ef3.png)

####2.3. 動作確認
まだ画面も何もない状態ですが、`npm start`でアプリを実行し、$metadataが取得できていれば疎通OKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5aaccbca-6e41-4a7e-22d3-9bd09aadacef.png)

###3. 一覧表示用のビューを作成
####3.1. ビューを生成
以下のコマンドで新規ビューを作成します。

```
yo easy-ui5 project new view
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6c29d7a5-8b09-6d73-70ee-26c21db98cbb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b8a7ea46-4342-a4de-bc58-5a7122ce0f1d.png)

####3.2. Routeを修正
manifest.jsonのroutesを修正し、List Viewがデフォルトで表示されるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f4230b44-9d93-43ee-32bc-0ce16ea29915.png)

####3.3. ビューを実装
一覧を表示するビューを作成します。

```xml:List.view.xml
 <mvc:View controllerName="demo.bookingapp.controller.List"
  displayBlock="true"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc">
  
  <Table id="table" 
    items="{
      path: '/Orders',
      parameters: {
        $orderby: 'orderId'
      }}" mode="MultiSelect" selectionChange="onSelectionChange" busy="{viewModel>/busy}">
    <headerToolbar>
      <OverflowToolbar>
        <content>
          <Title text="Orders" />
          <ToolbarSpacer/>
          <Button icon="sap-icon://add" press="onAdd" /> 
          <Button text="Delete" press="onDelete" enabled="{viewModel>/deleteEnabled}" />     
        </content>
      </OverflowToolbar>
    </headerToolbar>
    <columns>
      <Column>
        <Text text="Order ID"/>
      </Column>
      <Column>
        <Text text="Order Date"/>
      </Column>   
      <Column>
        <Text text="Order Time"/>
      </Column>           
      <Column>
        <Text text="Comment"/>
      </Column>        
      <Column>
        <Text text="Created At"/>
      </Column>     
      <Column>
        <Text text="Status"/>
      </Column>    
      <Column>
        <Text text="Editing Status"/>
      </Column>              
    </columns>
    <items>
			<ColumnListItem type="Navigation" press="onDetailPress">
				<Text text="{orderId}"/>
        <Text text="{date}"/>
        <Text text="{time}"/>
        <Text text="{comment}"/>
        <Text text="{createdAt}"/>
        <Text text="{to_status/name}"/>
        <Text text="{ 
            path: 'HasDraftEntity',
            formatter: '.editingStatus'}"/>
			</ColumnListItem>    
    </items>  
  </Table>
</mvc:View>
```
ODataへのバインドは以下の部分で行っています。parametersで$orderbyを指定し、orderIdの昇順にソートされるようにしています。

**OData V2との違い**
OData V2ではパラメータは`$`なしで指定しましたが、V4ではODataのクエリオプションと同じ形式で、`$`をつけて指定します。

```xml
  <Table id="table" 
    items="{
      path: '/Orders',
      parameters: {
        $orderby: 'orderId'
      }}"
```

####3.4. コントローラーを実装
onInitではビューで使用するモデルを定義します。

```javascript
sap.ui.define([
    "demo/bookingapp/controller/BaseController",
    "sap/ui/model/json/JSONModel",
    "sap/m/MessageBox",
    "sap/m/MessageToast"
], function (Controller, JSONModel, MessageBox, MessageToast) {
    "use strict";

    return Controller.extend("demo.bookingapp.controller.List", {
        onInit: function () {
            var oModel = new JSONModel({
                deleteEnabled: false,
                busy: false
            });
            this.setModel(oModel, "viewModel");
        },
```

onSelectionChangeで、テーブルの行が選択されたときにDelteボタンを有効にします。

```javascript
        onSelectionChange: function (oEvent) {
            var aContexts = oEvent.getSource().getSelectedContexts();
            if (aContexts.length > 0) {
                this.setProperty("viewModel", "deleteEnabled", true);
            } else {
                this.setProperty("viewModel", "deleteEnabled", false);
            }
        },
```

※setPropertyメソッドはBaseControllerで定義しています。

```javascript
            setProperty: function (sModelName, sPropertyName, value) {
                this.getModel(sModelName).setProperty(`/${sPropertyName}`, value);
            },  
```

以下はテーブル項目"Editing Status"用のフォーマッターです。ドラフトが存在するかどうかによって、Active（ドラフトなし）or Draft（ドラフトあり）を表示します。

```javascript
        editingStatus: function (sHasDraftEntity) {
            var status = "";
            if (sHasDraftEntity === "Yes") {
                status = "Draft";
            } else {
                status = "Active";
            }
            return status;
        },
```


以上で、以下のような画面が表示されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/70d14032-ee1d-784f-ce9b-71d7c3fb2ba2.png)
行を選択するとDeleteボタンが有効になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/dfb4ad31-a650-3d3c-34d7-5adbae60c902.png)

###4. 削除処理
Deleteボタンを押したときの処理を実装します。ポイントは以下です。

- 複数行を選択してのDeleteが可能
- 選択行が単数か複数かによって削除確認のメッセージを変える（お好みで…）

####4.1. 削除処理を実装

```javascript
        //Deleteボタンが押されたら
        onDelete: function () {
            var oTable = this.byId("table");
            var aContexts = oTable.getSelectedContexts();
            this._orderId = aContexts[0].getProperty("orderId");
            var confirmMessage = this._getDeleteMessage(aContexts).confirmMessage

            MessageBox.confirm(confirmMessage, {
				actions: ["Delete", MessageBox.Action.CLOSE],
				emphasizedAction: "Delete",                
                onClose: this._deleteOrders.bind(this)
            });
        },

        //削除確認のメッセージを取得
        _getDeleteMessage: function (aContexts) {
            var oMessage = {};
            if (aContexts.length > 1) {
                oMessage.confirmMessage = `Delete selected orders?`;
                oMessage.deleteMessage = `Orders has been deleted`;
            } else {                
                oMessage.confirmMessage = `Delete order ${this._orderId}?`;
                oMessage.deleteMessage = `Order ${this._orderId} has been deleted`;
            }
            return oMessage;
        },

        //オーダー削除
        _deleteOrders: function (oAction) {
            if (oAction === MessageBox.Action.CLOSE) { 
                return
            }
                       
            var oTable = this.byId("table");
            var aContexts = oTable.getSelectedContexts();
            this.setProperty("viewModel", "busy", true);
            var aDeletePromises = aContexts.map(oContext => {
                return oContext.delete("$auto")
            });

            Promise.all(aDeletePromises)
            .then(() => {
                var deleteMessage = this._getDeleteMessage(aContexts).deleteMessage
                MessageToast.show(deleteMessage, {
                    closeOnBrowserNavigation: false
                });
                this._doRefresh();       
                this.setProperty("viewModel", "busy", false);     
            })
            .catch(error => {
                console.log(error);
                this.setProperty("viewModel", "busy", false);
            });
            
        },

        //削除結果をテーブルに反映
        _doRefresh: function () {
            var oTable = this.byId("table");
            oTable.getBinding("items").refresh();
        },
```

実際にオーダーを削除しているのは以下の部分です。テーブルから選択された行のコンテキストを取ってきて、各コンテキストに対して[delete()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/delete)を実行しています。`$auto`というのは更新用のグループIDで、同じグループIDを持つ操作が1つの$batchリクエストにまとめられます。

```javascript
        _deleteOrders: function (oAction) {
            ...                       
            var oTable = this.byId("table");
            var aContexts = oTable.getSelectedContexts();
            this.setProperty("viewModel", "busy", true);
            var aDeletePromises = aContexts.map(oContext => {
                return oContext.delete("$auto")
            });
```

**OData V2との違い**
OData V2の場合、[CRUD処理はモデルのメソッドを使用して実行していました](https://sapui5.hana.ondemand.com/#/topic/6c47b2b39db9404582994070ec3d57a2#loioff667e12b8714f3595e68f3e7c0e7a14)。これに対してOData V4では、コンテキストのメソッドを使用してCRUD処理を実施します。

####4.2. 動作確認

- 1行のみ指定した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/95d522cc-40dc-381c-b147-9953e47f29fe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/05a0e1c8-2688-6c1c-07ca-5fe659078d53.png)
- 複数行指定した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9a9c1598-64f5-7ac9-0a66-a4aaa3ce9264.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5fc92640-a59e-514d-84d2-6c91c6edf66c.png)

開発者ツールでNetworkタブを見ると、1つの$batchリクエストに複数のリクエストが入っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/abb51d4b-53d7-d89e-071a-0b8307e2f3c1.png)

##まとめ
①OData V4では、バインド時のパラメータは`$`をつけて指定する
[ドキュメント](https://sapui5.hana.ondemand.com/#/topic/abd4d7c7548d4c29ab8364d3904a6d74)では以下のように説明されています。

>**Change**
Binding parameter names: The binding parameter name for an OData system query option is identical to the system query option name: $expand, $select, ... (V2 uses expand, select).
**Reason**
Simplification: The OData V4 model simplifies the binding parameter structure to just one map where all entries in the map are OData query options, with the exception of entries that have a key starting with "$$" (binding-specific parameters). In all cases, the names of the binding parameters are exactly the same as in the OData URL sent to the server.

②OData V4では、コンテキストのメソッドを使用してCRUDオペレーションを行う
>**Change**
No CRUD methods on model: Model does not support the methods create, read, remove, update.
**Reason**
Simplification: read, update, create and remove operations are available implicitly via the bindings. Bindings can also be used without controls. It is not possible to trigger requests for specific OData URLs. For more information, see [Accessing Data in Controller Code](https://sapui5.hana.ondemand.com/#/topic/17b30ac2d5474078be31e695e97450cc.html).