【Fiori】Canvas Pageを使ったFiori elementsの拡張

##はじめに
Canvas Pageを使ったFiori elementsアプリの拡張方法について調べてみました。
Canvas Pageを使うと、Fiori elementsアプリに新しいページを追加することができます。これにより、Fiori elemensアプリ内部のナビゲーションターゲットとしてCanvas Pageを持ってくることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2e6bbdad-efba-ac20-5f9d-33b90600c392.png)

##ステップ
1. Canvas Pageのコンポーネントを作成する
2. Canvas PageをList Reportのナビゲーション先に指定する
3. List Reportからパラメータを受け渡す

##1. Canvas Pageのコンポーネントを作成する
Canvas Pageのコンポーネント作成方法は、基本的には[前回](https://qiita.com/tami/items/dfe536f46b2eedca44ab)紹介したObject Pageに埋め込むコンポーネントの作成方法と同じです。前回作成したライブラリに、comp2というフォルダを新たに作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e9b13953-00e9-f8b6-2efa-db9ebbf6a141.png)
前回はフラグメントのみの構成でしたが、今回はビュー + コントローラーとしました。（どちらの構成でもちゃんと動きます）

```json:manifest.json
{
	"_version": "1.8.0",
	"sap.app": {
		"id": "demo.testcomponent.comp2",
		"type": "component",
		"embeddedBy": "../",
		"i18n": "i18n/i18n.properties",
		"applicationVersion": {
			"version": "1.0.0"
		},
		"title": "{{compTitle}}",
		"description": "{{compDescription}}"
	},
	"sap.ui": {
		"technology": "UI5",
		"icons": {
			"icon": "",
			"favIcon": "",
			"phone": "",
			"phone@2": "",
			"tablet": "",
			"tablet@2": ""
		},
		"deviceTypes": {
			"desktop": true,
			"tablet": true,
			"phone": true
		},
		"supportedThemes": [
			"sap_fiori_3"
		]
	},
	"sap.ui5": {
		"rootView": {
			"viewName": "demo.testcomponent.comp2.view.App",
			"type": "XML",
			"async": true,
			"id": "App"
		},		
		"resources": {
			"js": [],
			"css": []
		},
		"dependencies": {
			"minUI5Version": "1.65.6",
			"libs": {
				"sap.ui.core": {
					"lazy": false
				},
				"sap.m": {}
			},
			"components": {}
		},
		"models": {
			"i18n": {
				"type": "sap.ui.model.resource.ResourceModel",
				"uri": "i18n/i18n.properties"
			}
		},
		"contentDensities": {
			"compact": true,
			"cozy": true
		},
		"routing": {
			"config": {
				"routerClass": "sap.m.routing.Router",
				"viewType": "XML",
				"async": true,
				"viewPath": "demo.testcomponent.comp2.view",
				"controlAggregation": "pages",
				"controlId": "app",
				"clearControlAggregation": false
			},
			"routes": [
				{
					"name": "App",
					"pattern": "",
					"target": [
						"App"
					]
				}
			],
			"targets": {
				"App": {
					"viewName": "App"
				}
			}
		}
	}
}
```

```javascript:component.js
sap.ui.define([
	"sap/ui/core/UIComponent",
	"sap/suite/ui/generic/template/extensionAPI/ReuseComponentSupport",
	"sap/ui/model/json/JSONModel"
], function(UIComponent, ReuseComponentSupport, JSONModel) {
	"use strict";
	
	return UIComponent.extend("demo.testcomponent.comp2.Component", {
		metadata: {
			manifest: "json",
			properties: {
				/* Standard properties for reuse components */
				uiMode: {
					type: "string",
					group: "standard"
				},
				semsnticObject: {
					type: "string",
					group: "standard"
				},
				stIsAreaVisible: {
					type: "boolean",
					group: "standard"
				},
				
				/* Component specific properties */
				documentNumber: {
					type: "string",
					group: "specific",
					defaultValue: ""
				},

			}
		},
		
		// Standard life time event of a component. Used to transform this component into a reuse component for Fiori Elements
		init: function () {
			//Transform this component into a reuse component for Fiori Elements:
			ReuseComponentSupport.mixInto(this, "compModel");
			// Defensive call of init of the super class:
			(UIComponent.prototype.init || jQuery.noop).apply(this, arguments);
			
			var oLocalModel = {
				lastRefreshed: new Date()
			};
			this.setModel(new JSONModel(oLocalModel), "localModel");
		},
		
        stRefresh: function(oModel, oBindingContext, oExtensionAPI)     		    
        },
 		
	});	
});
```

```xml:App.view.xml
<mvc:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m"
	xmlns:form="sap.ui.layout.form"
	xmlns:smartForm="sap.ui.comp.smartform"
	xmlns:smartField="sap.ui.comp.smartfield"
	controllerName="demo.testcomponent.comp2.controller.App" xmlns:html="http://www.w3.org/1999/xhtml">

	<App id="app">
		<form:SimpleForm id="simpleForm">
			<Label text="Created At" labelFor="createdAt"/>
			<Text id="createdAt" text="{
				path: 'CreatedAt',
				type: 'sap.ui.model.type.DateTime',
				formatOptions: {
					style: 'medium'
				}}"/>
			<Label text="Created by" labelFor="createdBy"/>
			<Text id="createdBy" text="{CreatedBy}"/>
			<Label text="Delivery Status" labelFor="deliveryStatus"/>
			<Text id="deliveryStatus" text="{DeliveryStatus}"/>

		</form:SimpleForm>
	</App>
	
</mvc:View>
```

この時点では、コントローラーには何も入れていません。

##2. Canvas PageをList Reportのナビゲーション先に指定する
List Reportで行を選択したらCanvas Pageに遷移するようにします。今回は、Delivery Statusが'D'の場合はCanvas Pageに、そうでない場合はもともとのObject Pageに遷移させます。

manifest.jsonにComponentUsageを追加します。

```json
	"sap.ui5": {
		...,
		"dependencies": {
			"minUI5Version": "1.65.6",
			"libs": {
				...,
				"demo.testcomponent": {
					"minVersion": "1.0.0"
				}
			},
			"components": {}
		},
		"componentUsages": {
			"myCanvasComponent": {
				"name": "demo.testcomponent.comp2",
				"settings": {},
				"componentData": {}
			}
		}
```
続いて、Object Pageの下にCanvas Pageの設定を追加します。Canvas PageはObject Pageと並列の関係なので、同じ階層に追加ます。

```json
		"pages": {
			"ListReport|Z_C_SO_MOB67": {
				"entitySet": "Z_C_SO_MOB67",
				"component": {
					
				},
				"pages": {
					"ObjectPage|Z_C_SO_MOB67": {
						
					},
					"myCanvasPage": {
						"component": {
							"name": "sap.suite.ui.generic.template.Canvas",
							"settings": {
								"requiredControls": {
									"footerBar": true
								}
							}
						},
						"implementingComponent": {
							"componentUsage": "myCanvasComponent",
							"settings": {}
						},
						"routingSpec": {
							"noOData": true,
							"noKey": true,
							"routeName": "toCanvasPage"
						}
					}
				}
```

List Reportのコントローラーの拡張に以下を追加します。一覧から条件を判定してCanvas Page（またはObject Page）に遷移します。routeNameにはmanifest.jsonで指定したCanvas PageのrouteNameを設定します。

```javascript
	onListNavigationExtension: function(oEvent) {
		 var oNavigationController = this.extensionAPI.getNavigationController();
		 var oBindingContext = oEvent.getSource().getBindingContext();
		 var oObject = oBindingContext.getObject();
		 if (oObject.DeliveryStatus == "D") {
		    oNavigationController.navigateInternal("", {     
		    	routeName: "toCanvasPage"
		    });  
		 } else {
		    // return false to trigger the default internal navigation
		    return false;
		 }
		 // return true is necessary to prevent further default navigation
		 return true;
	 },	
```

この結果、Delivery Statusが'D'の行を選択するとCanvas Pageに遷移します。
ただし、画面にコンテキストがバインドされていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e3155041-0025-752d-659b-f1eee0798a7c.png)

