チュートリアル: キャンバス アプリ データセット コンポーネントの作成
このチュートリアルでは、キャンバス アプリのデータセット コード コンポーネントを作成して、展開し、画面へ追加し、Visual Studio Code を使ったコンポーネントをテストします。 このコード コンポーネントは、ページ区切りされたスクロール可能なデータセットのグリッドを表示し、ソートやフィルター処理が可能な列を提供します。 また、インジケーターの列を構成することにより、特定の行を強調表示することもできます。 これはアプリ開発者からの一般的な要望であり、ネイティブのキャンバス アプリ コンポーネントを使用して実装するには複雑となる場合があります。 コード コンポーネントは、キャンバス アプリとモデル駆動型アプリの両方で機能するように記述できます。 ただし、このコンポーネントは、特にキャンバス アプリでの使用を想定して作成されています。
これらの要件にに加えて、コード コンポーネントがベストプラクティスのガイダンスに従っていることも確認する必要があります。
- Microsoft Fluent UI を使用する
- 設計時と実行時の両方でコード コンポーネント ラベルをローカライズします
- コードコンポーネントが親のキャンバス アプリの画面で指定された幅と高さで表示されることを確認してください
- アプリの開発者が入力プロパティやアプリの外部要素を使ってユーザー インターフェースを可能な限りカスタマイズできるように検討してください
注意
開始する前に、すべてのコンポーネントの前提条件を満たしていることを確認してください。
コード
PowerApps-Samples/component-framework/CanvasGridControl/ から完全なサンプルをダウンロードできます。
新しい pcfproj
プロジェクトを作成します
コード コンポーネントで使用する新しいフォルダを作成します。 たとえば、
C:\repos\CanvasGrid
などとします。Visual Studio Code を開き、ファイル > フォルダを開く に移動し、
CanvasGrid
フォルダを選択します。 Visual Studio Code のインストール時に Windows Explorer の拡張機能を追加した場合は、フォルダ内のコンテキスト メニューからコードで開くを使用できます。 また、現在のディレクトリがこの場所に設定されている場合、コマンド プロンプトでcode .
を使って任意のフォルダを Visual Studio Code に読み込むこともできます。新たな Visual Studio Code PowerShell ターミナル (ターミナル > 新規ターミナル) 内で、pac pcf init コマンドを使用して、新しいコードコンポーネント プロジェクトを作成します。
pac pcf init --namespace SampleNamespace --name CanvasGrid --template dataset
または、次の短いフォームを使用します:
pac pcf init -ns SampleNamespace -n CanvasGrid -t dataset
これにより、必要なモジュールを定義した
packages.json
を含む、新しいpcfproj
と関連ファイルが現在のフォルダに追加されます。 必要なモジュールをインストールするには、npm install を使用します:npm install
注意
The term 'npm' is not recognized as the name of a cmdlet, function, script file, or operable program.
というメッセージが表示された場合、前提条件がすべてインストールされているかどうか、特に node.js (LTS 版を推奨) がインストールされているかどうかを確認してください。
テンプレートには、さまざまな構成ファイルとともに、index.ts
ファイルが含まれています。 これは、コード コンポーネントの開始点であり、コンポーネントの実装で説明されているライフサイクル メソッドが含まれています。
Microsoft Fluent UI をインストールする
UI の作成には Microsoft Fluent UI と React を使用するため、これらの依存関係を加味してインストールする必要があります。 ターミナルで以下を使用します:
npm install react react-dom @fluentui/react
これにより、モジュールが packages.json
に追加され、node_modules
フォルダにインストールされます。 必要なモジュールは npm install
を使って復元するため、node_modules
をソース コントロールにコミットする必要はありません。
Microsoft Fluent UI の利点の一つは、一貫性のある アクセシビリティ の高い UI を提供することです。
eslint
を構成する
pac pcf init が使用するテンプレートは eslint
モジュールをプロジェクトにインストールし、.eslintrc.json
ファイルを追加することで構成します。 Eslint
では、TypeScript と React のコーディング スタイルを構成する必要があります。 詳細については、リンティング - コード・コンポーネントのベスト・プラクティスとガイダンスを参照してください。
データセット プロパティを定義する
CanvasGrid\ControlManifest.Input.xml
ファイルは、コード コンポーネントの動作を記述するメタデータを定義します。 コントロール属性には、コンポーネントの名前空間と名前がすでに含まれています。
ヒント
属性が別々の行に表示されるように XMLをフォーマットすると、読みやすくなる場合があります。 Visual Studio Code Marketplace で選択した XML フォーマット ツールを見つけてインストールします: xml フォーマット拡張機能を検索。
以下の例は、読みやすくするために、属性を別々の行にしたフォーマットにしています。
コード コンポーネントがバインドされるレコードを定義する必要があります。これを行うには、既存の data-set
要素の代わりに、control
要素内に以下を追加します:
レコードデータセットは、コードコンポーネントがキャンバス アプリに追加されると、データソースにバインドされます。 プロパティ セットは、ユーザーがそのデータセットの列の 1 つを、行の強調表示インジケーターとして使用するように構成する必要があることを示します。
ヒント
複数のデータセット要素を指定できます。 これは、あるデータセットを検索し、別のデータセットを使ってレコードのリストを表示する場合に便利です。
入力プロパティと出力プロパティの定義
データセットに加えて、以下の入力プロパティを指定できます:
HighlightValue
- アプリ開発者が、HighlightIndicator
property-set
として定義された列と比較する値を指定できます。 この値が等しい場合、その行がハイライト表示されます。HighlightColor
- アプリ開発者がを行の強調に使用する色を指定できるようになります。
ヒント
キャンバス アプリで使用するコード コンポーネントを作成する際には、コード コンポーネントの共通部分のスタイル設定に入力プロパティを提供することをお勧めします。
入力プロパティに加えて、コードコンポーネント内で適用されるフィルター アクションによって行数が変更されると、FilteredRecordCount
という名前の出力プロパティが更新されます (OnChange
イベントがトリガーされます)。 これは、親アプリの内に No Rows Found
のメッセージを表示したいときに便利です。
注意
将来的には、コード コンポーネントがカスタム イベントに対応し、一般的な OnChange
イベントではなく、特定のイベントを定義できるようになります。
この 3 つのプロパティを定義するには、CanvasGrid\ControlManifest.Input.xml
ファイルの data-set
要素の下に以下を追加します:
<property name="FilteredRecordCount"
display-name-key="FilteredRecordCount_Disp"
description-key="FilteredRecordCount_Desc"
of-type="Whole.None"
usage="output" />
<property name="HighlightValue"
display-name-key="HighlightValue_Disp"
description-key="HighlightValue_Desc"
of-type="SingleLine.Text"
usage="input"
required="true"/>
<property name="HighlightColor"
display-name-key="HighlightColor_Disp"
description-key="HighlightColor_Desc"
of-type="SingleLine.Text"
usage="input"
required="true"/>
このファイルを保存し、コマンド ラインで以下を使用します:
npm run build
注意
npm run build
の実行中に次のようなエラーが発生した場合:
[2:48:57 PM] [build] Running ESLint...
[2:48:57 PM] [build] Failed:
[pcf-1065] [Error] ESLint validation error:
C:\repos\CanvasGrid\CanvasGrid\index.ts
2:47 error 'PropertyHelper' is not defined no-undef
index.ts ファイルを開き、次の行のすぐ上に // eslint-disable-next-line no-undef
を追加します。
import DataSetInterfaces = ComponentFramework.PropertyHelper.DataSetApi;
再度、npm run build
を実行します。
コンポーネントがビルドされると、以下が表示されます:
自動生成されたファイル
CanvasGrid\generated\ManifestTypes.d.ts
がプロジェクトに追加されます。 これは、ビルド プロセスの一部としてControlManifest.Input.xml
から生成され、入力/出力プロパティの操作に使用する型を提供します。ビルドの出力が
out
フォルダに追加されます。bundle.js
はブラウザ内で実行されるトランスパイルされた JavaScript、ControlManifest.xml
はデプロイ時に使用されるControlManifest.Input.xml
のファイルを再フォーマットしたものです。注意
generated
、out
フォルダの内容を直接変更しないでください。 これらはビルドの過程で上書きされます。
Grid Fluent UI React コンポーネントを追加する
コード コンポーネントが React を使用している場合、 updateView メソッド内でレンダリングされるルート コンポーネントは 1 つである必要があります。 CanvasGrid
フォルダの中に、 Grid.tsx
という名前の新しい TypeScript ファイルを追加し、以下の内容を追加します:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Overlay } from '@fluentui/react/lib/Overlay';
import {
ScrollablePane,
ScrollbarVisibility
} from '@fluentui/react/lib/ScrollablePane';
import { Stack } from '@fluentui/react/lib/Stack';
import { Sticky } from '@fluentui/react/lib/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/Sticky';
import { IObjectWithKey } from '@fluentui/react/lib/Selection';
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
import * as React from 'react';
type DataSet = ComponentFramework.PropertyHelper.DataSetApi.EntityRecord & IObjectWithKey;
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
}
const onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props, defaultRender) => {
if (props && defaultRender) {
return (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
{defaultRender({
...props,
})}
</Sticky>
);
}
return null;
};
const onRenderItemColumn = (
item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord,
index?: number,
column?: IColumn,
) => {
if (column && column.fieldName && item) {
return <>{item?.getFormattedValue(column.fieldName)}</>;
}
return <></>;
};
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
} = props;
const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);
const items: (DataSet | undefined)[] = React.useMemo(() => {
setIsLoading(false);
const sortedRecords: (DataSet | undefined)[] = sortedRecordIds.map((id) => {
const record = records[id];
return record;
});
return sortedRecords;
}, [records, sortedRecordIds, hasNextPage, setIsLoading]);
const gridColumns = React.useMemo(() => {
return columns
.filter((col) => !col.isHidden && col.order >= 0)
.sort((a, b) => a.order - b.order)
.map((col) => {
const sortOn = sorting && sorting.find((s) => s.name === col.name);
const filtered =
filtering &&
filtering.conditions &&
filtering.conditions.find((f) => f.attributeName == col.name);
return {
key: col.name,
name: col.displayName,
fieldName: col.name,
isSorted: sortOn != null,
isSortedDescending: sortOn?.sortDirection === 1,
isResizable: true,
isFiltered: filtered != null,
data: col,
} as IColumn;
});
}, [columns, sorting]);
const rootContainerStyle: React.CSSProperties = React.useMemo(() => {
return {
height: height,
width: width,
};
}, [width, height]);
return (
<Stack verticalFill grow style={rootContainerStyle}>
<Stack.Item grow style={{ position: 'relative', backgroundColor: 'white' }}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
></DetailsList>
</ScrollablePane>
{(itemsLoading || isComponentLoading) && <Overlay />}
</Stack.Item>
</Stack>
);
});
Grid.displayName = 'Grid';
注意
このファイルの拡張子 tsx
は、React で使われる XML スタイルの構文に対応する TypeScript ファイルです。 ビルド プロセスによって標準の JavaScript にコンパイルされます。
グリッド設計メモ
このセクションには、Grid.tsx
コンポーネントの設計に関する一般情報が含まれます。
機能コンポーネントです
これは React の機能コンポーネントですが、同様にクラス コンポーネントである可能性もあります。 これは、好みのコーディング スタイルに基づくものです。 クラス コンポーネントと機能コンポーネントを同じプロジェクトに混在させることもできます。 関数コンポーネントとクラス コンポーネントは、Reactで使用されている tsx
XML スタイルの構文を使用します。 詳細: 機能コンポーネントとクラス コンポーネント
bundle.js のサイズを最小化する
以下の代わりに、パスベースのインポートを使用して ChoiceGroup
Fluent UI コンポーネントをインポートする場合:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
Stack
} from "@fluentui/react";
このコードは次を使用します:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Stack } from '@fluentui/react/lib/Stack';
これにより、バンドルサイズが小さくなり、必要な容量が少なくなり、ランタイムパフォーマンスが向上します。
別の方法としては、ツリー シェイクがあります。
destructuring の割り当て
このコード:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
} = props;
destructuring 割り当て を使用します。 このように、レンダリングに必要な属性をプロップから抽出し、使用するたびに props.
を前置するのではなく、その属性を使用します。
コードは、React.memo を使用して機能コンポーネントをラップし、入力プロップが変更されない限りレンダリングされないようにします。
React.useMemo の使用
React.useMemo は、入力されたプロップス options
または configuration
が変更されたときにのみ、作成されたアイテム配列が更されるようにする目的で使用します。 これは関数コンポーネントのベスト プラクティスであり、子コンポーネントの不要なレンダリングを減らすことができます。
その他の注意事項:
Stack
の中のDetailsList
がラップされているのは、後でページング コントロールのあるフッター要素を追加する目的があるためです。- Fluent UI
Sticky
コンポーネントは、グリッドをスクロールしてもヘッダー列が表示されたままになるように (onRenderDetailsHeader
を使用して)、ヘッダー列をラップする目的で使用されます。 setKey
はinitialFocusedIndex
と共にDetailsList
に渡され、現在のページが変わったときに、スクロール位置と選択がリセットされるようになっています。- 関数
onRenderItemColumn
は、セルのコンテンツをレンダリングするために使用されます。 これは、行項目を受け取り、列の表示値を返すために、getFormattedValue を使用します。 getValue メソッドは、代替のレンダリングを提供するために使用できる値を返します。getFormattedValue
の利点は、日付やルックアップなどの非文字列型のカラムに対して、フォーマットされた文字列が含まれていることです。 gridColumns
ブロックは、データセットコンテキストで提供されたカラムのオブジェクト形状を、DetailsList
列のプロップで想定される形状にマッピングしています。このブロックは React.useMemo フックでラップされているため、columns
やsorting
のプロップが変更されたときにのみ出力が変更されます。 コード コンポーネントのコンテキストで提供されているソートやフィルタの詳細が、マッピングされている列と一致する列に、ソートやフィルターのアイコンを表示できます。column.order
プロパティを使用して列がソートされ、アプリの開発者が定義したグリッド上の正しい順序で表示されます。- React コンポーネントでは、
isComponentLoading
の内部状態を維持しています。 これは、ユーザーがソートやフィルターのアクションを選択すると、sortedRecordIds
が更新されて状態がリセットされるまで、視覚的な手がかりとしてグリッドをグレーにすることができるためです。itemsLoading
という追加の入力プロップは、データセット コンテキストが提供する dataset.loading プロパティにマッピングされています。 これらのフラグは、Fluent UIOverlay
コンポーネントを使用して実装された視覚的な読み込みキューを制御する目的で使用されます。
index.ts の更新
次のステップは、index.ts
ファイルに変更を加えて、Grid.tsx.
で定義されたプロパティと一致するようにすることです。
import ステートメントを追加してアイコンを初期化する
index.ts
のヘッダーに、既存のインポートを以下のように置き換えます:
import {IInputs, IOutputs} from './generated/ManifestTypes';
import DataSetInterfaces = ComponentFramework.PropertyHelper.DataSetApi;
type DataSet = ComponentFramework.PropertyTypes.DataSet;
注意
このコードは Fluent UI のアイコンセットを使用しているため、initializeIcons
のインポートが必要となります。 テスト ハーネス内でアイコンを読み込むには、initializeIcons
を呼び出す必要があります。 これらはキャンバス アプリの内部ですでに初期化されています。
CanvasGrid クラスにフィールドを追加する
CanvasGrid
クラスの下に以下のクラス フィールドを追加します:
export class CanvasGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {
/**
* Empty constructor.
*/
constructor() {
}
init メソッドを更新する
以下を init
に追加します:
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement): void {
// Add control initialization code
}
init
関数は、アプリ画面でコード コンポーネントが初期化される際に最初に呼び出されます。 以下への参照を保存します:
notifyOutputChanged
: これは、プロパティのひとつが変更されたことをキャンバス アプリに通知する目的で呼び出されるコールバックです。container
: これは、コード コンポーネントの UI を追加する DOM 要素です。resources
: 現在のユーザーの言語でローカライズされた文字列を取得するために使用されます。
context.mode.trackContainerResize(true)) は、コード コンポーネントのサイズが変更されたときに updateView
を呼び出す目的で使用します。
注意
現在のところ、コード コンポーネントがテスト ハーネス内で実行されているかどうかを判断する方法はありません。 control-dimensions
div
がインジケーターとして存在するかどうかを検出する必要があります。
updateView メソッドを更新します
以下を updateView
に追加します:
public updateView(context: ComponentFramework.Context<IInputs>): void {
// Add code to update control view
}
以下が表示されます:
init
関数の中で受け取った DOM コンテナへの参照を渡して React.createElement を呼び出します。Grid
コンポーネントはGrid.tsx
内に定義され、ファイルの先頭にインポートされます。init
関数内で trackContainerResize(true) を呼び出しているため、allocatedWidth
とallocatedHeight
は、それらが変更されるたびに (たとえば、アプリがコードコンポーネントのサイズを変更したり、フルスクリーンモードになったりした場合)、親コンテキストから提供されます。- updatedProperties の配列に
dataset
という文字列が含まれている場合は、表示する新しい行があることを検出できます。 - テスト ハーネスでは、
updatedProperties
配列は入力されていないので、init
関数で設定したisTestHarness
フラグを使って、sortedRecordId
とrecords
を設定するロジックをショートさせます。 データの再レンダリングが必要な場合を除いて、子コンポーネントに渡される際にこれらを変更させないように、変更されるまで現在の値への参照を維持します。 - コード コンポーネントは、どのページを表示しているかという状態を維持しているため、親コンテキストがレコードを最初のページにリセットすると、ページ番号がリセットされます。
hasPreviousPage
が false の場合に 1 ページ目に戻ってくるとわかります。
破壊メソッドを更新します
最後に、コードコンポーネントが破棄されたときに整理する必要があります:
テスト ハーネスを開始します
すべてのファイルが保存され、端末で使用されていることを確認します:
npm start watch
サンプルの 3 つのレコードを使って入力されたコード コンポーネントのグリッドを表示するには、幅と高さを設定する必要があります。 続いて、レコードのセットを Dataverse から CSV ファイルにエクスポートし、データ入力 > レコード パネル を使用してテスト ハーネスに読み込むことができます:
以下は、.csv ファイルに保存して使用できるカンマ区切りのサンプル データです:
address1_city,address1_country,address1_stateorprovince,address1_line1,address1_postalcode,telephone1,emailaddress1,firstname,fullname,jobtitle,lastname
Seattle,U.S.,WA,7842 Ygnacio Valley Road,12150,555-0112,someone_m@example.com,Thomas,Thomas Andersen (sample),Purchasing Manager,Andersen (sample)
Renton,U.S.,WA,7165 Brock Lane,61795,555-0109,someone_j@example.com,Jim,Jim Glynn (sample),Owner,Glynn (sample)
Snohomish,U.S.,WA,7230 Berrellesa Street,78800,555-0106,someone_g@example.com,Robert,Robert Lyon (sample),Owner,Lyon (sample)
Seattle,U.S.,WA,931 Corte De Luna,79465,555-0111,someone_l@example.com,Susan,Susan Burk (sample),Owner,Burk (sample)
Seattle,U.S.,WA,7765 Sunsine Drive,11910,555-0110,someone_k@example.com,Patrick,Patrick Sands (sample),Owner,Sands (sample)
Seattle,U.S.,WA,4948 West Th St,73683,555-0108,someone_i@example.com,Rene,Rene Valdes (sample),Purchasing Assistant,Valdes (sample)
Redmond,U.S.,WA,7723 Firestone Drive,32147,555-0107,someone_h@example.com,Paul,Paul Cannon (sample),Purchasing Assistant,Cannon (sample)
Issaquah,U.S.,WA,989 Caravelle Ct,33597,555-0105,someone_f@example.com,Scott,Scott Konersmann (sample),Purchasing Manager,Konersmann (sample)
Issaquah,U.S.,WA,7691 Benedict Ct.,57065,555-0104,someone_e@example.com,Sidney,Sidney Higa (sample),Owner,Higa (sample)
Monroe,U.S.,WA,3747 Likins Avenue,37925,555-0103,someone_d@example.com,Maria,Maria Campbell (sample),Purchasing Manager,Campbell (sample)
Duvall,U.S.,WA,5086 Nottingham Place,16982,555-0102,someone_c@example.com,Nancy,Nancy Anderson (sample),Purchasing Assistant,Anderson (sample)
Issaquah,U.S.,WA,5979 El Pueblo,23382,555-0101,someone_b@example.com,Susanna,Susanna Stubberod (sample),Purchasing Manager,Stubberod (sample)
Redmond,U.S.,WA,249 Alexander Pl.,86372,555-0100,someone_a@example.com,Yvonne,Yvonne McKay (sample),Purchasing Manager,McKay (sample)
注意
読み込んだ CSV ファイルの列に関わらず、テスト ハーネスには 1 つの列しか表示されません。 これは、テスト ハーネスが定義されたものが 1 つの場合には property-set
しか表示しないためです。 property-set
が定義されていない場合は、読み込んだ CSV ファイルのすべての列に入力されます。
行選択を追加
Fluent UI の DetailsList
では既定でレコードの選択が可能ですが、選択されたレコードはコード コンポーネントの出力にはリンクされません。 Selected
と SelectedItems
のプロパティは、キャンバス アプリ内で選択されたレコードを反映し、関連するコンポーネントが更新されるようにする必要があります。 この例では、一度にひとつの項目のみを選択できるように設定しているため、SelectedItems
にはひとつのレコードしか入りません。
Grid.tsx インポートの更新
Grid.tsx
内のインポートに以下を追加します:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Overlay } from '@fluentui/react/lib/Overlay';
import {
ScrollablePane,
ScrollbarVisibility
} from '@fluentui/react/lib/ScrollablePane';
import { Stack } from '@fluentui/react/lib/Stack';
import { Sticky } from '@fluentui/react/lib/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/Sticky';
import { IObjectWithKey } from '@fluentui/react/lib/Selection';
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
import * as React from 'react';
setSelectedRecords を GridProps に追加します
Grid.tsx
内の GridProps
インターフェースに、以下を追加します:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
}
setSelectedRecords プロパティをグリッドに追加します
Grid.tsx
関数コンポーネントの中で、props
の destructuring を更新し、新しいプロップ setSelectedRecords
を追加します。
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
} = props;
そのすぐ下に、次を追加します:
const forceUpdate = useForceUpdate();
const onSelectionChanged = React.useCallback(() => {
const items = selection.getItems() as DataSet[];
const selected = selection.getSelectedIndices().map((index: number) => {
const item: DataSet | undefined = items[index];
return item && items[index].getRecordId();
});
setSelectedRecords(selected);
forceUpdate();
}, [forceUpdate]);
const selection: Selection = useConst(() => {
return new Selection({
selectionMode: SelectionMode.single,
onSelectionChanged: onSelectionChanged,
});
});
React.useCallback と useConst フックは、これらの値がレンダリング間で変動せず、不要な子コンポーネントのレンダリングを引き起こすことがないようにする目的で使用されます。
useForceUpdate フックは、選択が更新されたときに、更新された選択数を反映してコンポーネントが再レンダリングされるようにする目的で使用されます。
選択範囲を DetailsList に追加
そして、選択状態を保持するために作成された selection
オブジェクトは、DetailsList
コンポーネントに渡されます:
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
></DetailsList>
setSelectedRecords コールバックを定義する
index.ts
の中に新しい setSelectedRecords
のコールバックを定義し、それを Grid
のコンポーネントに渡す必要があります。 CanvasGrid
クラスの最上位近く、以下を追加します:
export class CanvasGrid
implements ComponentFramework.StandardControl<IInputs, IOutputs>
{
notifyOutputChanged: () => void;
container: HTMLDivElement;
context: ComponentFramework.Context<IInputs>;
sortedRecordsIds: string[] = [];
resources: ComponentFramework.Resources;
isTestHarness: boolean;
records: {
[id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
};
currentPage = 1;
filteredRecordCount?: number;
注意
このメソッドは、コード コンポーネントの現在の this
インスタンスに結合するため、矢印関数として定義されています。
setSelectedRecordIds の呼び出しは、キャンバス アプリに選択が変更されたことを通知し、SelectedItems
と Selected
を参照する他のコンポーネントが更新されるようにします。
新しいコールバックを入力 props に追加します
最後に、Grid
コンポーネントの updateView
メソッドの入力プロップに、新しいコール バックを追加します:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
}),
this.container
);
OnSelect
イベントの呼び出し
キャンバス アプリでは、ギャラリーやグリッドでアイテムの選択が行われると (例: シェブロン アイコンの選択)、OnSelect
イベントが発生するというパターンがあります。 このパターンは、データセットの openDatasetItem メソッドを使って実装できます。
onNavigate を GridProps インターフェイスに追加
先ほどと同様に、Grid
コンポーネントにコールバック プロップを追加するには、Grid.tsx
の中の GridProps
インターフェイスに次のように追加します:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<
string,
ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
}
onNavigate を Grid プロップを追加
ここでも、新しいプロップをプロップの destructuring に追加する必要があります:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
} = props;
onItemInvoked を DetailsList に追加
DetailList
は onItemInvoked
というコールバック プロップを備えており、これにコールバックを渡します:
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
></DetailsList>
index.ts に onNavigate メソッドを追加
setSelectedRecords
方式の直下の index.ts
に onNavigate
方式を追加する:
onNavigate = (
item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
): void => {
if (item) {
this.context.parameters.records.openDatasetItem(item.getNamedReference());
}
};
これは単にデータセットレコードの openDatasetItem
メソッドを呼び出し、コード コンポーネントが OnSelect
イベントを発生させるものです。 このメソッドは、コード コンポーネントの現在の this
インスタンスに結合するため、矢印関数として定義されています。
このコールバックを updateView
メソッド内の Grid
コンポーネントのプロップに渡す必要があります:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
}),
this.container
);
すべてのファイルを保存すると、テスト ハーネスがリロードされます。 Ctrl
+ Shift
+ I
(または F12
) を使用し、ファイルを開く (Ctrl
+ P
) を使用して index.ts
を検索した場合、onNavigate
メソッド内にブレークポイントを置くことができます。 行をダブルクリック (またはカーソル キーでハイライトして Enter
を押す) と、DetailsList
が onNavigate
のコールバックを呼び出すため、ブレークポイントがヒットします。
_this
への参照があるのは、この関数が矢印関数として定義されており、this
のインスタンスをキャプチャするために JavaScript のクロージャにトランスパイルされているためです。
ローカライズを追加する
先に進む前に、リソース文字列をコード コンポーネントに追加して、ページング、ソート、フィルターなどのメッセージにローカライズされた文字列を使用できるようにする必要があります。 新しいファイル CanvasGrid\strings\CanvasGrid.1033.resx
を追加し、 Visual Studio リソース エディタまたは拡張子付きの Visual Studio Code を使用して次のように入力します。
件名 | 価値 |
---|---|
Records_Dataset_Display |
レコード |
FilteredRecordCount_Disp |
フィルターされたレコードの件数 |
FilteredRecordCount_Desc |
フィルター処理後のレコード数 |
HighlightValue_Disp |
ハイライト値 |
HighlightValue_Desc |
行を示す値を強調表示する必要があります |
HighlightColor_Disp |
蛍光ペンの色 |
HighlightColor_Desc |
使用して行を強調表示する色 |
HighlightIndicator_Disp |
インジケータ フィールドを強調表示する |
HighlightIndicator_Desc |
ハイライト値と比較するフィールドの名前に設定します |
Label_Grid_Footer |
ページ {0} ({1} が選択されました) |
Label_SortAZ |
A から Z |
Label_SortZA |
降順 |
Label_DoesNotContainData |
データを含まない |
Label_ShowFullScreen |
全画面表示 |
ヒント
resx
ファイルを直接編集することはお勧めしません。 代わりに、Visual Studio リソース エディタ、または Visual Studio Code の拡張機能のどちらかを使用できます。 Visual Studio Code 拡張を検索: resx エディターの Visual Studio マーケットプレイスを検索
このファイルのデータは、CanvasGrid.1033.resx
ファイルをメモ帳で開き、以下の XML コンテンツをコピーすることによっても設定できます。
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Records_Dataset_Display" xml:space="preserve">
<value>Records</value>
</data>
<data name="FilteredRecordCount_Disp" xml:space="preserve">
<value>Filtered Record Count</value>
</data>
<data name="FilteredRecordCount_Desc" xml:space="preserve">
<value>The number of records after filtering</value>
</data>
<data name="HighlightValue_Disp" xml:space="preserve">
<value>Highlight Value</value>
</data>
<data name="HighlightValue_Desc" xml:space="preserve">
<value>The value to indicate a row should be highlighted</value>
</data>
<data name="HighlightColor_Disp" xml:space="preserve">
<value>Highlight Color</value>
</data>
<data name="HighlightColor_Desc" xml:space="preserve">
<value>The color to highlight a row using</value>
</data>
<data name="HighlightIndicator_Disp" xml:space="preserve">
<value>Highlight Indicator Field</value>
</data>
<data name="HighlightIndicator_Desc" xml:space="preserve">
<value>Set to the name of the field to compare against the Highlight Value</value>
</data>
<data name="Label_Grid_Footer" xml:space="preserve">
<value>Page {0} ({1} Selected)</value>
</data>
<data name="Label_SortAZ" xml:space="preserve">
<value>A to Z</value>
</data>
<data name="Label_SortZA" xml:space="preserve">
<value>Z to A</value>
</data>
<data name="Label_DoesNotContainData" xml:space="preserve">
<value>Does not contain data</value>
</data>
<data name="Label_ShowFullScreen" xml:space="preserve">
<value>Show Full Screen</value>
</data>
</root>
input
/output
プロパティと dataset
および関連する property-set
のリソース文字列があります。 これらは、開発者のブラウザ言語に基づいて、デザイン時に Power Apps Studio で使用されます。 getString を使ってランタイムに取得できるラベル文字列を追加することもできます。 詳しくは、ローカリゼーション API コンポーネントの実装を参照してください。
この新しいリソース ファイルは、resources
要素内の ControlManifest.Input.xml
ファイルに追加する必要があります:
列の並べ替えとフィルターを追加する
グリッドの列ヘッダーを使ってソートやフィルターできるようにする場合、Fluent UI DetailList
は列ヘッダにコンテキスト メニューを簡単に追加する方法を提供しています。
GridProps に onSort と onFilter を追加する
まず、ソートとフィルターのためのコールバック関数を提供するために、Grid.tsx
の中の GridProps
インターフェースに onSort
と onFilter
を追加します:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<
string,
ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
onNavigate: (
item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
) => void;
}
プロップに onSort、onFilter、およびリソースを追加する
続いて、これらの新しいプロップと resources
の参照を (ソートやフィルターをする目的でローカライズされたラベルを取得できるように)、プロップスのデストラクションに追加します:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
} = props;
ContextualMenu コンポーネントをインポートする
Fluent UI が提供する ContextualMenu
コンポーネントを使用できるように、 Grid.tsx
の先頭にいくつかのインポートを追加する必要があります。 パス ベースのインポートを使うことで、バンドルのサイズを小さくすることができます。
import { ContextualMenu, DirectionalHint, IContextualMenuProps } from '@fluentui/react/lib/ContextualMenu';
コンテキスト メニューのレンダリング機能を追加する
今度は、行のすぐ下の Grid.tsx
にコンテキスト メニュー レンダリング機能を追加します
const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);
:
以下が表示されます:
contextualMenuProps
の状態は、Fluent UI のContextualMenu
コンポーネントを使ってレンダリングされるコンテキスト メニューの可視性を制御する目的で使用されます。- このコードは、フィールドにデータが含まれない値のみを表示するシンプルなフィルタを提供しています。 これを拡張して、追加のフィルタリングを提供できます。
- このコードは、ローカライズ可能なコンテキスト メニューのラベルを表示する目的で
resources.getString
を使用しています。 React.useCallback
フックは (React.useMemo
と同様)、依存する値が変化したときにのみコールバックが変更されるようになっています。 これにより、子コンポーネントのレンダリングが最適化されます。
これらの新しいコンテキストメニュー関数を、列選択とコンテキスト メニューのイベントに追加します
これらの新しいコンテキストメニュー関数を、列選択とコンテキスト メニューのイベントに追加します。 const gridColumns
を更新し、onColumnContextMenu
と onColumnClick
のコールバックを追加します:
const gridColumns = React.useMemo(() => {
return columns
.filter((col) => !col.isHidden && col.order >= 0)
.sort((a, b) => a.order - b.order)
.map((col) => {
const sortOn = sorting && sorting.find((s) => s.name === col.name);
const filtered =
filtering &&
filtering.conditions &&
filtering.conditions.find((f) => f.attributeName == col.name);
return {
key: col.name,
name: col.displayName,
fieldName: col.name,
isSorted: sortOn != null,
isSortedDescending: sortOn?.sortDirection === 1,
isResizable: true,
isFiltered: filtered != null,
data: col,
} as IColumn;
});
}, [columns, sorting]);
レンダリングされた出力にコンテキスト メニューを追加する
コンテキスト メニューを表示するには、レンダリングされた出力に追加する必要があります。 返された出力の DetailsList
コンポーネントの直下に以下を追加します:
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
onItemInvoked={onNavigate}
></DetailsList>
</ScrollablePane>
onSort および OnFilter 関数を追加する
並べ替えとフィルタリングの UI を追加したため、コード コンポーネントにバインドされたレコードに対して実際に並べ替えとフィルタリングを実行するために、コールバックを index.ts
に追加する必要があります。 onNavigate
関数の直下の index.ts
に以下を追加します:
onSort = (name: string, desc: boolean): void => {
const sorting = this.context.parameters.records.sorting;
while (sorting.length > 0) {
sorting.pop();
}
this.context.parameters.records.sorting.push({
name: name,
sortDirection: desc ? 1 : 0,
});
this.context.parameters.records.refresh();
};
onFilter = (name: string, filter: boolean): void => {
const filtering = this.context.parameters.records.filtering;
if (filter) {
filtering.setFilter({
conditions: [
{
attributeName: name,
conditionOperator: 12, // Does not contain Data
},
],
} as ComponentFramework.PropertyHelper.DataSetApi.FilterExpression);
} else {
filtering.clearFilter();
}
this.context.parameters.records.refresh();
};
以下が表示されます:
- 並べ替えとフィルターは、並べ替え と フィルター処理 を使ってデータセットのプロパティに適用されます。
- 並べ替え列を変更する際には、並べ替え配列自体を置き換えるのではなく、既存の並べ替え定義をポップで削除する必要があります。
- 更新は、ソートやフィルターが適用された後に呼び出す必要があります。 フィルターと並べ替えが同時に適用された場合、更新は一度で済みます。
OnSort および OnFilter コールバックをグリッド レンダリングに追加する
最後に、この 2 つのコールバックを Grid
のレンダリング呼び出しに渡します:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
onNavigate: this.onNavigate,
}),
this.container
);
注意
この時点では、テスト ハーネスは並べ替えやフィルターに対応していないため、テストを行うことはできません。 その後、pac pcf push を使ってデプロイし、キャンバス アプリに追加してテストできます。 必要であれば、このステップをスキップして、コード コンポーネントがキャンバス アプリ内でどのように見えるかを確認することができます。
FilteredRecordCount 出力プロパティを更新する
グリッドは内部でレコードをフィルターできるようになったため、表示されたレコードの数をキャンバス アプリに報告することが重要です。 これは、「レコードなし」などのメッセージを表示させるためです。
ヒント
これをコードコンポーネント内で実装することも可能ですが、アプリ開発者の自由度を高めるためにも、ユーザーインターフェースはできるだけキャンバス アプリに任せることをお勧めします。
ControlManifest.Input.xml
の中に FilteredRecordCount
という出力プロパティが既に定義されています。 フィルター処理が行われ、フィルターされたレコードが読み込まれると、文字列 updatedProperties を dataset
配列に入れて updateView
関数が呼び出されます。 レコード数が変更された場合は、notifyOutputChanged
を呼び出して、FilteredRecordCount
プロパティを使用するコントロールを更新しする必要があることをキャンバス アプリが認識できるようにする必要があります。 index.ts
の updateView
方式の内部、ReactDOM.render
のすぐ上、allocatedHeight
の下に次を追加します:
FilteredRecordCount を getOutputs に追加する
これは、先に定義したコード コンポーネント クラスの filteredRecordCount
が、受信した新しいデータと異なる場合に更新されます。 notifyOutputChanged
が呼び出された後、getOutputs
が呼ばれたときに値が返されるようにする必要があるため、getOutputs
メソッドを更新します:
グリッドにページングを追加する
大規模なデータセットの場合、キャンバス アプリはレコードを複数のページに分割します。 ページ ナビゲーション コントロールを表示するフッターを追加できます。 各ボタンは Fluent UI IconButton
を使ってレンダリングされるため、それをインポートする必要があります。
IconButton を追加してインポートにする
Grid.tsx
内のインポートにこれを追加します:
import { IconButton } from '@fluentui/react/lib/Button';
stringFormat 関数を追加する
次のステップを実行して、簡単な関数 stringFormat
を使って、ページ インジケータ ラベルのフォーマットをリソース文字列 ("Page {0} ({1} Selected)"
) から読み込む機能を追加します。 この関数は、別のファイルに均等にして、コンポーネント間で共有することができます:
このチュートリアルでは、Grid.tsx
の上部、type DataSet ...
のすぐ下に追加します。
function stringFormat(template: string, ...args: string[]): string {
for (const k in args) {
template = template.replace("{" + k + "}", args[k]);
}
return template;
}
ページング ボタンを追加する
Grid.tsx
には、ScrollablePane
を含む既存の Stack.Item
の下に次の Stack.Item
を追加します:
return (
<Stack verticalFill grow style={rootContainerStyle}>
<Stack.Item grow style={{ position: 'relative', backgroundColor: 'white' }}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
onItemInvoked={onNavigate}
></DetailsList>
{contextualMenuProps && <ContextualMenu {...contextualMenuProps} />}
</ScrollablePane>
{(itemsLoading || isComponentLoading) && <Overlay />}
</Stack.Item>
</Stack>
);
以下が表示されます:
Stack
はフッターがDetailsList
の下に重なるようにします。grow
属性は、グリッドが使用可能なスペースを埋めるように拡張する目的で使用されます。- 前のステップで追加した
stringFormat
関数を使って、リソース文字列 ("Page {0} ({1} Selected)"
) とフォーマットからページ インジケータ ラベルのフォーマットを読み込みます。 - ページングの
IconButtons
にアクセシビリティに使用するalt
テキストを提供することができます。 - フッターのスタイルは、コード コンポーネントに追加された CSS ファイルを参照する CSS クラス名を使っても同様に適用できます。
ページングをサポートするコールバック プロップを追加する
続いて、不足している loadFirstPage
、loadNextPage
、loadPreviousPage
のコールバック プロップを追加する必要があります。
GridProps
インターフェースに以下を追加します:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
onSort: (name: string, desc: boolean) => void;
onFilter: (name: string, filtered: boolean) => void;
}
新しいページングプロップをグリッドに追加する
これらの新しいプロップを、プロップの destructuring に追加します:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
onSort,
onFilter,
resources,
} = props;
index.ts にコールバックを追加する
これらのコールバックを、onFilter
のメソッド配下の index.ts
に追加します:
loadFirstPage = (): void => {
this.currentPage = 1;
this.context.parameters.records.paging.loadExactPage(1);
};
loadNextPage = (): void => {
this.currentPage++;
this.context.parameters.records.paging.loadExactPage(this.currentPage);
};
loadPreviousPage = (): void => {
this.currentPage--;
this.context.parameters.records.paging.loadExactPage(this.currentPage);
};
次に、 Grid
レンダリング呼び出しを更新して、これらのコールバックを含めます。
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
onNavigate: this.onNavigate,
onSort: this.onSort,
onFilter: this.onFilter,
}),
this.container
);
全画面表示サポートを追加する
コード コンポーネントは、フルスクリーン モードで表示する機能を提供します。 これは、画面サイズが小さい場合や、キャンバス アプリの画面内にコードコ ンポーネントのスペースが限られている場合に特に有効です。
Fluent UI Link コンポーネントのインポート
全画面表示モードを起動するには、Fluent UI Link
コンポーネントを使用できます。 Grid.tsx
の上部にあるインポートに追加します。
import { Link } from '@fluentui/react/lib/Link';
リンク コントロールを追加する
フル スクリーンのリンクを追加するには、ページングコントロールを含む既存の Stack
に以下を追加します。
注意
これをネストされた Stack
に必ず追加してください。ルート Stack
には追加しないでください。
<Stack horizontal style={{ width: '100%', paddingLeft: 8, paddingRight: 8 }}>
<IconButton
alt="First Page"
iconProps={{ iconName: 'Rewind' }}
disabled={!hasPreviousPage}
onClick={loadFirstPage}
/>
<IconButton
alt="Previous Page"
iconProps={{ iconName: 'Previous' }}
disabled={!hasPreviousPage}
onClick={loadPreviousPage}
/>
<Stack.Item align="center">
{stringFormat(
resources.getString('Label_Grid_Footer'),
currentPage.toString(),
selection.getSelectedCount().toString(),
)}
</Stack.Item>
<IconButton
alt="Next Page"
iconProps={{ iconName: 'Next' }}
disabled={!hasNextPage}
onClick={loadNextPage}
/>
</Stack>
以下が表示されます:
- このコードは、ローカライズに対応するラベルを表示するリソースを使用します。
- フルスクリーン モードが開いている場合、リンクは表示されません。 その代わりに、親アプリのコンテキストが閉じるアイコンを自動的にレンダリングします。
全画面表示をサポートするプロップを GridProps に追加する
ソートとフィルターのためのコールバック関数を提供するために、Grid.tsx
の中の GridProps
インターフェースに onFullScreen
と isFullScreen
を追加します:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
onSort: (name: string, desc: boolean) => void;
onFilter: (name: string, filtered: boolean) => void;
loadFirstPage: () => void;
loadNextPage: () => void;
loadPreviousPage: () => void;
}
全画面表示をサポートするプロップをグリッドに追加する
これらの新しいプロップを、プロップの destructuring に追加します:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
onSort,
onFilter,
resources,
loadFirstPage,
loadNextPage,
loadPreviousPage,
} = props;
index.ts を更新して、全画面表示をグリッドに対してサポートする
これらの新しいプロップを提供するために、index.ts
内部に、loadPreviousPage
下にコールバック メソッドを追加します:
onFullScreen = (): void => {
this.context.mode.setFullScreen(true);
};
setFullScreen への呼び出しにより、コード コンポーネントは全画面表示モードを開き、init
メソッドで trackContainerResize(true)
への呼び出しにより、それに応じて allocatedHeight
と allocatedWidth
を調整します。 全画面表示モードが開かれると、updateView
が呼び出され、コンポーネントのレンダリングを新しいサイズに更新することができます。 updatedProperties
には、発生しているトランジションに応じて fullscreen_open
または fullscreen_close
が入ります。
フルスクリーン モードの状態を保存するには、index.ts
の内側の CanvasGrid
クラスに新しい isFullScreen
フィールドを追加します:
export class CanvasGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {
notifyOutputChanged: () => void;
container: HTMLDivElement;
context: ComponentFramework.Context<IInputs>;
sortedRecordsIds: string[] = [];
resources: ComponentFramework.Resources;
isTestHarness: boolean;
records: {
[id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
};
currentPage = 1;
filteredRecordCount?: number;
updateView を編集して状態を追跡する
updateView
メソッドに、以下を追加して、状態を追跡します:
public updateView(context: ComponentFramework.Context<IInputs>): void {
const dataset = context.parameters.records;
const paging = context.parameters.records.paging;
const datasetChanged = context.updatedProperties.indexOf("dataset") > -1;
const resetPaging =
datasetChanged &&
!dataset.loading &&
!dataset.paging.hasPreviousPage &&
this.currentPage !== 1;
if (resetPaging) {
this.currentPage = 1;
}
コールバックと isFullScreen フィールドを渡してグリッドにレンダリングします
以上で、コールバックと isFullScreen
のフィールドを Grid
のレンダリング プロップに渡すことができます:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
onNavigate: this.onNavigate,
onSort: this.onSort,
onFilter: this.onFilter,
loadFirstPage: this.loadFirstPage,
loadNextPage: this.loadNextPage,
loadPreviousPage: this.loadPreviousPage,
}),
this.container
);
行の強調表示
以上で、条件付きの行ハイライト機能を追加する準備が完了です。 すでに HighlightValue
と HighlightColor
の入力プロパティと、HighlightIndicator
property-set
の入力プロパティ定義が完了しています。 property-set
では、開発者が HighlightValue
で提供する値と比較するために、使用するフィールドを指名することができます。
強調表示をサポートするインポート タイプ
DetailsList
でのカスタム行レンダリングには、追加のインポートが必要です。 @fluentui/react/lib/DetailsList
からのタイプがすでにいくつかあるため、IDetailsListProps
、IDetailsRowStyles
および DetailsRow
を Grid.tsx
のそのインポート ステートメントに追加します:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps
} from '@fluentui/react/lib/DetailsList';
続いて、const rootContainerStyle
ブロックの直下に以下を追加して、カスタム行のレンダラーを作成します:
const onRenderRow: IDetailsListProps['onRenderRow'] = (props) => {
const customStyles: Partial<IDetailsRowStyles> = {};
if (props && props.item) {
const item = props.item as DataSet | undefined;
if (highlightColor && highlightValue && item?.getValue('HighlightIndicator') == highlightValue) {
customStyles.root = { backgroundColor: highlightColor };
}
return <DetailsRow {...props} styles={customStyles} />;
}
return null;
};
以下が表示されます:
- 開発者は
HighlightIndicator
のエイリアスで指名したフィールドの値を、次を使って取得できます:
item?.getValue('HighlightIndicator')
。 HighlightIndicator
フィールドの値がhighlightValue
フィールドの値 (コード コンポーネントの input プロパティで提供される) が一致した場合、行に背景色を追加できます。DetailsRow
コンポーネントは、DetailsList
が定義した列のレンダリングに使用されるコンポーネントです。 背景色以外の動作を変更する必要はありません。
強調表示をサポートするためのプロップを追加します
updateView
内部のレンダリングで提供される highlightColor
と highlightValue
のために、プロップをいくつか追加します。 すでに GridProps
のインターフェイスに追加しているため、あとはプロップの destructuring に追加するだけです:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
onSort,
onFilter,
resources,
loadFirstPage,
loadNextPage,
loadPreviousPage,
onFullScreen,
isFullScreen,
} = props;
DetailsList に onRenderRow メソッドを追加します
onRenderRow
メソッドを DetailsList
プロップに渡します。
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
onItemInvoked={onNavigate}
></DetailsList>
コンポーネントを展開および構成する
すべての機能の実装が完了したため、テストに向けてコード コンポーネントを Microsoft Dataverse にデプロイする必要があります。
Dataverse 環境で、接頭辞が
samples
の公開元が作成されていることを確認してください:ご自身の公開元である可能性もあります。ただし、以下の pac pcf push へのコールで公開元の接頭辞パラメータを更新してください。 詳細については、ソリューションの公開元を作成するを参照してください。
公開元の保存後は、ご利用の環境に対して CLI を認証する準備が整ったことを意味します。コンパイルされたコード コンポーネントはプッシュできます。 コマンドラインでは、以下を使用します:
pac auth create --url https://myorg.crm.dynamics.com
myorg.crm.dynamics.com
はご利用の Dataverse 環境の URL に置き換えてください。 プロンプトが表示されたら、アドミニストレーター/カスタマイザーのユーザーでサインインしてください。 これらのユーザーロールが提供する権限は、コードコンポーネントを Dataverse にデプロイするために必要となります。コード コンポーネントをデプロイするには、次を使用します:
pac pcf push --publisher-prefix samples
注意
エラー
Missing required tool: MSBuild.exe/dotnet.exe. Please add MSBuild.exe/dotnet.exe in Path environment variable or use 'Developer Command Prompt for VS
が発生した場合は、Visual Studio 2019 for Windows & Mac または Build Tools for Visual Studio 2019 のいずれかをインストールする必要があります。その際、「前提条件」に記載のとおり、「.NET build tools」 ワークロードを必ず選択してください。このプロセスが完了すると、お使いの環境に PowerAppTools_samples という名前の小さな一時的なソリューションが作成され、
CanvasGrid
コード コンポーネントがこのソリューションに追加されます。 必要に応じて、コード コンポーネントを後からソリューションに移行できます。 詳しくは、コード コンポーネント アプリケーションの ライフサイクル管理 (ALM) を参照してください。キャンバス アプリ内でコード コンポーネントを使用するには、ご利用の環境で キャンバス アプリの Power Apps Component Framework を有効にする必要があります。
a. 管理センター (admin.powerplatform.microsoft.com) を開き、ご利用の環境に移動します。 b. 設定 > 製品 > 機能に移動します。 キャンバス アプリ用 Power Apps Component Framework が オンになっていることを確認してください:
タブレットレイアウトを使用して新しいキャンバスアプリを作成します。
インサートパネルから、さらにコンポーネントを入手するを選択します。
コンポーネントのインポートペインで、コード タブをを選択します。
CanvasGrid
コンポーネントを選択します。インポート を選択します。 挿入 パネルのコード コンポーネントにコード コンポーネントが表示されます。
CanvasGrid
コンポーネントを画面上にドラッグして、Contacts
テーブルに Microsoft Dataverse でバインドします。CanvasGrid
コード コンポーネントには、プロパティ パネルで以下のプロパティを設定します:- ハイライト値 =
1
- これは、レコードが非アクティブなときのstatecode
の値です。 - ハイライト カラー =
#FDE7E9
- これは、レコードが非アクティブな場合に使用する色です。 HighlightIndicator
="statecode"
- 比較対象となるフィールドです。 これは DATA セクションの詳細パネルになります。
- ハイライト値 =
新規
TextInput
コンポーネントを追加し、txtSearch
と名付けます。CanvasGrid.Items
プロパティをSearch(Contacts,txtSearch.Text,"fullname")
更新します。テキスト入力をすると、連絡先がグリッドにフィルターされるのがわかります。
新しいをテキストラベル追加し、テキストを「レコードが見つかりません」に設定します。 キャンバス グリッドの上にラベルを配置します。
テキストラベルの Visible プロパティを
CanvasGrid1.FilteredRecordCount=0
に設定します。
これは、txtSearch
の値に一致するレコードがない場合、またはコンテキスト メニューを使用して列フィルターを適用してもレコードがない場合 (たとえば、Full Name にはデータが含まれない)、ラベルが表示されることを意味します。
表示フォームを追加します (挿入パネルの入力グループから)。
Contacts
テーブルにフォームDataSource
を設定し、フォーム フィールドをいくつか追加します。フォームの
Item
プロパティをCanvasGrid1.Selected
に設定します。グリッド上の項目を選択すると、フォームには選択された項目が表示されるようになります。
キャンバスアプリ
scrDetails
に、新しい画面を追加します。前の画面からフォームをコピーして、新しい画面に貼り付けます。
CanvasGrid1.OnSelect
プロパティをNavigate(scrDetails)
に設定します。グリッドの行を選択するアクションを実行すると、項目が選択された状態でアプリが 2 画面目に遷移することがわかります。
デプロイ後のデバッグ
Ctrl+Shift+I
を使用して開発ツールを開くと、キャンバス アプリ内で実行中のコード コンポーネントを簡単にデバッグできます。
Ctrl+P
を選択し、Grid.tsx
または Index.ts
を入力します。 続いてブレーク ポイントを設定し、コードをステップアップしていきます。
コンポーネントにさらに変更を加える必要がある場合は、毎回デプロイする必要はありません。 その代わり、デバッグ コード コンポーネント で説明した手法でFiddler AutoResponder を作成し、npm start watch
の実行中にローカル ファイル システムからファイルを読み込むようにします。
AutoResponderは、次のようになります:
REGEX:(.*?)((?'folder'css|html)(%252f|\/))?SampleNamespace\.CanvasGrid[\.\/](?'fname'[^?]*\.*)(.*?)$
C:\repos\CanvasGrid\out\controls\CanvasGrid\${folder}\${fname}
また、Access-Control-Allow-Origin
のヘッダーを追加するフィルターを有効にする必要があります。 詳細: Microsoft Dataverse にデプロイした後のデバッグ。
AutoResponder ファイルを取り込むには、ブラウザのキャッシュを空にして最新の情報に更新する必要があります。 読み込んだ後は、フィドラーがファイルにキャッシュ コントロール ヘッダを追加してキャッシュされないようにするため、ブラウザを更新するだけです。
変更の完了後は、マニフェストのパッチ バージョンを増分し、pac pcf push を使用して再デプロイできます。
これまでは、最適化されていない開発用のビルドをデプロイしていたので、実行時の動作は遅くなります。 CanvasGrid.pcfproj
を編集することで、pac pcf push を使って最適化されたビルドをデプロイできます。 OutputPath
の配下に、以下を追加します: <PcfBuildMode>production</PcfBuildMode>
<PropertyGroup>
<Name>CanvasGrid</Name>
<ProjectGuid>a670bba8-e0ae-49ed-8cd2-73917bace346</ProjectGuid>
<OutputPath>$(MSBuildThisFileDirectory)out\controls</OutputPath>
</PropertyGroup>
関連記事
Microsoft Power Platform によるアプリケーション ライフサイクル管理 (ALM)
Power Apps Component Framework API の参照
最初のコンポーネントを作成する
コード コンポーネントのデバッグ
注意
ドキュメントの言語設定についてお聞かせください。 簡単な調査を行います。 (この調査は英語です)
この調査には約 7 分かかります。 個人データは収集されません (プライバシー ステートメント)。