13.2K Views
June 24, 23
スライド概要
Naoya Okada
Power Apps component framework でクリックを 検知するコンポーネント作ってみた 2023/06/24 Japan Power Platform Game Builders #6 岡田 尚也
自己紹介 ▪ Twitter https://twitter.com/nanoka7o8 ▪ Zenn https://zenn.dev/nanoka ▪ Qiita https://qiita.com/nanoka
なんでクリックを検知するコンポーネント作ったの? みなさんは、Power Apps でゲームを作成するときに、「Power Apps クリック 座標 取得」って 検索したことはないでしょうか わたしはあります そのたびに、あーまだ標準的な機能ではできないんだなと感じるのを繰り返していたのですが、 今回衝動的に、なければ作ればいいか!と思って作ってみたというのがきっかけです 作ってみると、そこまで複雑な手順ではなかったので、ぜひみなさんにもどれくらいの手間で、どんな ものが作れるか知っていただき、 Power Apps でゲームを作成する際の1つの手段としていただ けたらなと思いお話しにきました
目次 ▪ PCF(Power Apps component framework)ってなに? ▪ 開発環境をつくってみよう ▪ クリックを検知するコンポーネントを開発してみよう (デモ) ▪ リリースしてみよう (デモ)
PCF(Power Apps component framework)ってなに?
PCF(Power Apps component framework)って なに? ▪ Power Apps のアプリで使えるコンポーネント(部品)を開発するためのもの ▪ Power Apps には利用者が独自に JavaScript(TypeScript) で開発した部品を導入す る仕組みがあり、この部品をコード コンポーネントという ▪ この仕組み、枠組みを利用するためのプログラムのテンプレートなどを PCF という ▪ どちらかというとプロ開発者向け
どんなものが開発できる? ▪ JavaScript で実装できる範囲のものであれば、だいたい開発できる ▪ キャンバスアプリ、モデル駆動型アプリ、一部 Power Pages にも対応したものを開発できる ▪ 大きくフィールド コンポーネントと、データセット コンポーネントの2種類が存在する
フィールド コンポーネント ▪ テキスト入力のようなものが開発できる ▪ (モデル駆動型アプリでは)単一の入力項目に重ねるように設定する https://pcf.gallery/pcf-qr-code/
データセット コンポーネント ▪ ギャラリーやデータテーブルのようなものが開発できる ▪ (モデル駆動型アプリでは)複数のレコードを表示するビューなどに重ねるように設定する https://pcf.gallery/gantt-view/
クリックを検知するコンポーネント
参考:公開しているクリック & ドラッグ コンポーネント ▪ https://github.com/7o83/PCF_Get_XY_Control
開発環境をつくってみよう
開発環境をつくってみよう 今回は WSL と VSCode を使った PCF 開発環境を作る 大きくは、以下の3手順を行うWSL をインストールする ▪ WSL 環境の準備 ▪ .NET SDK のインストール ▪ VSCode のインストール
開発環境をつくってみよう(WSL 環境の準備) まずは、WSL をインストールする [手順] ▪ WSL をインストールする ▪ Ubuntu を入手し利用可能にする ▪ WSL に Node.js をインストールする [参考] ▪ Node.js を Linux 用 Windows サブシステム (WSL2) にインストールする https://learn.microsoft.com/ja-jp/windows/devenvironment/javascript/nodejs-on-wsl
WSL をインストールする PowerShell を管理者権限で起動し wsl – install を実行する
Ubuntu を入手し利用可能にする Microsoft Store で Ubuntu を入手する
WSL に Node.js をインストールする Ubuntu を起動し、以下のコマンドで、nvm 経由で Node.js をインストールする • sudo apt update • sudo apt upgrade • curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash • source ~/.bashrc • nvm install --lts
開発環境をつくってみよう(.NET SDK のインストール) 次に、 Power Platform Tools for Visual Studio Code の動作やコンポーネントのビルド に必要な .NET SDK をインストールする [手順] ▪ WSL に .NET SDK をインストールする [参考] ▪ Ubuntu に .NET SDK または .NET ランタイムをインストールする https://learn.microsoft.com/ja-jp/dotnet/core/install/linux-ubuntu
WSL に .NET SDK をインストールする Ubuntu を起動し、以下のコマンドで、.NET SDK をインストールする • sudo apt-get update && ¥ sudo apt-get install -y dotnet-sdk-7.0
開発環境をつくってみよう(VSCode のインストール) 最後に、Visual Studio Code と必要な拡張機能をインストールする [手順] ▪ Visual Studio Code をインストールする ▪ Remote - WSL 拡張機能をインストールする ▪ Power Platform Tools for Visual Studio Code拡張機能をインストールする [参考] ▪ Visual Studio Code をインストールする https://learn.microsoft.com/ja-jp/windows/devenvironment/javascript/nodejs-on-wsl#install-visual-studio-code
Visual Studio Code をインストールする Visual Studio Code をホストマシン(Windows)にインストールする [参考] ▪ Visual Studio Code https://code.visualstudio.com/
Remote - WSL 拡張機能をインストールする Visual Studio Code を起動し、Remote - WSL 拡張機能 をインストールする [参考] ▪ Remote - WSL 拡張機能 https://marketplace.visualstudio.com/items?itemName=ms-vscoderemote.vscode-remote-extensionpack
Power Platform Tools for Visual Studio Code拡張機能をインストールする Visual Studio Code を起動し、Power Platform Tools for Visual Studio Code拡張 機能をインストールする
クリックを検知する コンポーネントを 開発してみよう
クリックを検知するコンポーネントを開発してみよう 以下の3手順を行う ※たくさんファイルができるが編集するのは3ファイル程度でOK ▪ 新しいプロジェクトの作成 ▪ マニフェストの実装(ControlManifest.Input.xml) ▪ コンポーネント ロジックの実装(index.ts / css)
新しいプロジェクトの作成(作業フォルダの作成) 以下コマンドを実行し、作業フォルダを作成、VSCode を起動する • mkdir -m777 01_demo • mkdir -m777 01_demo/20230624_demo • cd 01_demo/20230624_demo • code .
新しいプロジェクトの作成(プロジェクトの作成) 以下コマンドを実行し、新しいプロジェクトを作成する • pac pcf init --namespace ns20230624demo --name ctl20230624demo --template field -run-npm-install
マニフェストの実装 コンポーネントのプロパティや、CSS 等のファイルへの参照を追加する ▪ ControlManifest.Input.xml を開く
マニフェストの実装(コンポーネントのプロパティ) ▪ 22行目に以下を加える <property name="offsetX" display-name-key="offsetX Value" description-key="offsetX Value" of-type-group="numbers" usage="bound" required="true" /> <property name="offsetY" display-name-key="offsetY Value" description-key="offsetY Value" of-type-group="numbers" usage="bound" required="true" /> <property name="display" display-name-key="display Value (Transparent or not)" description-key="display Value (Transparent or not)" of-type="TwoOptions" usage="bound" required="true" /> ▪ 21行目を消す
マニフェストの実装(コンポーネントのプロパティ) ▪ 20行目に以下を加える <type-group name="numbers"> <type>Whole.None</type> <type>Currency</type> <type>FP</type> <type>Decimal</type> </type-group>
マニフェストの実装(コンポーネントのプロパティ)
現時点のマニフェストは以下の通り(コピーしてお使いください)
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="ns20230624demo" constructor="ctl20230624demo" version="0.0.1" display-name-key="ctl20230624demo" description-key="ctl20230624demo description" control-type="standard" >
<!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this contr ol will be considered as premium and please also add the external domain it is using.
If it is not using any external service, please set the enabled="false" and DO NOT add any domain below. The "enabled" will be false by default.
Example1:
<external-service-usage enabled="true">
<domain>www.Microsoft.com</domain>
</external-service-usage>
Example2:
<external-service-usage enabled="false">
</external-service-usage>
-->
<external-service-usage enabled="false">
<!--UNCOMMENT TO ADD EXTERNAL DOMAINS
<domain></domain>
<domain></domain>
-->
</external-service-usage>
<type-group name="numbers">
<type>Whole.None</type>
<type>Currency</type>
<type>FP</type>
<type>Decimal</type>
</type-group>
<!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->
<property name="offsetX" display-name-key="offsetX Value"
description-key="offsetX Value" of-type-group="numbers" usage="bound"
required="true" />
<property name="offsetY" display-name-key="offsetY Value"
description-key="offsetY Value" of-type-group="numbers" usage="bound"
required="true" />
<property name="display" display-name-key="display Value (Transparent or not)"
description-key="display Value (Transparent or not)" of-type="TwoOptions" usage="bound"
required="true" />
<!-Property node's of-type attribute can be of-type-group attribute.
Example:
<type-group name="numbers">
<type>Whole.None</type>
<type>Currency</type>
<type>FP</type>
<type>Decimal</type>
</type-group>
<property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
-->
<resources>
<code path="index.ts" order="1"/>
<!-- UNCOMMENT TO ADD MORE RESOURCES
<css path="css/ctl20230624demo.css" order="1" />
<resx path="strings/ctl20230624demo.1033.resx" version="1.0.0" />
-->
</resources>
<!-- UNCOMMENT TO ENABLE THE SPECIFIED API
<feature-usage>
<uses-feature name="Device.captureAudio" required="true" />
<uses-feature name="Device.captureImage" required="true" />
<uses-feature name="Device.captureVideo" required="true" />
<uses-feature name="Device.getBarcodeValue" required="true" />
<uses-feature name="Device.getCurrentPosition" required="true" />
<uses-feature name="Device.pickFile" required="true" />
<uses-feature name="Utility" required="true" />
<uses-feature name="WebAPI" required="true" />
</feature-usage>
-->
</control>
</manifest>
マニフェストから型定義を生成 以下コマンドを実行し、型定義を自動生成する • npm run refreshTypes
一旦デバッグ(プロパティの反映確認) 以下コマンドを実行し、動作を確認する • npm run build • npm start watch
マニフェストの実装(CSS ファイルの参照の追加) ▪ 49行目に以下を加える <css path="css/ctl20230624demo.css" order="1" /> ▪ css フォルダを作成し、ctl20230624demo.css ファイルを作成する
マニフェストの実装(CSS ファイルの参照の追加)
現時点のマニフェストは以下の通り(コピーしてお使いください)
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="ns20230624demo" constructor="ctl20230624demo" version="0.0.1" display-name-key="ctl20230624demo" description-key="ctl20230624demo description" control-type="standard" >
<!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this control will be considered as premium and please also add the external domain it is using.
If it is not using any external service, please set the enabled="false" and DO NOT add any domain below. The "enabled" will be false by default.
Example1:
<external-service-usage enabled="true">
<domain>www.Microsoft.com</domain>
</external-service-usage>
Example2:
<external-service-usage enabled="false">
</external-service-usage>
-->
<external-service-usage enabled="false">
<!--UNCOMMENT TO ADD EXTERNAL DOMAINS
<domain></domain>
<domain></domain>
-->
</external-service-usage>
<type-group name="numbers">
<type>Whole.None</type>
<type>Currency</type>
<type>FP</type>
<type>Decimal</type>
</type-group>
<!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->
<property name="offsetX" display-name-key="offsetX Value"
description-key="offsetX Value" of-type-group="numbers" usage="bound"
required="true" />
<property name="offsetY" display-name-key="offsetY Value"
description-key="offsetY Value" of-type-group="numbers" usage="bound"
required="true" />
<property name="display" display-name-key="display Value (Transparent or not)"
description-key="display Value (Transparent or not)" of-type="TwoOptions" usage="bound"
required="true" />
<!-Property node's of-type attribute can be of-type-group attribute.
Example:
<type-group name="numbers">
<type>Whole.None</type>
<type>Currency</type>
<type>FP</type>
<type>Decimal</type>
</type-group>
<property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
-->
<resources>
<code path="index.ts" order="1"/>
<css path="css/ctl20230624demo.css" order="1" />
<!-- UNCOMMENT TO ADD MORE RESOURCES
<css path="css/ctl20230624demo.css" order="1" />
<resx path="strings/ctl20230624demo.1033.resx" version="1.0.0" />
-->
</resources>
<!-- UNCOMMENT TO ENABLE THE SPECIFIED API
<feature-usage>
<uses-feature name="Device.captureAudio" required="true" />
<uses-feature name="Device.captureImage" required="true" />
<uses-feature name="Device.captureVideo" required="true" />
<uses-feature name="Device.getBarcodeValue" required="true" />
<uses-feature name="Device.getCurrentPosition" required="true" />
<uses-feature name="Device.pickFile" required="true" />
<uses-feature name="Utility" required="true" />
<uses-feature name="WebAPI" required="true" />
</feature-usage>
-->
</control>
</manifest>
コンポーネント ロジックの実装(displayの反映)
▪ 4行目に以下を加える
private
private
private
private
_context: ComponentFramework.Context<IInputs>;
_container: HTMLDivElement;
targetDivElement: HTMLDivElement;
_display: boolean;
▪ 26行目の init に以下を加える
console.log("call init");
context.mode.trackContainerResize(true);
this._context = context;
this._container = document.createElement("div");
this.targetDivElement = document.createElement("div");
this._container.appendChild(this.targetDivElement);
container.appendChild(this._container);
this._display = this._context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
コンポーネント ロジックの実装(displayの反映) ▪ 52行目の updateView に以下を加える console.log("call updateView"); this.targetDivElement.style.setProperty("height", this._context.mode.allocatedHeight.toString() + "px"); this.targetDivElement.style.setProperty("width", this._context.mode.allocatedWidth.toString() + "px");
コンポーネント ロジックの実装(displayの反映)
現時点の index.ts は以下の通り(コピーしてお使いください)
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class ctl20230624demo implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _context: ComponentFramework.Context<IInputs>;
private _container: HTMLDivElement;
private targetDivElement: HTMLDivElement;
private _display: boolean;
/**
* Empty constructor.
*/
constructor()
{
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to pro perty names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling ' setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement): void
{
console.log("call init");
context.mode.trackContainerResize(true);
this._context = context;
this._container = document.createElement("div");
this.targetDivElement = document.createElement("div");
this._container.appendChild(this.targetDivElement);
container.appendChild(this._container);
this._display = this._context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
// Add control initialization code
}
/**
* Called when any value in the property bag has changed. This includes field values, data -sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to nam es defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void
{
this.targetDivElement.style.setProperty("height", this._context.mode.allocatedHeight.toString() + "px");
this.targetDivElement.style.setProperty("width", this._context.mode.allocatedWidth.toString() + "px");
// Add code to update control view
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs
{
return {};
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void
{
// Add code to cleanup control if necessary
}
}
コンポーネント ロジックの実装(displayの反映) コンポーネントの動作を追加する ▪ ctl20230624demo.css に以下を加える .ns20230624demo.ctl20230624demo .TargetRect { background: rgba(149, 37, 155, 0.5); } .ns20230624demo.ctl20230624demo .TargetRectTransparent { background: rgba(149, 37, 155, 0); }
一旦デバッグ(displayの反映) 先ほど立ち上げた画面が自動で更新されているので確認する ※閉じてしまった場合は、再度 npm start watch
init コンポーネントを初期処理を記述するところ ▪ 主に、描画する HTML 要素の作成や、それらに発生するイベントの追加、初期状態の設定 を行う ▪ context.parameter.xxxxxx.raw!; でコンポーネントに設定したプロパティを取得できる (共通) ▪ プログラム中で、コンポーネントの縦、横サイズを利用したい場合は、以下を記入しなければな らない context.mode.trackContainerResize(true);
コンポーネント ロジックの実装(displayの即時反映) ▪ 55行目の updateView に以下を加える this._display = context.parameters.display.raw!; this.targetDivElement.setAttribute("class", this._display ? "TargetRect" : "TargetRectTransparent" );
コンポーネント ロジックの実装(displayの即時反映)
現時点の index.ts は以下の通り(コピーしてお使いください)
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class ctl20230624demo implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _context: ComponentFramework.Context<IInputs>;
private _container: HTMLDivElement;
private targetDivElement: HTMLDivElement;
private _display: boolean;
private _refreshData: EventListenerOrEventListenerObject;
/**
* Empty constructor.
*/
constructor()
{
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to pro perty names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling ' setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement): void
{
console.log("call init");
context.mode.trackContainerResize(true);
this._context = context;
this._container = document.createElement("div");
this.targetDivElement = document.createElement("div");
this._container.appendChild(this.targetDivElement);
container.appendChild(this._container);
this._display = this._context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
// Add control initialization code
}
/**
* Called when any value in the property bag has changed. This includes field values, data -sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to nam es defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void
{
console.log("call updateView");
this.targetDivElement.style.setProperty("height", this._context.mode.allocatedHeight.toString() + "px");
this.targetDivElement.style.setProperty("width", this._context.mode.allocatedWidth.toString() + "px");
this._display = context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
// Add code to update control view
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs
{
return {};
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void
{
// Add code to cleanup control if necessary
}
}
一旦デバッグ(displayの即時反映) 先ほど立ち上げた画面が自動で更新されているので確認する ※閉じてしまった場合は、再度 npm start watch
updateView コンポーネントを再描画する役割 ▪ 主に、描画する HTML 要素の更新を行う ▪ ユーザから、画面からどちらからのプロパティの変更でも実行される
コンポーネント ロジックの実装(クリック処理の追加)
▪ 8行目に以下を加える
private _refreshData: EventListenerOrEventListenerObject;
private _offsetX: number;
private _offsetY: number;
public refreshData(evt: Event): void {
console.log("call refreshData");
this._offsetX = (evt as MouseEvent).offsetX as number;
this._offsetY = (evt as MouseEvent).offsetY as number;
alert("X: " + this._offsetX + " , " + "Y: " + this._offsetY);
}
▪ 51行目の init に以下を加える
this._refreshData = this.refreshData.bind(this);
this.targetDivElement.addEventListener("click", this._refreshData);
コンポーネント ロジックの実装(クリック処理の追加)
現時点の index.ts は以下の通り(コピーしてお使いください)
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class ctl20230624demo implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _context: ComponentFramework.Context<IInputs>;
private _container: HTMLDivElement;
private targetDivElement: HTMLDivElement;
private _display: boolean;
private _refreshData: EventListenerOrEventListenerObject;
private _offsetX: number;
private _offsetY: number;
public refreshData(evt: Event): void {
console.log("call refreshData");
this._offsetX = (evt as MouseEvent).offsetX as number;
this._offsetY = (evt as MouseEvent).offsetY as number;
alert("X: " + this._offsetX + " , " + "Y: " + this._offsetY);
}
/**
* Empty constructor.
*/
constructor()
{
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to pro perty names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling ' setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement): void
{
console.log("call init");
context.mode.trackContainerResize(true);
this._context = context;
this._container = document.createElement("div");
this.targetDivElement = document.createElement("div");
this._container.appendChild(this.targetDivElement);
container.appendChild(this._container);
this._display = this._context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
this._refreshData = this.refreshData.bind(this);
this.targetDivElement.addEventListener("click", this._refreshData);
// Add control initialization code
}
/**
* Called when any value in the property bag has changed. This includes field values, data -sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to nam es defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void
{
console.log("call updateView");
this.targetDivElement.style.setProperty("height", this._context.mode.allocatedHeight.toString() + "px");
this.targetDivElement.style.setProperty("width", this._context.mode.allocatedWidth.toString() + "px");
this._display = context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
// Add code to update control view
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs
{
return {};
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void
{
// Add code to cleanup control if necessary
}
}
一旦デバッグ(クリック処理の追加) 先ほど立ち上げた画面が自動で更新されているので確認する ※閉じてしまった場合は、再度 npm start watch
コンポーネント ロジックの実装(座標の即時反映) ▪ 11行目に以下を加える private _notifyOutputChanged: () => void; ▪ 17行目の refreshData に以下を加える this._notifyOutputChanged(); ▪ 53行目の init に以下を加える this._notifyOutputChanged = notifyOutputChanged;
コンポーネント ロジックの実装(座標の即時反映)
▪ 83行目の getOutputs に以下を加える
console.log("call getOutputs");
▪ 84行目の getOutputs の return の間に以下を加える
return {
offsetX: this._offsetX,
offsetY: this._offsetY,
display: this._display
};
コンポーネント ロジックの実装(座標の即時反映)
現時点の index.ts は以下の通り(コピーしてお使いください)
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class ctl20230624demo implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _context: ComponentFramework.Context<IInputs>;
private _container: HTMLDivElement;
private targetDivElement: HTMLDivElement;
private _display: boolean;
private _refreshData: EventListenerOrEventListenerObject;
private _offsetX: number;
private _offsetY: number;
private _notifyOutputChanged: () => void;
public refreshData(evt: Event): void {
console.log("call refreshData");
this._offsetX = (evt as MouseEvent).offsetX as number;
this._offsetY = (evt as MouseEvent).offsetY as number;
alert("X: " + this._offsetX + " , " + "Y: " + this._offsetY);
this._notifyOutputChanged();
}
/**
* Empty constructor.
*/
constructor()
{
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to pro perty names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling ' setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement): void
{
console.log("call init");
context.mode.trackContainerResize(true);
this._context = context;
this._container = document.createElement("div");
this.targetDivElement = document.createElement("div");
this._container.appendChild(this.targetDivElement);
container.appendChild(this._container);
this._display = this._context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
this._notifyOutputChanged = notifyOutputChanged;
this._refreshData = this.refreshData.bind(this);
this.targetDivElement.addEventListener("click", this._refreshData);
// Add control initialization code
}
/**
* Called when any value in the property bag has changed. This includes field values, data -sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to nam es defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void
{
console.log("call updateView");
this.targetDivElement.style.setProperty("height", this._context.mode.allocatedHeight.toString() + "px");
this.targetDivElement.style.setProperty("width", this._context.mode.allocatedWidth.toString() + "px");
this._display = context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
// Add code to update control view
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs
{
console.log("call getOutputs");
return {
offsetX: this._offsetX,
offsetY: this._offsetY,
display: this._display
};
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void
{
// Add code to cleanup control if necessary
}
}
一旦デバッグ(座標の即時反映) 先ほど立ち上げた画面が自動で更新されているので確認する ※閉じてしまった場合は、再度 npm start watch
getOutputs コンポーネント内の処理の結果をプロパティに反映させる役割 ▪ 指定したプロパティをコンポーネント側から更新する ▪ 更新したいプロパティを return する ▪ 主に notifyOutputChabged が呼び出されたタイミングで実行されるが、高頻度(10ms 以内)に連続して呼び出された場合は実行されない
コンポーネント ロジックの実装(イベントの削除) ▪ 98行目の destory に以下を加える console.log("call destoroy"); this.targetDivElement.removeEventListener("click", this._refreshData); ▪ 16行目の以下を消す alert("X: " + this._offsetX + " , " + "Y: " + this._offsetY);
コンポーネント ロジックの実装(イベントの削除)
現時点の index.ts は以下の通り(コピーしてお使いください)
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class ctl20230624demo implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _context: ComponentFramework.Context<IInputs>;
private _container: HTMLDivElement;
private targetDivElement: HTMLDivElement;
private _display: boolean;
private _refreshData: EventListenerOrEventListenerObject;
private _offsetX: number;
private _offsetY: number;
private _notifyOutputChanged: () => void;
public refreshData(evt: Event): void {
console.log("call refreshData");
this._offsetX = (evt as MouseEvent).offsetX as number;
this._offsetY = (evt as MouseEvent).offsetY as number;
this._notifyOutputChanged();
}
/**
* Empty constructor.
*/
constructor()
{
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to pro perty names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling ' setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement): void
{
console.log("call init");
context.mode.trackContainerResize(true);
this._context = context;
this._container = document.createElement("div");
this.targetDivElement = document.createElement("div");
this._container.appendChild(this.targetDivElement);
container.appendChild(this._container);
this._display = this._context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
this._notifyOutputChanged = notifyOutputChanged;
this._refreshData = this.refreshData.bind(this);
this.targetDivElement.addEventListener("click", this._refreshData);
// Add control initialization code
}
/**
* Called when any value in the property bag has changed. This includes field values, data -sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to nam es defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void
{
console.log("call updateView");
this.targetDivElement.style.setProperty("height", this._context.mode.allocatedHeight.toString() + "px");
this.targetDivElement.style.setProperty("width", this._context.mode.allocatedWidth.toString() + "px");
this._display = context.parameters.display.raw!;
this.targetDivElement.setAttribute("class",
this._display
? "TargetRect"
: "TargetRectTransparent"
);
// Add code to update control view
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs
{
console.log("call getOutputs");
return {
offsetX: this._offsetX,
offsetY: this._offsetY,
display: this._display
};
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void
{
console.log("call destoroy");
this.targetDivElement.removeEventListener("click", this._refreshData);
// Add code to cleanup control if necessary
}
}
destroy コンポーネントに定義したイベントを削除する役割
リリースしてみよう
リリースしてみよう 以下の3手順を行う ▪ ESLint の設定を追記する ▪ パッケージ化する ▪ 環境にインポートする
ESLint の設定を追記する ビルド時のエラー回避のため、以下の設定を追記する ▪ .eslintrc.jsonを開く ▪ 23行目の rules に以下を加える "no-undef": "off"
パッケージ化する 作成したコンポーネントと同階層に移動し、Solutions フォルダ、solctl20230624demo フォ ルダを作成する 以下コマンドを実行し、ソリューションを作成、コンポーネントをパッケージ化する • cd Solutions/solctl20230624demo • pac solution init --publisher-name demo --publisher-prefix demo • pac solution add-reference --path ../../ • dotnet build
環境にインポートする Solutions/solctl20230624demo/bin/Debug/solctl20230624demo.zip が生成 されているので、それをソリューションの画面からインポートすると、使用可能となる
その他 ▪ コードコンポーネントを使って作ったキャンバスアプリは、キャンバスアプリのエクスポート機能によっ てよその環境に持っていけるがよその環境で再編集はできない ※ただしコンポーネントの性質で不可の場合もあるかも ▪ ソリューションとして、コードコンポーネントを移行すれば、よその環境でも編集可能 ▪ 一度インポートしたソリューションを更新したい場合は、Solution.xml でソリューションのバー ジョンを上げる必要がある
コンポーネントのバージョン指定箇所
ソリューションのバージョン指定箇所
まとめ
まとめ ▪ PCF は JavaScript で開発した独自のコンポーネントを利用するための仕組み、枠組み ▪ キャンバスアプリ、モデル駆動型アプリ、どちらでも対応が可能で、 Power Pages にも一部対 応が可能 ▪ フィールド コンポーネント、データセット コンポーネントの2つがあり、それぞれ、テキスト入力のよ うなもの、ギャラリーやデータテーブルのようなものを開発できる ▪ 作業の過程で、多くのファイルが出来上がるので難しそうに見えるが、実際操作するファイルは 3ファイル程度で安心 ▪ index.ts の中にある init updateView getOutputs destroy に追記するだけで開発が 可能 みなさん、ぜひ使ってみてください!!!!!
参考
参考 Power Apps component frameworkの概要 https://learn.microsoft.com/ja-jp/power-apps/developer/componentframework/overview GitHub Microsoft/PowerApps-Samples https://github.com/microsoft/PowerApps-Samples PCF Gallery https://pcf.gallery/ PowerApps コンポーネントフレームワーク (kenichiro nakamura さんの記事) https://qiita.com/kenakamu/items/5dd4c64b14c453fa7d85