[ドキュメント](https://sapui5.hana.ondemand.com/#/topic/82c4b57424804ae2bc7f937c9b403525)を見ると以下の説明があり、何もしなくてもバインドされてくれそうですが・・・
>Note that the context of the parent page is passed to the canvas page in this scenario. In this example, this means that controls that are embedded in the theImplementingComponentQualifiedName component can be directly bound to properties of theMainEntitySet. 

私のやり方が間違っているのかもしれませんが、これ以上はわからなかったので、バインド用のパスをナビゲーションの時に渡すことにします。

##3. List Reportからパラメータを受け渡す
###List Report側
ナビゲーションの際にパラメータを渡すには、manifest.jsonのrouteingSpecで、noKeyをfalseにします。

```json
                        "routingSpec": {
                            "noOData": true,
                            "noKey": false,
                            "routeName": "toCanvasPage"
                        }
```

ナビゲーションの際にbindingContextからパスを取得し、エンコードしてナビゲーションのパラメータに渡します。エンコードするのは、スラッシュ（/）が入っているとナビゲーションがうまく動かないためです。

```javascript
		 if (oObject.DeliveryStatus == "D") {
		 	var encodedPath = encodeURIComponent(oBindingContext.getPath());
		    oNavigationController.navigateInternal(encodedPath, {     
		    	routeName: "toCanvasPage"
		    });  
		 } 
```

###Canvas Page側
Component.jsにパラメータを受け取る処理を追加します。

```javascript:Component.js
        stRefresh: function(oModel, oBindingContext, oExtensionAPI) {
		    this.oExtensionAPI = oExtensionAPI;
		    var oNavigationController = oExtensionAPI.getNavigationController();    
		    var aKeys = oNavigationController.getCurrentKeys();    
		    var sPath = decodeURIComponent(aKeys[aKeys.length - 1]);
		    var oComponentModel = this.getComponentModel();    
		    oComponentModel.setProperty("/sPath", sPath);     		    
        }
```
コントローラーでパスを取得し、フォームにバインドします。

```javascript:App.controller.js
		onInit: function () {
			var sPath = this.getOwnerComponent().getComponentModel().getProperty("/sPath");
			this.byId("simpleForm").bindElement(sPath);
		},
```

以上の設定により、Canvas Pageにデータが表示されるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6cfa3451-d916-50f2-0e50-c2b2bcb69597.png)

##Canvas Pageは使えるのか？
Fiori elementsアプリから別のページに遷移することは、External Navigationを使っても実現できます（※）。その際は遷移先のアプリの種類に制限はないので、そちらの方が便利な気がします。
Canvas Pageを使うメリットは、Canvas Pageをターゲットマッピングに登録しなくてもいいことくらいでしょうか。

※[Example: Replacing Standard Navigation in a Responsive Table in the List Report](https://sapui5.hana.ondemand.com/#/topic/a12ad6058db84e8cb60a3a9b2ade63fb)