Flexible Programming Model: 登録と照会で別のページを使う試みでできたこと、できなかったこと

## はじめに
[Fiexible Programming Model](https://qiita.com/tami/items/5205f84137b5455c74cc)を使うと、Fiori elementsのテンプレートで作ったアプリにカスタムページを差し込むことが可能です。このシナリオに関して、以下のYoutube動画で興味深い例が紹介されていました。

https://www.youtube.com/watch?v=ULtFF-0dn9w&list=PLo17W6sWsxWM0yervb4VUxglVAop34_DQ&index=4

それは、List Reportから開く「登録画面」にカスタムページを使うというものでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/228e1699-e01c-4417-8561-686a2f9a122c.png)

これは使えそう、ということで自分でも試してみました。この記事では、試してみた結果できたこと、できなかったことについて記録します。

## チャレンジ
元の動画では登録画面をカスタムページに置き換えるのみでしたが、照会画面は通常のObject Pageを使いたいと思ったので、以下のようなことができるか試してみました。

- List Reportで"Create"ボタンを押したらカスタムページを開く
- テーブル行をクリックしたらObject Pageを開く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4dcdc7d4-da4b-ddbe-5f5c-36a8fc724baf.png)

結果だけ先に知りたい方は、[動作確認](https://qiita.com/tami/items/bd16adb3ca014cf00b1b#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D)以降のセクションをお読みください。

## 前提
- OData V4を使用していること
- ドラフトが有効であること
以下のリポジトリにあるCAPのプロジェクトをベースに作成します。
https://github.com/miyasuta/flex-orders

UIアノテーションは以下のファイルで設定しています。
https://github.com/miyasuta/flex-orders/blob/main/srv/order-srv-ui.cds

## ステップ
1. List Reportのテンプレートでアプリを生成
1. カスタムページを作成
1. Routingの設定
1. コントローラーの拡張

### 1. List Reportのテンプレートでアプリを生成
ウィザードで"List Report Page"を選択してアプリを生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4284318a-c857-a4d2-2431-2a9e1613b3ea.png)

初期状態では、"Create"をクリックするとObject Pageが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/77b17ee7-a53a-d37d-8ed0-a42f20d753e1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e0a53717-5756-2682-6d3a-96a358907e16.png)

### 2. カスタムページを作成
以下のGitリポジトリを参考に、Custom Page用のビューとコントローラーを作成します。
https://github.com/SAP-samples/fiori-elements-v4-cap-advanced/tree/fpmVideos/app/travel_processor/webapp/ext/view

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/600be54b-31e5-c7d6-2c36-c3831ccff0dd.png)

#### ビュー
2ステップ + プレビュー画面のウィザードです。各ステップではBuilding Blocks（ネームスペースにmacros:とついているコントロール）を使用しています。プレビュー画面では、各ステップで入力した項目を照会モードで表示させるつもりです。
```xml:webapp/ext/view/OrderCreate.view.xml
<mvc:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns:macros="sap.fe.macros" xmlns:form="sap.ui.layout.form" id="review" controllerName="flex.createwizard.ext.view.OrderCreate" xmlns="sap.m">
    <NavContainer id="wizardNavContainer">
        <pages>
            <Page id="createOrderPage" title="Guided Order Creation">
                <content>
                    <Wizard id="CreateOrderWizard" class="sapUiResponsivePadding--header sapUiResponsivePadding--content" complete="reviewOrder">
                        <WizardStep id="stepCustomer" title="Customer" validated="true">
                            <VBox width="800px">      
                                <Text text="Select the customer." />
                                <macros:Field metaPath="customer_ID" id="customer" />
                            </VBox>
                        </WizardStep>
                        <WizardStep id="stepItems" validated="true" title="Items">
                            <macros:Table metaPath="to_Items/@com.sap.vocabularies.UI.v1.LineItem" id="items" busy="{ui>/isBusy}" />
                        </WizardStep>
                    </Wizard>
                </content>
                <footer>
                    <OverflowToolbar>
                        <ToolbarSpacer />
                        <Button id="cancelButton" text="Cancel" press="cancelDocument" />
                    </OverflowToolbar>
                </footer>
            </Page>
            <Page id="reviewPage" title="Review New Order">
                <content>
                    <macros:Form readOnly="true" metaPath="@com.sap.vocabularies.UI.v1.FieldGroup#main" id="reviewGeneral" />
                    <macros:Table readOnly="true" enableExport="false" isSearchable="false" personalization="false"
                        metaPath="to_Items/@com.sap.vocabularies.UI.v1.LineItem" id="itemsDisplay"  />
                    <MessageStrip class="sapUiSmallMarginBottom" type="Warning" text="By clicking on Create New Order you accept you read and accept our internal order guidelines." showIcon="true" />
                    <Button text="Create New Order" press="createOrder" type="Emphasized" />
                </content>     
            </Page>
        </pages>
    </NavContainer>
</mvc:View>
```
#### コントローラー
ビューには3つのボタンがあり、コントローラーの以下のイベントハンドラが対応しています。
**reviewOrder**: "Review"ボタンを押したときに、reviewPageを表示する
**createOrder**: "Create New Order"ボタンを押したときに、ドキュメントを保存し、List Report画面に戻る
**cancelDocument**: "Cancel"ボタンを押したときに、ドキュメントを破棄し、List Report画面に戻る

