##はじめに
この記事は、OData V4でドラフト対応したアプリを作るシリーズの3回目です。
今回は、フリースタイルのUI5で以下のような画面を作成します。
ソースコードは以下に格納しています。
https://github.com/miyasuta/odata-v4-freestyle/tree/master/app/demo.bookingapp
##詳細画面の初期処理
一覧画面から詳細画面への遷移には2パターンあります。①登録(+)ボタンが押されたとき、②行選択されたときです。①②はルーティングのパラメータにIDがあるかどうかによって区別します。
詳細画面では、IDがなければ登録モードで開きます。IDがあるときは、渡されたIDをもとにドラフトの有無をチェックします。ドラフトがあればドラフトを編集モードで開き、ドラフトがなければ有効バージョンを照会モードで開きます。
##ステップ
- 詳細画面のビューを作成
- 一覧画面からのルーティング
- 登録モードで開く
- 編集・照会モードで開く
- 照会モードから編集モードへの切り替え(ドラフトの作成)
- 保存処理
- ドラフトをキャンセル
###1. 詳細画面のビューを作成
####1.1. ビューを生成
以下のビューを作成します。(easy-ui5でのビュー生成方法はこちら)
<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つのセクションがあり、それぞれフラグメントで実装しています。
####1.2. Routeを修正
manifest.jsonのroutesを以下のように修正します。
DetailのルートのpatternはDetail/:id:
としています。コロンで囲ったパラメータは任意となります。
"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. 一覧画面からのルーティング
一覧画面のコントローラーに、登録ボタンが押されたときおよび行選択されたときのイベントハンドラを追加します。イベントハンドラの中で詳細画面へのルーティングを行います。
//登録ボタンを押下
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)を実行します。
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);
}
},
以下が新規のエンティティを登録する処理です。
_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()メソッドを使って登録、という順序で行います。
create()メソッドが返すコンテキストのcreated()というメソッドは、Promiseを返します。このPromiseは、バックエンドで登録が完了したときに解決します。
※ドラフトがある場合もない場合も、登録の操作は同じです。
※ドラフトに対応したエンティティの場合、上記の操作でドラフトバージョンが登録されます。有効バージョンは保存を行ったタイミングで登録されます。
###4. 編集・照会モードで開く
ルートがマッチした際(_onRouteMatched)、IDがあれば更新・照会処理(_handleEditDisplay)を実行します。
_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);
});
},
↓まず、有効バージョンのコンテキストを取得し、
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);
↓最後にIsActiveEntityを指定してパスを作り、Viewにバインドします。
ドラフトが存在するときはドラフトバージョンをバインドするため、IsActiveEntity=false
、そうでなければ有効バージョンをバインドするため、IsActiveEntity=true
とします。
_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"というプロパティを取得しています。
oContext.requestProperty("HasDraftEntity")
プロパティを取得する場合、コンテキストのgetProperty()またはrequestProperty()メソッドを使用します。
getProperty()は、すでにクライアント側にそのプロパティを持っている場合に使用します。バックエンドへのリクエストは行われません。
requestProperty()は、プロパティがクライアント側になければバックエンドへのアクセスを行います。requestProperty()はPromiseを返します。
参考:Accessing Data in Controller Code
###5. 照会モードから編集モードへの切り替え(ドラフトの作成)
照会モードのエンティティは、まだドラフトバージョンを持っていません。編集(Edit)ボタンを押したタイミングでドラフトバージョンを作成します。
ドラフトの作成と有効化には、Ordersエンティティの**"draftEdit"と"draftActivate"**というアクションを使用します。ODataの$metadataを見てみると、以下のセクションがあります。ドラフト対応したエンティティの場合、以下のアクションが自動的に生成されます。
<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>
編集ボタンが押されたときのイベントハンドラを以下のように実装します。
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のアクションを実行し、返ってきたコンテキストを画面にバインドします。
_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()メソッドを使用します。第一引数のパスにアクション/ファンクションを指定し、第二引数には必要なパラメータを指定します。今回のケースはBound Action(特定のエンティティに対して実行する操作)のため、エンティティのコンテキストを渡します。
"OrderingService.draftEdit(...)"
のように(...)をつけると、アクション/ファンクションが即座に実行されず、ContextBindingのexecute()メソッドを使って実行のタイミングをコントロールすることができます。
###6. 保存処理
保存(Save)ボタンが押されたら、ドラフトを有効化します。ドラフトを有効化するには、draftActivateのアクションを実行します。
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()メソッドを使用します。これはパート(2)で有効バージョンを削除した方法と同じです。
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()またはrequestProperty()メソッドを使用する。
②OData V4でアクションやファンクションを実行するには、モデルのbindContext()メソッドを使用する。
"OrderingService.draftEdit(...)"
のように(...)をつけると、アクション/ファンクションが即座に実行されず、ContextBindingのexecute()メソッドを使って実行のタイミングをコントロールすることができる。
③ドラフトの作成には"draftEdit"、ドラフトの有効化には"draftActivate"というアクションを使用する。