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

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

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

##詳細画面の初期処理
一覧画面から詳細画面への遷移には2パターンあります。①登録(+)ボタンが押されたとき、②行選択されたときです。①②はルーティングのパラメータにIDがあるかどうかによって区別します。

詳細画面では、IDがなければ登録モードで開きます。IDがあるときは、渡されたIDをもとにドラフトの有無をチェックします。ドラフトがあればドラフトを編集モードで開き、ドラフトがなければ有効バージョンを照会モードで開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/220bb977-8ffc-48dc-db49-5f20de26c630.png)

##ステップ
1. 詳細画面のビューを作成
1. 一覧画面からのルーティング
1. 登録モードで開く
1. 編集・照会モードで開く
1. 照会モードから編集モードへの切り替え（ドラフトの作成）
1. 保存処理
1. ドラフトをキャンセル

###1. 詳細画面のビューを作成
####1.1. ビューを生成
以下のビューを作成します。（easy-ui5でのビュー生成方法は[こちら](https://qiita.com/tami/items/64ec2426b7225bab1575#31-%E3%83%93%E3%83%A5%E3%83%BC%E3%82%92%E7%94%9F%E6%88%90)）

```xml:view/Detail.view.xml
 <mvc:View controllerName="demo.bookingapp.controller.Detail"
  displayBlock="true"
  xmlns="sap.uxap"
  xmlns:m="sap.m"
  xmlns:mvc="sap.ui.core.mvc"
  xmlns:core="sap.ui.core">

  <ObjectPageLayout id="ObjectPageLayout" showFooter="{viewModel>/editable}" upperCaseAnchorBar="false" busy="{viewModel>/busy}">
    <headerTitle>
      <ObjectPageDynamicHeaderTitle >
        <heading>
          <m:VBox>
            <m:Title text="{orderId}"/>
            <m:Text text="{to_status/name}"/>          
          </m:VBox>
        </heading>
        <actions>
          <m:Button text="Edit" type="Emphasized" press="onEdit" visible="{= !${viewModel>/editable}}"/>
          <m:Button text="Delete" type="Ghost" press="onDelete" visible="{= !${viewModel>/editable}}"/>
        </actions>
      </ObjectPageDynamicHeaderTitle>
    </headerTitle>
    <sections>
      <ObjectPageSection id="orderInfo" titleUppercase="false" title="Order Information">
        <subSections>
          <ObjectPageSubSection>
            <blocks>
              <core:Fragment fragmentName="demo.bookingapp.fragments.Form" type="XML" />
            </blocks>
          </ObjectPageSubSection>
        </subSections>
      </ObjectPageSection>

      <ObjectPageSection id="orderItems" titleUppercase="false" title="Order Items">
        <subSections>
          <ObjectPageSubSection>
            <blocks>
              <core:Fragment fragmentName="demo.bookingapp.fragments.Items" type="XML" />
            </blocks>
          </ObjectPageSubSection>
        </subSections>
      </ObjectPageSection>
    </sections>
		<footer>
			<m:OverflowToolbar>
				<m:ToolbarSpacer />
        <m:Text text="{viewModel>/savingStatus}" visible="{viewModel>/isStatusVisible}"/>
				<m:Button type="Emphasized" text="Save" press="onSave"/>
				<m:Button type="Ghost" text="Cancel" press="onCancel"/>
			</m:OverflowToolbar>
		</footer>    
  </ObjectPageLayout>
</mvc:View>
```
詳細画面には2つのセクションがあり、それぞれフラグメントで実装しています。

- [ヘッダ部分のフラグメント](https://github.com/miyasuta/odata-v4-freestyle/blob/master/app/demo.bookingapp/uimodule/webapp/fragments/Form.fragment.xml)
- [明細部分のフラグメント](https://github.com/miyasuta/odata-v4-freestyle/blob/master/app/demo.bookingapp/uimodule/webapp/fragments/Items.fragment.xml)

####1.2. Routeを修正
manifest.jsonのroutesを以下のように修正します。
Detailのルートのpatternは`Detail/:id:`としています。コロンで囲ったパラメータは任意となります。

```manifest.json
    "routing": {
      "config": {
        "routerClass": "sap.m.routing.Router",
        "viewType": "XML",
        "viewPath": "demo.bookingapp.view",
        "controlId": "idAppControl",
        "controlAggregation": "pages",
        "async": true
      },
      "routes": [
        {
          "name": "List",
          "pattern": "",
          "target": [
            "TargetList"
          ]
        },
        {
          "name": "Detail",
          "pattern": "Detail/:id:",
          "target": [
            "TargetDetail"
          ]
        }
      ],
      "targets": {
        "TargetList": {
          "viewType": "XML",
          "viewId": "List",
          "viewName": "List"
        },
        "TargetDetail": {
          "viewType": "XML",
          "viewId": "Detail",
          "viewName": "Detail"
        }
      }
    }
```

###2. 一覧画面からのルーティング
一覧画面のコントローラーに、登録ボタンが押されたときおよび行選択されたときのイベントハンドラを追加します。イベントハンドラの中で詳細画面へのルーティングを行います。

```controller/List.controller.js
        //登録ボタンを押下
        onAdd: function () {
            this.getRouter().navTo("Detail", {});
        },

        //行選択
        onDetailPress: function (oEvent) {
            var id = oEvent.getSource().getBindingContext().getProperty("ID");
            this.getRouter().navTo("Detail", {
                id: id
            });
        },
```




###3. 登録モードで開く
詳細画面のコントローラーの初期処理について見ていきます。
ルートがマッチした際（_onRouteMatched）、ルーティングで渡されたIDを見て、IDがなければ登録処理（_handleCreate）を実行します。

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

    return Controller.extend("demo.bookingapp.controller.Detail", {
        onInit: function () {
            this._mode = undefined; //1: create 2: edit 3: display
            this.getRouter().getRoute("Detail").attachMatched(this._onRouteMatched, this);

            var oModel = new JSONModel({
                savingStatus: "",
                isStatusVisible: false,
                deleteEnabled: false,
                editable: false,
                busy: false
            });
            this.setModel(oModel, "viewModel");
        },

        _onRouteMatched: function (oEvent) {
            var oArgs = oEvent.getParameter("arguments");
            var id = oArgs.id;
            //create
            if (id === undefined) {
                this._handleCreate();
            //edit or display
            } else {
                this._handleEditDisplay(id);
            }                 
        },   
```

以下が新規のエンティティを登録する処理です。

```javascript
        _handleCreate: function () {
            var oListBinding = this.getModel().bindList("/Orders");
            var oContext = oListBinding.create();
            oContext.created()
            .then(() => {
                //登録が完了したら
                this.getView().bindObject(oContext.getPath());
                this._attachPatchEvents();
                this._mode = 1;                
                this._setEditable(true);
            });
        },
```

**OData V4の操作のポイント**
OData V4でのエンティティの登録は、①ListBindingを取得、②ListBindingの[create()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.ODataListBinding%23methods/create)メソッドを使って登録、という順序で行います。
create()メソッドが返すコンテキストの[created()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/created)というメソッドは、Promiseを返します。このPromiseは、バックエンドで登録が完了したときに解決します。

※ドラフトがある場合もない場合も、登録の操作は同じです。
※ドラフトに対応したエンティティの場合、上記の操作でドラフトバージョンが登録されます。有効バージョンは保存を行ったタイミングで登録されます。


###4. 編集・照会モードで開く
ルートがマッチした際（_onRouteMatched）、IDがあれば更新・照会処理（_handleEditDisplay）を実行します。

```javascript:controller/Detail.controller.js
        _handleEditDisplay: function (id) {
            var oModel = this.getModel();
            var oContextBinding = oModel.bindContext(`/Orders(ID=${id},IsActiveEntity=true)`);
            var oContext = oContextBinding.getBoundContext();
            oContext.requestProperty("HasDraftEntity")
            .then(hasDraft => {
                //bind context to the view
                this._bindContext(hasDraft, id, oContext);
                this._attachPatchEvents();
                this._getOrderId();

                if (hasDraft) {
                    //open in edit mode
                    this._mode = 2;
                    this._setEditable(true);
                } else {
                    //open in display mode
                    this._mode = 3;
                    this._setEditable(false);
                }
            })
            .catch(error => {
                console.log(error);
            });              
        },
```

↓まず、有効バージョンのコンテキストを取得し、

```javascript
            var oModel = this.getModel();
            var oContextBinding = oModel.bindContext(`/Orders(ID=${id},IsActiveEntity=true)`);
            var oContext = oContextBinding.getBoundContext();
```
↓有効バージョンがドラフトを持っているかどうか確認し、

```javascript
            oContext.requestProperty("HasDraftEntity")
            .then(hasDraft => {
                //bind context to the view
                this._bindContext(hasDraft, id, oContext);
```
↓最後にIsActiveEntityを指定してパスを作り、Viewにバインドします。
ドラフトが存在するときはドラフトバージョンをバインドするため、`IsActiveEntity=false`、そうでなければ有効バージョンをバインドするため、`IsActiveEntity=true`とします。

```javascript
        _bindContext: function (hasDraft, id, oContext) {
            //if draft exists, bind the draft version
            //else bind the active version
            var isActive = !hasDraft;
            this.getView().bindObject(`/Orders(ID=${id},IsActiveEntity=${isActive})`);
        },
```

**OData V4の操作のポイント**
上の処理では、選択されたエンティティがドラフトバージョンを持っているか確認するため、"HasDraftEntity"というプロパティを取得しています。

```javascript
oContext.requestProperty("HasDraftEntity")
```

プロパティを取得する場合、コンテキストの[getProperty()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/getProperty)または[requestProperty()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/requestProperty)メソッドを使用します。
getProperty()は、すでにクライアント側にそのプロパティを持っている場合に使用します。バックエンドへのリクエストは行われません。
requestProperty()は、プロパティがクライアント側になければバックエンドへのアクセスを行います。requestProperty()はPromiseを返します。

参考：[Accessing Data in Controller Code](https://sapui5.hana.ondemand.com/#/topic/17b30ac2d5474078be31e695e97450cc)

###5. 照会モードから編集モードへの切り替え（ドラフトの作成）
照会モードのエンティティは、まだドラフトバージョンを持っていません。編集(Edit)ボタンを押したタイミングでドラフトバージョンを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2befaac2-6699-7ea0-5fe1-fa5fbe356f77.png)

ドラフトの作成と有効化には、Ordersエンティティの**"draftEdit"**と**"draftActivate"**というアクションを使用します。ODataの$metadataを見てみると、以下のセクションがあります。ドラフト対応したエンティティの場合、以下のアクションが自動的に生成されます。

```xml
<Action Name="draftActivate" IsBound="true" EntitySetPath="in">
	<Parameter Name="in" Type="OrderingService.Orders"/>
	<ReturnType Type="OrderingService.Orders"/>
</Action>
<Action Name="draftEdit" IsBound="true" EntitySetPath="in">
	<Parameter Name="in" Type="OrderingService.Orders"/>
	<Parameter Name="PreserveChanges" Type="Edm.Boolean"/>
	<ReturnType Type="OrderingService.Orders"/>
</Action>
```

編集ボタンが押されたときのイベントハンドラを以下のように実装します。

```javascript:controller/Detail.controller.js
        onEdit: function () {
            //create draft
            this.setProperty("viewModel", "busy", true);
            this._createDraft()
            .then(oCreateContext => {
                this._attachPatchEvents();
                //set to edit mode
                this._mode = 2;
                this._setEditable(true);   
                this.setProperty("viewModel", "busy", false);             
            })
            .catch(error => {
                MessageBox.error(error.message, {});
                this.setProperty("viewModel", "busy", false);
            });         
        },
```

ドラフトを作成するのは、`_createDraft`メソッドの以下の部分です。draftEditのアクションを実行し、返ってきたコンテキストを画面にバインドします。

```javascript
        _createDraft: function () {
            return new Promise((resolve, reject) => {
                var oContext = this.getView().getObjectBinding().getBoundContext();
                var oModel = this.getModel();
                var oOperation = oModel.bindContext("OrderingService.draftEdit(...)", oContext);
                oOperation.execute()
                .then(oUpdatedContext => {
                    this.getView().bindObject(oUpdatedContext.getPath());
                    resolve();
                })                      
                .catch(error => {
                    reject(error);
                }); 
            });
        },
```

**OData V4の操作のポイント**
ODataのアクションやファンクションを実行する場合、モデルの[bindContext()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.ODataModel%23methods/bindContext)メソッドを使用します。第一引数のパスにアクション/ファンクションを指定し、第二引数には必要なパラメータを指定します。今回のケースはBound Action（特定のエンティティに対して実行する操作）のため、エンティティのコンテキストを渡します。
`"OrderingService.draftEdit(...)"`のように(...)をつけると、アクション/ファンクションが即座に実行されず、ContextBindingの[execute()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.ODataContextBinding%23methods/execute)メソッドを使って実行のタイミングをコントロールすることができます。

参考：[OData Operations](https://sapui5.hana.ondemand.com/#/topic/b54f7895b7594c61a83fa7257fa9d13f)

###6. 保存処理
保存(Save)ボタンが押されたら、ドラフトを有効化します。ドラフトを有効化するには、draftActivateのアクションを実行します。

```javascript:controller/Detail.controller.js
        onSave: function () {
            var hasError = this._doCheck();
            if (hasError) {
                return;
            }

            var oContext = this.getView().getBindingContext();
            var oModel = this.getModel();
            var oOperation = oModel.bindContext("OrderingService.draftActivate(...)", oContext);
            oOperation.execute()
            .then(() => {
                MessageToast.show("Data has been saved", {
                    closeOnBrowserNavigation: false
                });
                var oRouter = this.getOwnerComponent().getRouter();
                oRouter.navTo("List");
            })                      
            .catch(error => {               
                MessageBox.error(error.message, {});
            });
```
###7. ドラフトをキャンセル
キャンセル(Cancel)ボタンが押されたら、作成中のドラフトを削除します。ドラフトの削除には、コンテキストの[delete()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/delete)メソッドを使用します。これは[パート(2)](https://qiita.com/tami/items/64ec2426b7225bab1575#4-%E5%89%8A%E9%99%A4%E5%87%A6%E7%90%86)で有効バージョンを削除した方法と同じです。

```javascript:controller/Detail.controller.js
        onCancel: function () {
            const { confirmMessage, buttonText } = this._getDeleteMessage();
            MessageBox.confirm(confirmMessage, {
				actions: [buttonText, MessageBox.Action.CLOSE],
				emphasizedAction: buttonText,                
                onClose: this._discardDraft.bind(this)
            });
        },   

        _discardDraft: function (oAction) {
            if (oAction === MessageBox.Action.CLOSE) { 
                return
            }

            var oContext = this.getView().getBindingContext();
            oContext.delete("$auto")
            .then(()=> {
                var deleteMessage = this._getDeleteMessage().deleteMessage;
                MessageToast.show(deleteMessage, {
                    closeOnBrowserNavigation: false
                });
                var oRouter = this.getOwnerComponent().getRouter();
                oRouter.navTo("List");                
            });
        },
```
##まとめ
①OData V4でプロパティを取得するには、コンテキストの[getProperty()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/getProperty)または[requestProperty()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.Context%23methods/requestProperty)メソッドを使用する。

②OData V4でアクションやファンクションを実行するには、モデルの[bindContext()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.ODataModel%23methods/bindContext)メソッドを使用する。
`"OrderingService.draftEdit(...)"`のように(...)をつけると、アクション/ファンクションが即座に実行されず、ContextBindingの[execute()](https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.ODataContextBinding%23methods/execute)メソッドを使って実行のタイミングをコントロールすることができる。

③ドラフトの作成には"draftEdit"、ドラフトの有効化には"draftActivate"というアクションを使用する。