```javascript:
sap.ui.define(["sap/fe/core/PageController", "sap/m/MessageToast"], function(PageController, MessageToast) {
	"use strict";

	return PageController.extend("flex.createwizard.ext.view.OrderCreate", {

        reviewOrder: function () {
			this.byId("wizardNavContainer").to(this.byId("reviewPage"));
		},

        createOrder: function () {
            var that = this;
			this.editFlow.saveDocument(this.getView().getBindingContext()).then(function(){
                that.routing.navigateToRoute('OrdersList');
            })
		},

        cancelDocument: function () {
            var that = this;
			this.editFlow.cancelDocument(this.getView().getBindingContext(), {
                control: this.byId("cancelButton")
            }).then(function(){
                that.routing.navigateToRoute('OrdersList');
            })
		}
	});
});
```
注目したいのが、`this.editFlow.saveDocument`と`this.editFlow.cancelDocument`というメソッドです。これらは[ExtensionAPI](https://sapui5.hana.ondemand.com/#/api/sap.fe.core.ExtensionAPI)で提供される[EditFlow](https://sapui5.hana.ondemand.com/#/api/sap.fe.core.controllerextensions.EditFlow)というクラスのメソッドで、ODataのエンティティを操作することができます。
Extension APIを使うことができるのは、コントローラーが通常見る`sap.ui.core.mvc.Controller`ではなく`sap.fe.core.PageController`というFlexible Programming Model用の特別なクラスを使っているためです。

### 3. Routingの設定
作成したカスタムページを表示させるため、manifest.jsonでRoutingの設定を行います。

まず、"routes"セクションにカスタムページ用のルートを追加します。"pattern"にはもともとObjectPageで使用していたパターンを指定します。これは、"Create"ボタンが押されるとURLのハッシュ(#)以降が自動的に`Orders({key})`になるためです。ObjectPageには別のパターンを指定します。ここでは`OrderDetail({key}):?query:`としました。

```json:変更前
    "routing": {
      "config": {},
      "routes": [
        ...,
        {
          "pattern": "Orders({key}):?query:",
          "name": "OrdersObjectPage",
          "target": "OrdersObjectPage"
        },
        ...
      ]
```

```json:変更後
    "routing": {
      "config": {},
      "routes": [
        ...,
        {
          "pattern": "Orders({key}):?query:",
          "name": "CreateWizard",
          "target": "CreateWizard"
        },
        {
          "pattern": "OrderDetail({key}):?query:",
          "name": "OrdersObjectPage",
          "target": "OrdersObjectPage"
        },
        ...
      ]
```

次に、"targets"セクションにカスタムページ用のターゲットを追加します。カスタムページでは、`sap.fe.core.fpm`というコンポーネントを指定します。これにより、ビューでBuilding Blocksを使えるようになります。
参考：[SAP Fiori Elements: Flexible Programming Model Explorer > Building Blocks > Usage in Custom Apps](https://sapui5.hana.ondemand.com/test-resources/sap/fe/core/fpmExplorer/index.html#/buildingBlocks/guidance/guidanceCustomApps)

```json
      "targets": {
        "OrdersList": {
          ...
        },
        "CreateWizard": {
            "type": "Component",
            "id": "entryPage",
            "name": "sap.fe.core.fpm",
            "options": {
                "settings": {
                    "viewName": "flex.createwizard.ext.view.OrderCreate",
                    "entitySet": "Orders"
                }
            }
        },
        "OrdersObjectPage": {
          ...
        },
```

ここまでで"Create"ボタンを押すとウィザードが開くようになりました。しかし、List Reportでテーブルの行を選択したときにも、カスタムページが開いてしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/573a846f-409a-f654-809b-1e55907e1b36.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/db54baf9-6d8d-5390-fe82-f24e207befd0.png)
これは、URLのハッシュ以降が`#/Orders(1)`となっているためです。このパターンはカスタムページのルートに一致するため、カスタムページが開いてしまうのです。

### 4. コントローラーの拡張
テーブル行をクリックしたときにはObject Pageを開くように、コントローラーを拡張します。

#### manifest.json
まず、manifest.jsonの"sap.ui5"セクションに以下の設定を追加し、拡張したソースファイルの位置を指定します。

```json
  "sap.ui5": {
    ...
    "extends": {
        "extensions": {
            "sap.ui.controllerExtensions": {
                "sap.fe.templates.ListReport.ListReportController": {
                    "controllerName": "flex.createwizard.ext.controller.OrdersList"
                }
            }
        }
    }
  }
```

#### コントローラー拡張
上記で指定した位置にコントローラー拡張用のファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f26a2b16-9dcd-3d35-c771-b94cfa0c4a15.png)
ここでは、[Routing Extensibility](https://sapui5.hana.ondemand.com/test-resources/sap/fe/core/fpmExplorer/index.html#/controllerExtensions/controllerExtensionsOverview/routingExtensibility)と呼ばれるコントローラー拡張を使用して、Object Pageへナビゲーションする直前の動作を変更します。manifest.jsonで定義された"OrdersObjectPage"のルートへ遷移させるようにしています。

```javascript:OrdersList.controller.js
sap.ui.define(
	[
		"sap/ui/core/mvc/ControllerExtension",
	],
	function(ControllerExtension, ) {
		"use strict";
		return ControllerExtension.extend("flex.createwizard.ext.controller.OrdersList", {
			override: {
				routing: {					
					onBeforeNavigation: function(mNavigationParameters) {
						const oBindingContext = mNavigationParameters.bindingContext;
                        const parameters = {
                            key: /\(([^)]*)\)/.exec(oBindingContext.getPath())[1]
                        };
						this.base.getExtensionAPI().routing.navigateToRoute("OrdersObjectPage", parameters)
						return true;
					}
				}
			}
		});
	}
);
```

ここまでの設定でObject Pageは開きますが、Object Pageにデータが表示されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0ee7f9d6-868e-df72-6abb-f730d0d100b9.png)

manifest.jsonのターゲットに"contextPattern"を追加するとデータが表示されるようになります。推測ですが、Object Pageのパターンを`OrderDetail({key}):?query:`と変更したことにより、キー情報が取ってこられなくなったのだと思われます。contextPatternによってkeyがどのようにコンテキストにマッピングされるかを認識させることができるのだと理解しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0ad49b09-1ddb-ee72-420e-704f5c2a9b21.png)

