チュートリアル: キャンバス アプリ データセット コンポーネントの作成

このチュートリアルでは、キャンバス アプリのデータセット コード コンポーネントを作成して、展開し、画面へ追加し、Visual Studio Code を使ったコンポーネントをテストします。 このコード コンポーネントは、ページ区切りされたスクロール可能なデータセットのグリッドを表示し、ソートやフィルター処理が可能な列を提供します。 また、インジケーターの列を構成することにより、特定の行を強調表示することもできます。 これはアプリ開発者からの一般的な要望であり、ネイティブのキャンバス アプリ コンポーネントを使用して実装するには複雑となる場合があります。 コード コンポーネントは、キャンバス アプリとモデル駆動型アプリの両方で機能するように記述できます。 ただし、このコンポーネントは、特にキャンバス アプリでの使用を想定して作成されています。

これらの要件にに加えて、コード コンポーネントがベストプラクティスのガイダンスに従っていることも確認する必要があります。

  1. Microsoft Fluent UI を使用する
  2. 設計時と実行時の両方でコード コンポーネント ラベルをローカライズします
  3. コードコンポーネントが親のキャンバス アプリの画面で指定された幅と高さで表示されることを確認してください
  4. アプリの開発者が入力プロパティやアプリの外部要素を使ってユーザー インターフェースを可能な限りカスタマイズできるように検討してください

キャンバス グリッド デモ

注意

開始する前に、すべてのコンポーネントの前提条件を満たしていることを確認してください。

コード

PowerApps-Samples/component-framework/CanvasGridControl/ から完全なサンプルをダウンロードできます。

新しい pcfproj プロジェクトを作成します

  1. コード コンポーネントで使用する新しいフォルダを作成します。 たとえば、C:\repos\CanvasGrid などとします。

  2. Visual Studio Code を開き、ファイル > フォルダを開く に移動し、CanvasGrid フォルダを選択します。 Visual Studio Code のインストール時に Windows Explorer の拡張機能を追加した場合は、フォルダ内のコンテキスト メニューからコードで開くを使用できます。 また、現在のディレクトリがこの場所に設定されている場合、コマンド プロンプトで code . を使って任意のフォルダを Visual Studio Code に読み込むこともできます。

  3. 新たな 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
    
  4. これにより、必要なモジュールを定義した 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 要素内に以下を追加します:

<data-set name="sampleDataSet"
  display-name-key="Dataset_Display_Key">
</data-set>

レコードデータセットは、コードコンポーネントがキャンバス アプリに追加されると、データソースにバインドされます。 プロパティ セットは、ユーザーがそのデータセットの列の 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 のファイルを再フォーマットしたものです。

    注意

    generatedout フォルダの内容を直接変更しないでください。 これらはビルドの過程で上書きされます。

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を使用して)、ヘッダー列をラップする目的で使用されます。
  • setKeyinitialFocusedIndex と共に DetailsList に渡され、現在のページが変わったときに、スクロール位置と選択がリセットされるようになっています。
  • 関数 onRenderItemColumn は、セルのコンテンツをレンダリングするために使用されます。 これは、行項目を受け取り、列の表示値を返すために、getFormattedValue を使用します。 getValue メソッドは、代替のレンダリングを提供するために使用できる値を返します。 getFormattedValue の利点は、日付やルックアップなどの非文字列型のカラムに対して、フォーマットされた文字列が含まれていることです。
  • gridColumns ブロックは、データセットコンテキストで提供されたカラムのオブジェクト形状を、DetailsList 列のプロップで想定される形状にマッピングしています。このブロックは React.useMemo フックでラップされているため、columnssorting のプロップが変更されたときにのみ出力が変更されます。 コード コンポーネントのコンテキストで提供されているソートやフィルタの詳細が、マッピングされている列と一致する列に、ソートやフィルターのアイコンを表示できます。 column.order プロパティを使用して列がソートされ、アプリの開発者が定義したグリッド上の正しい順序で表示されます。
  • React コンポーネントでは、isComponentLoading の内部状態を維持しています。 これは、ユーザーがソートやフィルターのアクションを選択すると、sortedRecordIds が更新されて状態がリセットされるまで、視覚的な手がかりとしてグリッドをグレーにすることができるためです。 itemsLoading という追加の入力プロップは、データセット コンテキストが提供する dataset.loading プロパティにマッピングされています。 これらのフラグは、Fluent UI Overlay コンポーネントを使用して実装された視覚的な読み込みキューを制御する目的で使用されます。

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) を呼び出しているため、allocatedWidthallocatedHeight は、それらが変更されるたびに (たとえば、アプリがコードコンポーネントのサイズを変更したり、フルスクリーンモードになったりした場合)、親コンテキストから提供されます。
  • updatedProperties の配列に dataset という文字列が含まれている場合は、表示する新しい行があることを検出できます。
  • テスト ハーネスでは、updatedProperties 配列は入力されていないので、init 関数で設定した isTestHarness フラグを使って、sortedRecordIdrecords を設定するロジックをショートさせます。 データの再レンダリングが必要な場合を除いて、子コンポーネントに渡される際にこれらを変更させないように、変更されるまで現在の値への参照を維持します。
  • コード コンポーネントは、どのページを表示しているかという状態を維持しているため、親コンテキストがレコードを最初のページにリセットすると、ページ番号がリセットされます。 hasPreviousPage が false の場合に 1 ページ目に戻ってくるとわかります。

