前回に引き続き、今回は Web API を使うサンプルを解説します。
Common Data Service (CDS) Web API
まずは CDS Web API について紹介します。CDS Web API は、データの操作を中心に、CDS に関わる機能を実行することができる開発者向け API です。内容は Dynamics CE Web API と同じです。
参照: Common Data Service Web API の使用
- エンティティのメタデータ提供
- レコードの CRUD 操作
- データのクエリ
- レコードの関連付けなどユーザー関数の実行
- 重複検出などのシステム関数の実行
PowerApps Component Framework (PCF) でもこの CDS Web API をコンテキストから取得して利用することができ、内部的にレコード操作など様々なアクションが実行できるようになっています。
FetchXML
サンプルにも出てますが、CDS/Dynamics CE のレコードをクエリする際、REST/OData クエリの他に、独自実装であるFetxhML というクエリが利用できます。これはクエリを XML 形式で記述するもので、集計クエリを書いたりもできます。
サンプルの概要
今回のサンプルは特定のフィールドの値を操作するものではなく、各種ボタンをクリックした際に レコードの作成や削除など、Web API で出来ることを実行するコントロールです。
プロジェクトの作成
今回も既に作成した SampleSolution に追加するように開発していきます。
1. 前回まで作ってきた PCFControls フォルダに移動して、フィールド用のコントロールを作成。
mkdir WebAPIControl
cd WebAPIControl
2. 以下のコマンドを実行してプロジェクトを作成。
- 名前空間とプロジェクト名の指定
- template 引数は field を指定
pac pcf init --namespace SampleNamespace --name TSWebAPI --template field
3. npm パッケージをリストアして任意の IDE でフォルダを開く。ここでは Visual Studio Code を利用。
npm install
code .
コントロールマニフェストの編集とリソースの追加
これまで同様にまずはマニフェストから編集します。
1. ControlManifest.Input.xml を以下の様に編集。
- 文字列フィールドにバインド
- resources に ts と css、既定の画像 を指定
- uses-feature で WebAPI を指定
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="SampleNamespace" constructor="TSWebAPI"
version="1.0.0"
display-name-key="Web API Sample Control"
description-key="Web API Sample Control"
control-type="standard">
<property name="stringProperty"
display-name-key="Text Field to bind the control"
description-key="Text Field to bind the control"
of-type="SingleLine.Text" usage="bound" required="true" />
<resources>
<code path="index.ts" order="1" />
<css path="css/TS_WebAPI.css" order="2" />
</resources>
<feature-usage>
<uses-feature name="WebAPI" required="true" />
</feature-usage>
</control>
</manifest>
2. ファイルの作成から css フォルダパスを含めて TS_WebAPI.css を作成。中身を以下に差し替え。
.SampleNamespace\.TSWebAPI
{
font-family: 'SegoeUI-Semibold', 'Segoe UI Semibold', 'Segoe UI Regular', 'Segoe UI';
color: #1160B7;
}
.SampleNamespace\.TSWebAPI .TSWebAPI_Container
{
overflow-x: auto;
}
.SampleNamespace\.TSWebAPI .SampleControl_WebAPI_Header
{
color: rgb(51, 51, 51);
font-size: 1rem;
padding-top: 20px;
}
.SampleNamespace\.TSWebAPI .result_container
{
padding-bottom: 20px;
}
.SampleNamespace\.TSWebAPI .SampleControl_WebAPI_ButtonClass
{
text-decoration: none;
display: inline-block;
font-size: 14px;
cursor: pointer;
color: #1160B7;
background-color: #FFFFFF;
border: 1px solid black;
padding: 5px;
text-align: center;
min-width: 300px;
margin-top: 10px;
margin-bottom: 5px;
display: block;
}
3. ターミナルより以下コマンドでビルドを実行。
npm run build
index.ts の編集
1. まずは import の追加と定数の定義。
import {IInputs, IOutputs} from "./generated/ManifestTypes";
2. クラスとプロパティの定義。
export class TSWebAPI implements ComponentFramework.StandardControl<IInputs, IOutputs>
{
// コンテキスト
private _context: ComponentFramework.Context<IInputs>;
// コントロールをホストするコンテナ
private _container: HTMLDivElement;
// Web API で操作するエンティティの論理名
private static _entityName:string = "account";
// Web API で作成するエンティティの名前フィールド
private static _requiredAttributeName: string = "name";
// Web API で作成するエンティティの名前フィールドの値
private static _requiredAttributeValue: string = "Web API Custom Control (Sample)";
// Web API で作成するエンティティの Revenue フィールド
private static _currencyAttributeName: string = "revenue";
// コントロール描写完了フラグ
private _controlViewRendered: Boolean;
// 各種ボタン
private _createEntity1Button: HTMLButtonElement;
private _createEntity2Button: HTMLButtonElement;
private _createEntity3Button: HTMLButtonElement;
private _deleteRecordButton: HTMLButtonElement;
private _fetchXmlRefreshButton: HTMLButtonElement;
private _oDataRefreshButton: HTMLButtonElement;
// 結果を保持するエレメント
private _odataStatusContainerDiv: HTMLDivElement;
private _resultContainerDiv: HTMLDivElement;
}
3. init 初期化メソッドを追加。
/**
* init メソッド
* @param context : 各種オブジェクトや API へのアクセスを提供するコンテキスト
* @param notifyOutputChanged : 出力変更通知のコールバック
* @param state : 前回保存したステート
* @param container : UI コントロールを保持するコンテナ
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement)
{
this._context = context;
this._controlViewRendered = false;
this._container = document.createElement("div");
this._container.classList.add("TSWebAPI_Container");
container.appendChild(this._container);
}
4. PCF からの変更通知を処理する updateView メソッドを追加。
public updateView(context: ComponentFramework.Context<IInputs>): void
{
if (!this._controlViewRendered)
{
// 各種ボタンの追加
this.renderCreateExample();
this.renderDeleteExample();
this.renderFetchXmlRetrieveMultipleExample();
this.renderODataRetrieveMultipleExample();
// Web API の結果を表示
this.renderResultsDiv();
this._controlViewRendered = true;
}
}
5. 各種ボタンの描写メソッドである renderCreateExample、renderDeleteExample、renderFetchXmlRetrieveMultipleExample、renderODataRetrieveMultipleExample、renderResultsDiv を追加。
private renderCreateExample() : void
{
// ヘッダーを追加
let headerDiv: HTMLDivElement = this.createHTMLDivElement("create_container", true,
"クリックして取引先企業レコードを作成");
this._container.appendChild(headerDiv);
// Revenue 100 でレコード作成するボタン 1 の作成
let value1:string = "100";
this._createEntity1Button = this.createHTMLButtonElement(
this.getCreateRecordButtonLabel(value1),
this.getCreateButtonId(value1),
value1,
this.createButtonOnClickHandler.bind(this));
// Revenue 200 でレコード作成するボタン 2 の作成
let value2:string = "200";
this._createEntity2Button = this.createHTMLButtonElement(
this.getCreateRecordButtonLabel(value2),
this.getCreateButtonId(value2),
value2,
this.createButtonOnClickHandler.bind(this));
// Revenue 300 でレコード作成するボタン 3 の作成
let value3:string = "300";
this._createEntity3Button = this.createHTMLButtonElement(
this.getCreateRecordButtonLabel(value3),
this.getCreateButtonId(value3),
value3,
this.createButtonOnClickHandler.bind(this));
this._container.appendChild(this._createEntity1Button);
this._container.appendChild(this._createEntity2Button);
this._container.appendChild(this._createEntity3Button);
}
private renderDeleteExample() : void
{
// ヘッダーを追加
let headerDiv: HTMLDivElement = this.createHTMLDivElement("delete_container", true,
"クリックして取引先企業を削除");
this._container.appendChild(headerDiv);
// 削除ボタンの追加
this._deleteRecordButton = this.createHTMLButtonElement(
"削除するレコードを選択",
"delete_button",
null,
this.deleteButtonOnClickHandler.bind(this));
this._container.appendChild(this._deleteRecordButton);
}
private renderODataRetrieveMultipleExample() : void
{
// ヘッダーを追加
let containerClassName: string = "odata_status_container";
let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerClassName, true,
"リストの更新");
this._container.appendChild(statusDivHeader);
// 結果コンテナの追加
this._odataStatusContainerDiv = this.createHTMLDivElement(containerClassName, false, undefined);
this._container.appendChild(this._odataStatusContainerDiv);
// RetrieveMultiple 実行ボタンの作成
this._fetchXmlRefreshButton = this.createHTMLButtonElement(
"レコード数の更新",
"odata_refresh",
null,
this.refreshRecordCountButtonOnClickHandler.bind(this));
this._container.appendChild(this._fetchXmlRefreshButton);
}
private renderFetchXmlRetrieveMultipleExample() : void
{
// ヘッダーを追加
let containerName: string = "fetchxml_status_container";
let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerName, true,
"クリックして Revenue の平均を取得");
this._container.appendChild(statusDivHeader);
let statusDiv: HTMLDivElement = this.createHTMLDivElement(containerName, false, undefined);
this._container.appendChild(statusDiv);
// RetrieveMultiple 実行ボタンの作成
this._oDataRefreshButton = this.createHTMLButtonElement(
"Revenue の平均を計算",
"odata_refresh",
null,
this.calculateAverageButtonOnClickHandler.bind(this));
this._container.appendChild(this._oDataRefreshButton);
}
private renderResultsDiv()
{
// ヘッダーを追加
let resultDivHeader: HTMLDivElement = this.createHTMLDivElement("result_container", true,
"結果");
this._container.appendChild(resultDivHeader);
// 結果用ラベルの追加
this._resultContainerDiv = this.createHTMLDivElement("result_container", false, undefined);
this._container.appendChild(this._resultContainerDiv);
// 結果ラベルの更新
this.updateResultContainerText("準備完了");
}
6. レコード作成処理である createButtonOnClickHandler メソッドを追加。
- context.webAPI より各種アクションの実行
private createButtonOnClickHandler(event: Event): void
{
// ボタンのラベルより Revenue の値を取得
let currencyAttributeValue:Number = parseInt(
(event.srcElement! as Element)!.attributes.getNamedItem("buttonvalue")!.value
);
// 名前がユニークになるように日付を追加
let recordName: string = TSWebAPI._requiredAttributeValue + "_" + Date.now();
// レコードのフィールドと値を追加
var data: any = {};
data[TSWebAPI._requiredAttributeName] = recordName;
data[TSWebAPI._currencyAttributeName] = currencyAttributeValue;
// コールバック内で利用するため this を一時的に格納
var thisRef = this;
// Web API でレコードを作成
this._context.webAPI.createRecord(TSWebAPI._entityName, data).then
(
function (response: ComponentFramework.EntityReference)
{
// 作成されたレコードをの結果を作成
let id: string = response.id;
let resultHtml: string = TSWebAPI._entityName + " レコードが以下の値で作成されました。"
resultHtml += "<br />";
resultHtml += "<br />";
resultHtml += "id: " + id;
resultHtml += "<br />";
resultHtml += "<br />";
resultHtml += TSWebAPI._requiredAttributeName + ": " + recordName;
resultHtml += "<br />";
resultHtml += "<br />";
resultHtml += TSWebAPI._currencyAttributeName + ": " + currencyAttributeValue;
thisRef.updateResultContainerText(resultHtml);
},
function (errorResponse: any)
{
// エラーのハンドル
thisRef.updateResultContainerTextWithErrorResponse(errorResponse);
}
);
}
7. レコードの削除処理を追加。
- context.utils.lookupObjects : 指定したエンティティの検索
private deleteButtonOnClickHandler() : void
{
// Lookpu を起動して削除するレコードを選択
var lookUpOptions: any =
{
entityTypes: [TSWebAPI._entityName]
};
// コールバック内で利用するため this を一時的に格納
var thisRef = this;
// context.util.lookupObjects でレコード参照を起動
var lookUpPromise: any = this._context.utils.lookupObjects(lookUpOptions);
lookUpPromise.then
(
// Lookup の結果
(data: ComponentFramework.EntityReference[]) =>
{
if (data && data[0])
{
// 選択されたレコードの Id とエンティティタイプを取得
let id: string = data[0].id;
let entityType: string = data[0].entityType!;
// Web API でレコードの削除
this._context.webAPI.deleteRecord(entityType, id).then
(
function (response: ComponentFramework.EntityReference)
{
// 削除成功のメッセージを表示
let responseId: string = response.id;
let responseEntityType: string = response.entityType!;
// Generate HTML to inject into the result div to showcase the deleted record
thisRef.updateResultContainerText("エンティティ:" + responseEntityType + " ID: " + responseId + "のレコードを削除しました。");
},
function (errorResponse: any)
{
// Error handling code here
thisRef.updateResultContainerTextWithErrorResponse(errorResponse);
}
);
}
},
(error: any) =>
{
// エラーのハンドル
thisRef.updateResultContainerTextWithErrorResponse(error);
}
);
}
8. FetchXML の実行を行う calculateAverageButtonOnClickHandler メソッドを追加。
- retrieveMultipleRecords に fetchXml パラメーター指定
private calculateAverageButtonOnClickHandler() : void
{
// 集計 FetchXML を作成
let fetchXML: string = "<fetch distinct='false' mapping='logical' aggregate='true'>";
fetchXML += "<entity name='" + TSWebAPI._entityName + "'>";
fetchXML += "<attribute name='" + TSWebAPI._currencyAttributeName + "' aggregate='avg' alias='average_val' />";
fetchXML += "<filter>";
fetchXML += "<condition attribute='" + TSWebAPI._currencyAttributeName + "' operator='not-null' />";
fetchXML += "</filter>";
fetchXML += "</entity>";
fetchXML += "</fetch>";
// コールバック内で利用するため this を一時的に格納
var thisRef = this;
// Web API で RetrieveMultipleRecords を実行
this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, "?fetchXml=" + fetchXML).then
(
function (response: ComponentFramework.WebApi.RetrieveMultipleResponse)
{
// 結果を取得して画面に表示
let averageVal:Number = response.entities[0].average_val;
// Generate HTML to inject into the result div to showcase the result of the RetrieveMultiple Web API call
let resultHTML: string = "取得した取引先企業の Revenue 平均は " + averageVal;
thisRef.updateResultContainerText(resultHTML);
},
function (errorResponse: any)
{
// エラーのハンドル
thisRef.updateResultContainerTextWithErrorResponse(errorResponse);
}
);
}
9. REST エンドポイントを実行する refreshRecordCountButtonOnClickHandler メソッドを追加。
private refreshRecordCountButtonOnClickHandler() : void
{
// OData クエリの咲く絵師
let queryString: string = "?$select=" + TSWebAPI._currencyAttributeName + "&$filter=contains(" + TSWebAPI._requiredAttributeName +
",'" + TSWebAPI._requiredAttributeValue + "')";
// コールバック内で利用するため this を一時的に格納
var thisRef = this;
// レコードの取得
this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, queryString).then
(
function (response: any)
{
let count1: number = 0;
let count2: number = 0;
let count3: number = 0;
// 取得したレコードを順次処理して集計
for (let entity of response.entities)
{
// 取得したレコードの Revenue 列の値
let value:Number = entity[TSWebAPI._currencyAttributeName];
// 同じ値のレコード数をカウント
if (value == 100)
{
count1++;
}
else if (value == 200)
{
count2++;
}
else if (value == 300)
{
count3++;
}
}
// 結果を HTML で表現
let innerHtml: string = "Use above buttons to create or delete a record to see count update";
innerHtml += "<br />";
innerHtml += "<br />";
innerHtml += "値が 100 のレコードは" + count1 + "件";
innerHtml += "<br />";
innerHtml += "値が 200 のレコードは" + count2 + "件";
innerHtml += "<br />";
innerHtml += "値が 300 のレコードは" + count3 + "件";
// 結果をコントロールに追加
if (thisRef._odataStatusContainerDiv)
{
thisRef._odataStatusContainerDiv.innerHTML = innerHtml;
}
thisRef.updateResultContainerText("レコードカウントの更新完了");
},
function (errorResponse: any)
{
// エラーのハンドル
thisRef.updateResultContainerTextWithErrorResponse(errorResponse);
}
);
}
10. その他の画面描写用プライベートメソッドの追加。
private updateResultContainerText(statusHTML: string) : void
{
if (this._resultContainerDiv)
{
this._resultContainerDiv.innerHTML = statusHTML;
}
}
private updateResultContainerTextWithErrorResponse(errorResponse: any) : void
{
if (this._resultContainerDiv)
{
// Retrieve the error message from the errorResponse and inject into the result div
let errorHTML: string = "Web API call でエラー:";
errorHTML += "<br />"
errorHTML += errorResponse.message;
this._resultContainerDiv.innerHTML = errorHTML;
}
}
private getCreateRecordButtonLabel(entityNumber: string): string
{
return "値" + entityNumber + "でレコードを作成";
}
private getCreateButtonId(entityNumber: string): string
{
return "create_button_" + entityNumber;
}
private createHTMLButtonElement(buttonLabel: string, buttonId: string, buttonValue: string | null, onClickHandler: (event?: any) => void): HTMLButtonElement
{
let button: HTMLButtonElement = document.createElement("button");
button.innerHTML = buttonLabel;
if (buttonValue)
{
button.setAttribute("buttonvalue", buttonValue);
}
button.id = buttonId;
button.classList.add("SampleControl_WebAPI_ButtonClass");
button.addEventListener("click", onClickHandler);
return button;
}
private createHTMLDivElement(elementClassName: string, isHeader: boolean, innerText?: string): HTMLDivElement
{
let div: HTMLDivElement = document.createElement("div");
if(isHeader)
{
div.classList.add("SampleControl_WebAPI_Header");
elementClassName += "_header";
}
if (innerText)
{
div.innerText = innerText.toUpperCase();
}
div.classList.add(elementClassName);
return div;
}
11. 必須である getOutputs と destroy メソッドを追加。
public getOutputs(): IOutputs
{
return {};
}
public destroy()
{
}
コントロールのパッケージ化と配布
これまでと同じ方法でパッケージ化と配布を実行。
1. SampleSolution フォルダに移動し、以下のコマンドを実行。
pac solution add-reference --path ../WebAPIControl
2. SampleSolution\Other\Solution.xml でバージョン情報を更新。
3. 以下コマンドでパッケージをビルド。
msbuild /restore
msbuild /p:configuration=Release
4. https://make.powerapps.com に接続。管理者権限でログイン。すでに SampleSolution を入れた環境で Solutions を選択し、Import をクリック。コンパイルした zip をインポート。インポート時にパッケージが更新される旨が表示されるので、そのまま次へ。
6. インポートが終わったら Data | Entities | 取引先企業 | Forms | 取引先企業フォームを開き、「Switch to classic」をクリック。
7. 任意の一行テキストフィールドに対してプロパティよりコントロールを選択し、開発したコントロールを追加。
8. すべての保存してフォームを公開。
動作確認
最後に動作を確認しましょう。
1. アプリより取引先企業のレコードを開き、開発したコントロールを確認。
2.「値100でレコードを作成」ボタンをクリック。結果が表示されるので確認。
3. 同様に「値200でレコードを作成」「値300でレコードを作成」をクリックしてレコード作成後、「レコード数の更新」をクリック。結果を確認。
4.「削除するレコードの選択」ボタンをクリック。検索画面が出ることを確認。
5.「Web」とタイピングして作成したレコードが出ることを確認。
6. 1 件選択して「追加」をクリック。結果でレコード削除のメッセージを確認。
8. 最後に「Revenue の平均を計算」をクリックして結果を確認。
まとめ
Web API を利用すると、ほぼすべての操作がプログラムで記述できるため、カスタムコントロールの機能も強力になります。また CDS/Dynamics で出来ることも学べますので、ぜひ Web API について色々調べてみてください。次回はクライアントサイド側の API を見ていきます。