## 動作確認
#### 登録画面
"Create"ボタンを押すと、登録画面（カスタムページ）が開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/1c3497a8-4f88-e2dd-ff33-465eb3805830.png)
テーブルから明細の入力もできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/df9e1a0e-6fea-931f-7df2-0e17c273fbcf.png)
Reviewページでは、以下の問題がありました。
- フォームをreadOnlyに設定したにも関わらず、入力できてしまう
- テーブルにデータが表示されない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5a8b94c2-6de0-0782-fb32-e6099012b3f9.png)

SAP Communityに質問してみたところ、以下の回答を得ました。
- フォームにはそもそもreadOnlyというプロパティはない。フォームが編集できるかどうかはアノテーションまたはコンテキストによって決まる - [FIori elements building blocks: readOnly property is not working](https://answers.sap.com/questions/13779746/fiori-elements-building-blocks-readonly-property-i.html)
- ドラフト状態ではまだDBにデータが保存されていないので、readOnlyのテーブルが入力されたデータを取りに行くことはできないのではないか - [FIori elements building blocks: Table does not show data](https://answers.sap.com/questions/13779747/fiori-elements-building-blocks-table-does-not-show.html) 

**1つのページでまだ保存されていない状態のデータを、入力・照会の2つのモードで表示させるというのは良いやり方ではない**、という結論です。これはFlexible Programming Modelの制約というよりも、使い方がよくない例でした。

#### Object Pageへの遷移
テーブル行をクリックしてObject Pageに遷移します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/500491fb-e2eb-84f5-3173-d2af0a10811a.png)
Object Pageが開きました。URLのパターンは`#/OrderDetail(ID=8a5a163c-7715-4649-adc8-c9877fb1dd7b,IsActiveEntity=true)`となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/625b82d9-2bad-dd00-5ba8-552c859fcf39.png)
"Edit"ボタンを押すと、カスタムページになりました。このときのURLパターンは`#/Orders(1)`となっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/65dd3063-8eee-c5cd-c0ce-c66e5e0044ed.png)
「"Edit"ボタンを押すとURLパターンが自動的にもともとのObject Pageのパターンに変わり、そのパターンはカスタムページで使用しているためカスタムページが開いてしまう」という動きのようです。
残念ながら、Editボタンを押したときのルートパターンを制御する方法は見つかっていません。

## 考察
今回は、登録用のカスタムページを、もともとObject Pageを表示していたパターンで開くようにしたため、編集モードに切り替えるとカスタムページが開いてしまいました。これを回避するには、以下のような方法をとる必要があるのではないかと考えます。

**案1**
- 登録用のカスタムページは既存のページとは別のパターンで開くようにする
- カスタムの「登録」ボタンを作成し、カスタムボタンが押されたらカスタムページを開く
※標準の「登録」ボタンをどうやって消すのかは調査が必要

**案2**
- 登録画面はBuilding Blocksを使用して独立したアプリケーションとして作り、カスタムの「登録」ボタンが押されたら登録画面にナビゲーションする