LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-31

前回に引き続き、今回は 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 形式で記述するもので、集計クエリを書いたりもできます。

FetchXML の使用によるクエリの作成

サンプルの概要

今回のサンプルは特定のフィールドの値を操作するものではなく、各種ボタンをクリックした際に レコードの作成や削除など、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 を指定
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 を作成。中身を以下に差し替え。

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 でバージョン情報を更新。
image.png

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

msbuild /restore
msbuild /p:configuration=Release

4. https://make.powerapps.com に接続。管理者権限でログイン。すでに SampleSolution を入れた環境で Solutions を選択し、Import をクリック。コンパイルした zip をインポート。インポート時にパッケージが更新される旨が表示されるので、そのまま次へ。

5. 次の画面も既定のまま「インポート」をクリック。

6. インポートが終わったら Data | Entities | 取引先企業 | Forms | 取引先企業フォームを開き、「Switch to classic」をクリック。
image.png

7. 任意の一行テキストフィールドに対してプロパティよりコントロールを選択し、開発したコントロールを追加。

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

動作確認

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

1. アプリより取引先企業のレコードを開き、開発したコントロールを確認。
image.png

2.「値100でレコードを作成」ボタンをクリック。結果が表示されるので確認。
image.png

3. 同様に「値200でレコードを作成」「値300でレコードを作成」をクリックしてレコード作成後、「レコード数の更新」をクリック。結果を確認。
image.png

4.「削除するレコードの選択」ボタンをクリック。検索画面が出ることを確認。
image.png

5.「Web」とタイピングして作成したレコードが出ることを確認。
image.png

6. 1 件選択して「追加」をクリック。結果でレコード削除のメッセージを確認。
image.png

7. 「レコード数の更新」をクリック。結果を確認。
image.png

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

まとめ

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

目次へ戻る
次の記事へ

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2