破壊メソッドを更新します

最後に、コードコンポーネントが破棄されたときに整理する必要があります:

public destroy(): void {
    // Add code to cleanup control if necessary
}

テスト ハーネスを開始します

すべてのファイルが保存され、端末で使用されていることを確認します:

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では既定でレコードの選択が可能ですが、選択されたレコードはコード コンポーネントの出力にはリンクされません。 SelectedSelectedItems のプロパティは、キャンバス アプリ内で選択されたレコードを反映し、関連するコンポーネントが更新されるようにする必要があります。 この例では、一度にひとつの項目のみを選択できるように設定しているため、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.useCallbackuseConst フックは、これらの値がレンダリング間で変動せず、不要な子コンポーネントのレンダリングを引き起こすことがないようにする目的で使用されます。

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 の呼び出しは、キャンバス アプリに選択が変更されたことを通知し、SelectedItemsSelected を参照する他のコンポーネントが更新されるようにします。

新しいコールバックを入力 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 に追加

DetailListonItemInvoked というコールバック プロップを備えており、これにコールバックを渡します:

<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.tsonNavigate 方式を追加する:

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 を押す) と、DetailsListonNavigate のコールバックを呼び出すため、ブレークポイントがヒットします。

Canvas Data Grid が、OnNavigate in index.ts デバッグ

_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 ファイルに追加する必要があります:

<resources>
   <code path="index.ts"
      order="1" />
</resources>

列の並べ替えとフィルターを追加する

グリッドの列ヘッダーを使ってソートやフィルターできるようにする場合、Fluent UI DetailList は列ヘッダにコンテキスト メニューを簡単に追加する方法を提供しています。

GridProps に onSort と onFilter を追加する

まず、ソートとフィルターのためのコールバック関数を提供するために、Grid.tsx の中の GridProps インターフェースに onSortonFilter を追加します:

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);:

const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);

以下が表示されます:

  • contextualMenuProps の状態は、Fluent UI の ContextualMenu コンポーネントを使ってレンダリングされるコンテキスト メニューの可視性を制御する目的で使用されます。
  • このコードは、フィールドにデータが含まれない値のみを表示するシンプルなフィルタを提供しています。 これを拡張して、追加のフィルタリングを提供できます。
  • このコードは、ローカライズ可能なコンテキスト メニューのラベルを表示する目的で resources.getString を使用しています。
  • React.useCallback フックは (React.useMemo と同様)、依存する値が変化したときにのみコールバックが変更されるようになっています。 これにより、子コンポーネントのレンダリングが最適化されます。

これらの新しいコンテキストメニュー関数を、列選択とコンテキスト メニューのイベントに追加します

これらの新しいコンテキストメニュー関数を、列選択とコンテキスト メニューのイベントに追加します。 const gridColumns を更新し、onColumnContextMenuonColumnClick のコールバックを追加します:

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 という出力プロパティが既に定義されています。 フィルター処理が行われ、フィルターされたレコードが読み込まれると、文字列 updatedPropertiesdataset 配列に入れて updateView 関数が呼び出されます。 レコード数が変更された場合は、notifyOutputChanged を呼び出して、FilteredRecordCount プロパティを使用するコントロールを更新しする必要があることをキャンバス アプリが認識できるようにする必要があります。 index.tsupdateView 方式の内部、ReactDOM.render のすぐ上、allocatedHeight の下に次を追加します:

const allocatedHeight = parseInt(
    context.mode.allocatedHeight as unknown as string
);

FilteredRecordCount を getOutputs に追加する

これは、先に定義したコード コンポーネント クラスの filteredRecordCount が、受信した新しいデータと異なる場合に更新されます。 notifyOutputChanged が呼び出された後、getOutputs が呼ばれたときに値が返されるようにする必要があるため、getOutputs メソッドを更新します:

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

グリッドにページングを追加する

大規模なデータセットの場合、キャンバス アプリはレコードを複数のページに分割します。 ページ ナビゲーション コントロールを表示するフッターを追加できます。 各ボタンは 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 クラス名を使っても同様に適用できます。

ページングをサポートするコールバック プロップを追加する

続いて、不足している loadFirstPageloadNextPageloadPreviousPage のコールバック プロップを追加する必要があります。

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 コンポーネントを使用できます。 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 インターフェースに onFullScreenisFullScreen を追加します:

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) への呼び出しにより、それに応じて allocatedHeightallocatedWidth を調整します。 全画面表示モードが開かれると、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
);

行の強調表示

