PowerApps コンポーネントフレームワーク : Web API サンプル

[前回](https://qiita.com/kenakamu/items/ba99090d501bf815a7bf)に引き続き、今回は 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 の使用](https://docs.microsoft.com/ja-jp/powerapps/developer/common-data-service/webapi/overview)

- エンティティのメタデータ提供
- レコードの CRUD 操作
- データのクエリ
- レコードの関連付けなどユーザー関数の実行
- 重複検出などのシステム関数の実行

PowerApps Component Framework (PCF) でもこの CDS Web API をコンテキストから取得して利用することができ、内部的にレコード操作など様々なアクションが実行できるようになっています。

## FetchXML

サンプルにも出てますが、CDS/Dynamics CE のレコードをクエリする際、REST/OData クエリの他に、独自実装であるFetxhML というクエリが利用できます。これはクエリを XML 形式で記述するもので、集計クエリを書いたりもできます。

[FetchXML の使用によるクエリの作成](https://docs.microsoft.com/ja-jp/powerapps/developer/common-data-service/use-fetchxml-construct-query)

# サンプルの概要

今回のサンプルは特定のフィールドの値を操作するものではなく、各種ボタンをクリックした際に レコードの作成や削除など、Web API で出来ることを実行するコントロールです。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/86733e10-3f80-eadb-e0d7-5a12ecf1a2d4.png"/>


# プロジェクトの作成

今回も既に作成した SampleSolution に追加するように開発していきます。

1\. 前回まで作ってきた PCFControls フォルダに移動して、フィールド用のコントロールを作成。

```shell
mkdir WebAPIControl
cd WebAPIControl
```

2\. 以下のコマンドを実行してプロジェクトを作成。

- 名前空間とプロジェクト名の指定
- template 引数は field を指定

```shell
pac pcf init --namespace SampleNamespace --name TSWebAPI --template field
```

3\. npm パッケージをリストアして任意の IDE でフォルダを開く。ここでは Visual Studio Code を利用。

```shell
npm install
code .
```

# コントロールマニフェストの編集とリソースの追加

これまで同様にまずはマニフェストから編集します。

1\. ControlManifest.Input.xml を以下の様に編集。

- 文字列フィールドにバインド
- resources に ts と css、既定の画像 を指定
- uses-feature で WebAPI を指定

```xml:ControlManifest.Input.xml
<?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 を作成。中身を以下に差し替え。

```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\. ターミナルより以下コマンドでビルドを実行。

```shell
npm run build
```

# index.ts の編集

1\. まずは import の追加と定数の定義。

```typescript
import {IInputs, IOutputs} from "./generated/ManifestTypes";
```
2\. クラスとプロパティの定義。

```typescript
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 初期化メソッドを追加。

```typescript
/**
* 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 メソッドを追加。

```typescript
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 を追加。

```typescript
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 より各種アクションの実行

```typescript
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 : 指定したエンティティの検索

```typescript
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 パラメーター指定

```typescript
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 メソッドを追加。

```typescript
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\. その他の画面描写用プライベートメソッドの追加。

```typescript
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 メソッドを追加。

```typescript
public getOutputs(): IOutputs
{
	return {};
}

public destroy()
{
}
```

# コントロールのパッケージ化と配布

これまでと同じ方法でパッケージ化と配布を実行。

1\. SampleSolution フォルダに移動し、以下のコマンドを実行。

```shell
pac solution add-reference --path ../WebAPIControl
```

2\. SampleSolution\Other\Solution.xml でバージョン情報を更新。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/6b3b5862-992e-a6f0-a8c5-b7f63ac976bf.png)


3\. 以下コマンドでパッケージをビルド。

```shell
msbuild /restore
msbuild /p:configuration=Release
```

4\. https://make.powerapps.com に接続。管理者権限でログイン。すでに SampleSolution を入れた環境で Solutions を選択し、Import をクリック。コンパイルした zip をインポート。インポート時にパッケージが更新される旨が表示されるので、そのまま次へ。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/98c8d5ec-dc6a-0935-71ee-00bcbbcb7739.png"/>

5\. 次の画面も既定のまま「インポート」をクリック。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/f0fc2757-ac35-7a57-8092-c25ae9dcb458.png"/>

6\. インポートが終わったら Data | Entities | 取引先企業 | Forms | 取引先企業フォームを開き、「Switch to classic」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/ae8e7c7d-deea-afc6-85f3-3841fa8366aa.png)

7\. 任意の一行テキストフィールドに対してプロパティよりコントロールを選択し、開発したコントロールを追加。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/59fd2bd8-5a26-9034-8066-78dea3afe9d5.png"/>

8\. すべての保存してフォームを公開。

# 動作確認

最後に動作を確認しましょう。

1\. アプリより取引先企業のレコードを開き、開発したコントロールを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/55a11cec-fbb8-48be-be73-aaaa7f281f9f.png)

2\.「値100でレコードを作成」ボタンをクリック。結果が表示されるので確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/5b10e182-a390-b8e2-a5ea-22f168642bf9.png)

3\. 同様に「値200でレコードを作成」「値300でレコードを作成」をクリックしてレコード作成後、「レコード数の更新」をクリック。結果を確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/b0eea91b-4772-be58-2f4a-0df5ec13c11b.png)

4\.「削除するレコードの選択」ボタンをクリック。検索画面が出ることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/375bc2a5-b7e6-d5bf-4729-1356f92bbcc6.png)

5\.「Web」とタイピングして作成したレコードが出ることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/9bd9ea45-92a3-5191-2422-183cf72af2ce.png)

6\. 1 件選択して「追加」をクリック。結果でレコード削除のメッセージを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/216bd03b-ab10-be11-c7bb-640cc7978740.png)

7\. 「レコード数の更新」をクリック。結果を確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/80fdc7ea-9e06-d752-602d-680d3732f679.png)

8\. 最後に「Revenue の平均を計算」をクリックして結果を確認。

# まとめ

Web API を利用すると、ほぼすべての操作がプログラムで記述できるため、カスタムコントロールの機能も強力になります。また CDS/Dynamics で出来ることも学べますので、ぜひ Web API について色々調べてみてください。次回はクライアントサイド側の API を見ていきます。

[目次へ戻る](https://qiita.com/kenakamu/items/5dd4c64b14c453fa7d85)
[次の記事へ](https://qiita.com/kenakamu/items/ff58a7d0bca9f0e5e8ce)