以上で、条件付きの行ハイライト機能を追加する準備が完了です。 すでに HighlightValueHighlightColor の入力プロパティと、HighlightIndicator property-set の入力プロパティ定義が完了しています。 property-set では、開発者が HighlightValue で提供する値と比較するために、使用するフィールドを指名することができます。

強調表示をサポートするインポート タイプ

DetailsList でのカスタム行レンダリングには、追加のインポートが必要です。 @fluentui/react/lib/DetailsList からのタイプがすでにいくつかあるため、IDetailsListPropsIDetailsRowStyles および DetailsRowGrid.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 内部のレンダリングで提供される highlightColorhighlightValue のために、プロップをいくつか追加します。 すでに 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 にデプロイする必要があります。

  1. Dataverse 環境で、接頭辞が samples の公開元が作成されていることを確認してください:

    新しい公開元の追加

    ご自身の公開元である可能性もあります。ただし、以下の pac pcf push へのコールで公開元の接頭辞パラメータを更新してください。 詳細については、ソリューションの公開元を作成するを参照してください。

  2. 公開元の保存後は、ご利用の環境に対して CLI を認証する準備が整ったことを意味します。コンパイルされたコード コンポーネントはプッシュできます。 コマンドラインでは、以下を使用します:

    pac auth create --url https://myorg.crm.dynamics.com
    

    myorg.crm.dynamics.com はご利用の Dataverse 環境の URL に置き換えてください。 プロンプトが表示されたら、アドミニストレーター/カスタマイザーのユーザーでサインインしてください。 これらのユーザーロールが提供する権限は、コードコンポーネントを Dataverse にデプロイするために必要となります。

  3. コード コンポーネントをデプロイするには、次を使用します:

    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」 ワークロードを必ず選択してください。

  4. このプロセスが完了すると、お使いの環境に PowerAppTools_samples という名前の小さな一時的なソリューションが作成され、CanvasGrid コード コンポーネントがこのソリューションに追加されます。 必要に応じて、コード コンポーネントを後からソリューションに移行できます。 詳しくは、コード コンポーネント アプリケーションの ライフサイクル管理 (ALM) を参照してください。

    PowerAppsTools_samples の一時的ソリューション

  5. キャンバス アプリ内でコード コンポーネントを使用するには、ご利用の環境で キャンバス アプリの Power Apps Component Framework を有効にする必要があります。

    a. 管理センター (admin.powerplatform.microsoft.com) を開き、ご利用の環境に移動します。 b. 設定 > 製品 > 機能に移動します。 キャンバス アプリ用 Power Apps Component Frameworkオンになっていることを確認してください:

    コード コンポーネントを有効化する

  6. タブレットレイアウトを使用して新しいキャンバスアプリを作成します。

  7. インサートパネルから、さらにコンポーネントを入手するを選択します。

  8. コンポーネントのインポートペインで、コード タブをを選択します。

  9. CanvasGrid コンポーネントを選択します。

  10. インポート を選択します。 挿入 パネルのコード コンポーネントにコード コンポーネントが表示されます。

  11. CanvasGrid コンポーネントを画面上にドラッグして、Contacts テーブルに Microsoft Dataverse でバインドします。

  12. CanvasGrid コード コンポーネントには、プロパティ パネルで以下のプロパティを設定します:

    • ハイライト値 = 1 - これは、レコードが非アクティブなときの statecode の値です。
    • ハイライト カラー = #FDE7E9 - これは、レコードが非アクティブな場合に使用する色です。
    • HighlightIndicator = "statecode" - 比較対象となるフィールドです。 これは DATA セクションの詳細パネルになります。

    プロパティ パネル

  13. 新規 TextInput コンポーネントを追加し、txtSearch と名付けます。

  14. CanvasGrid.Items プロパティを Search(Contacts,txtSearch.Text,"fullname") 更新します。

    テキスト入力をすると、連絡先がグリッドにフィルターされるのがわかります。

  15. 新しいをテキストラベル追加し、テキストを「レコードが見つかりません」に設定します。 キャンバス グリッドの上にラベルを配置します。

  16. テキストラベルの Visible プロパティを CanvasGrid1.FilteredRecordCount=0 に設定します。

これは、txtSearch の値に一致するレコードがない場合、またはコンテキスト メニューを使用して列フィルターを適用してもレコードがない場合 (たとえば、Full Name にはデータが含まれない)、ラベルが表示されることを意味します。

  1. 表示フォームを追加します (挿入パネルの入力グループから)。

  2. Contacts テーブルにフォーム DataSource を設定し、フォーム フィールドをいくつか追加します。

  3. フォームの Item プロパティを CanvasGrid1.Selected に設定します。

    グリッド上の項目を選択すると、フォームには選択された項目が表示されるようになります。

  4. キャンバスアプリ scrDetails に、新しい画面を追加します。

  5. 前の画面からフォームをコピーして、新しい画面に貼り付けます。

  6. 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 分かかります。 個人データは収集されません (プライバシー ステートメント)。