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

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

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

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

キャンバス グリッド デモ。

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

注意

エンティティとテーブルの違いがわかりませんか? Microsoft Dataverse で「開発者: 用語を理解する」を参照してください。

コード

完全なサンプルは、こちら からダウンロードできます。

新しい 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 --namespace SampleNamespace --name CanvasGrid --template dataset
    

    または、次の短いフォームを使用します:

    pac pcf init -ns SampleNamespace -n CanvasGrid -t dataset
    
  4. これにより、必要なモジュールを定義した packages.json を含む、新しい pcfproj と関連ファイルが現在のフォルダに追加されます。 必要なモジュールをインストールするには、以下を使用します:

    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 ファイルは、コード コンポーネントの動作を記述するメタデータを定義します。 コントロール属性には、コンポーネントの名前空間と名前がすでに含まれています。 コード コンポーネントがバインドされるレコードを定義する必要があります。これを行うには、既存の data-set 要素の代わりに、control 要素内に以下を追加します:

<data-set name="records" display-name-key="Records_Dataset_Display">
      <property-set name="HighlightIndicator" display-name-key="HighlightIndicator_Disp" description-key="HighlightIndicator_Desc" of-type="SingleLine.Text" usage="bound" required="true" />
</data-set>

レコードデータセットは、コードコンポーネントがキャンバス アプリに追加されると、データソースにバインドされます。 プロパティ セットは、ユーザーがそのデータセットの列の 1 つを、行の強調表示インジケーターとして使用するように構成する必要があることを示します。

ヒント

複数のデータセット要素を指定できます。 これは、あるデータセットを検索し、別のデータセットを使ってレコードのリストを表示する場合に便利です。

入力プロパティと出力プロパティの定義

データセットに加えて、以下の 入力 プロパティを指定できます:

  • HighlightValue - アプリ開発者が、HighlightIndicator propert-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

コンポーネントがビルドされると、以下が表示されます:

  1. 自動生成されたファイル CanvasGrid\generated\ManifestTypes.d.ts がプロジェクトに追加されます。 これは、ビルド プロセスの一部として ControlManifest.Input.xml から生成され、入力/出力プロパティの操作に使用する型を提供します。

  2. ビルドの出力が out フォルダに追加されます。 bundle.js はブラウザ内で実行されるトランスパイルされた JavaScript、ControlManifest.xml はデプロイ時に使用される ControlManifest.Input.xml のファイルを再フォーマットしたものです。

    注意

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

Grid Fluent UI React コンポーネント

コード コンポーネントが React を使用している場合、updateView メソッド内でレンダリングされるルート コンポーネントは 1 つである必要があります。 CanvasGrid フォルダの中に、 Grid.tsxという名前の新しい TypeScript ファイルを追加し、以下の内容を追加します:

import { DetailsList } from '@fluentui/react/lib/components/DetailsList/DetailsList';
import {
    ConstrainMode,
    DetailsListLayoutMode,
    IColumn,
    IDetailsHeaderProps,
} from '@fluentui/react/lib/components/DetailsList/DetailsList.types';
import { Overlay } from '@fluentui/react/lib/components/Overlay/Overlay';
import { ScrollablePane } from '@fluentui/react/lib/components/ScrollablePane/ScrollablePane';
import { ScrollbarVisibility } from '@fluentui/react/lib/components/ScrollablePane/ScrollablePane.types';
import { Stack } from '@fluentui/react/lib/components/Stack/Stack';
import { Sticky } from '@fluentui/react/lib/components/Sticky/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/components/Sticky/Sticky.types';
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 にコンパイルされます。

上記のコードから、次のことがわかります:

  1. const { ... } = props; は「destructuring」と呼ばれ、レンダリングに必要な列 (フィールド) をプロップから抽出し、使用するたびに props. を前置するのではなく、その属性を使用します。

  2. パスベースのインポートを使用して Fluent UI コンポーネントをインポートすることで、バンドルのサイズを小さくすることができます。 代替案:

    import { DetailsList, Stack } from "@fluentui/react";
    

    次を使用します:

    import { DetailsList } from "@fluentui/react/lib/components/DetailsList/DetailsList";
    import { Stack } from "@fluentui/react/lib/components/Stack/Stack";
    

    別の方法としては、ツリー シェイクがあります。

  3. これは React の機能的なコンポーネントですが、同様にクラスのコンポーネントである可能性もあります。 これは、好みのコーディング スタイルに基づくものです。 クラス コンポーネントと機能コンポーネントは、同じプロジェクトの中で混在させることができます。 関数コンポーネントとクラス コンポーネントは、Reactで使用されている tsx XML スタイルの構文を使用します。

  4. React.memo を使用して機能コンポーネントをラップし、入力プロップが変更されない限りレンダリングされないようにします。

  5. React.useMemo は、入力されたプロップス options または configuration が変更されたときにのみ、作成されたアイテム配列が更されるようにする目的で使用します。 これは関数コンポーネントのベスト プラクティスであり、子コンポーネントの不要なレンダリングを減らすことができます。

  6. Stack の中の DetailsList がラップされているのは、後でページング コントロールのあるフッター要素を追加する目的があるためです。

  7. Fluent UI Sticky コンポーネントは、グリッドをスクロールしてもヘッダー列が表示されたままになるように (onRenderDetailsHeaderを使用して)、ヘッダー列をラップする目的で使用されます。

  8. setKeyinitialFocusedIndex と共に DetailsList に渡され、現在のページが変わったときに、スクロール位置と選択がリセットされるようになっています。

  9. 関数 onRenderItemColumn は、セルのコンテンツをレンダリングするために使用されます。 これは、列の表示値を返すために getFormattedValue を使用する行項目を受け取ります。 getValue メソッドは、代替のレンダリングを提供するために使用できる値を返します。 getFormattedValue の利点は、日付やルックアップなどの非文字列型のカラムに対して、フォーマットされた文字列が含まれていることです。

  10. gridColumns ブロックは、データセットコンテキストで提供されたカラムのオブジェクト形状を、DetailsList 列のプロップで想定される形状にマッピングしています。このブロックは useMemo の React フックでラップされているため、columnssorting のプロップが変更されたときにのみ出力が変更されます。 コード コンポーネントのコンテキストで提供されているソートやフィルタの詳細が、マッピングされている列と一致する列に、ソートやフィルターのアイコンを表示できます。 column.order プロパティを使用して列がソートされ、アプリの開発者が定義したグリッド上の正しい順序で表示されます。

  11. React コンポーネントでは、isComponentLoading の内部状態を維持しています。 これは、ユーザーがソートやフィルターのアクションを選択すると、sortedRecordIds が更新されて状態がリセットされるまで、視覚的な手がかりとしてグリッドをグレーにすることができるためです。 itemsLoading という追加の入力プロップは、データセット コンテキストが提供する dataset.loading プロパティにマッピングされています。 これらのフラグは、Fluent UI Overlay コンポーネントを使用して実装された視覚的な読み込みキューを制御する目的で使用されます。

index.ts のヘッダーに、既存のインポートを以下のように置き換えます:

import { initializeIcons } from "@fluentui/react/lib/Icons";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { Grid } from "./Grid";

initializeIcons(undefined, { disableWarnings: true });

注意

Fluent UI のアイコンセットを使用しているため、initializeIcons のインポートが必要となります。 テスト ハーネス内でアイコンを読み込むには、initializeIcons を呼び出す必要があります。 これらはキャンバス アプリの内部ですでに初期化されています。

export class CanvasGrid の下に以下のクラス フィールドを追加します:

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;

以下を init に追加します:

this.notifyOutputChanged = notifyOutputChanged;
this.container = container;
this.context = context;
this.context.mode.trackContainerResize(true);
this.resources = this.context.resources;
this.isTestHarness = document.getElementById("control-dimensions") !== null;

init 関数は、アプリ画面でコード コンポーネントが初期化される際に最初に呼び出されます。 以下への参照を保存します:

  • notifyOutputChanged - これは、プロパティのひとつが変更されたことをキャンバス アプリに通知する目的で呼び出されるコールバックです。

  • container - これは、コード コンポーネントの UI を追加する DOM 要素です。

  • resources - 現在のユーザーの言語でローカライズされた文字列を取得するために使用されます。

trackContainerResize(true) は、コード コンポーネントのサイズが変更されたときに updateView を呼び出す目的で使用します。

注意

現在のところ、コード コンポーネントがテスト ハーネス内で実行されているかどうかを判断する方法はありません。 control-dimensions div がインジケーターとして存在するかどうかを検出する必要があります。

以下を updateView に追加します:

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;
}
if (resetPaging || datasetChanged || this.isTestHarness) {
  this.records = dataset.records;
  this.sortedRecordsIds = dataset.sortedRecordIds;
}

// The test harness provides width/height as strings
const allocatedWidth = parseInt(
  context.mode.allocatedWidth as unknown as string
);
const allocatedHeight = parseInt(
  context.mode.allocatedHeight as unknown as string
);

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

以下が表示されます:

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

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

public destroy(): void {
    ReactDOM.unmountComponentAtNode(this.container);
}

npm start watch を使うと、テスト ハーネス内のシンプルなグリッドを見ることができます。 サンプルの 3 つのレコードを使って入力されたコード コンポーネントのグリッドを表示するには、幅と高さを設定する必要があります。 続いて、レコードのセットを Dataverse から CSV ファイルにエクスポートし、データ入力 > レコード パネル を使用してテスト ハーネスに読み込むことができます:

テスト ハーネス

注意

読み込んだ CSV ファイルの列に関わらず、テスト ハーネスには 1 つの列しか表示されません。 これは、テスト ハーネスが定義されたものが 1 つの場合には property-set しか表示しないためです。 property-set が定義されていない場合は、読み込んだ CSV ファイルのすべての列に入力されます。

行の選択を追加する

Fluent UI の DetailsListでは既定でレコードの選択が可能ですが、選択されたレコードはコード コンポーネントの出力にはリンクされません。 SelectedSelectedItems のプロパティは、キャンバス アプリ内で選択されたレコードを反映し、関連するコンポーネントが更新されるようにする必要があります。 この例では、一度にひとつの項目のみを選択できるように設定しているため、SelectedItems にはひとつのレコードしか入りません。

Grid.tsx 内のインポートに以下を追加します:

import { useConst } from "@fluentui/react-hooks/lib/useConst";
import { useForceUpdate } from "@fluentui/react-hooks/lib/useForceUpdate";
import { Selection } from "@fluentui/react/lib/Selection";
import { SelectionMode } from "@fluentui/react/lib/Utilities";

Grid.tsx 内の GridProps インターフェースに、以下を追加します:

setSelectedRecords: (ids: string[]) => void;

Grid.tsx 関数コンポーネントの中 (export const Grid = React.memo((props: GridProps) => { の直下) で、プロップの destructuring を更新し、新しいプロップ setSelectedRecords を追加します:

const { ...setSelectedRecords } = 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,
  });
});

useCallbackuseConst フックは、これらの値がレンダリング間で変動せず、不要な子コンポーネントのレンダリングを引き起こすことがないようにする目的で使用されます。 useForceUpdate フックは、選択が更新されたときに、更新された選択数を反映してコンポーネントが再レンダリングされるようにする目的で使用されます。

そして、選択状態を保持するために作成された選択オブジェクトは、DetailsList コンポーネントに渡されます:

<DetailsList
    ...
    selection={selection}

index.ts の中に新しい setSelectedRecords のコールバックを定義し、それを Grid のコンポーネントに渡す必要があります。 index.ts 内の CanvasGrid クラスの先頭に、以下を追加します:

setSelectedRecords = (ids: string[]): void => {
  this.context.parameters.records.setSelectedRecordIds(ids);
};

注意

このメソッドは、コード コンポーネントの現在の this インスタンスに結合するため、「矢印関数」として定義されています。

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

最後に、Grid コンポーネントの updateView メソッドの入力プロップに、新しいコール バックを追加します:

ReactDOM.render(
    React.createElement(Grid, {
        ...
        setSelectedRecords: this.setSelectedRecords,
    }),

OnSelect イベントの呼び出し

キャンバス アプリでは、ギャラリーやグリッドでアイテムの選択が行われると (例: シェブロン アイコンの選択)、OnSelect イベントが発生するというパターンがあります。 このパターンは、データセットの openDatasetItem メソッドを使って実装できます。

先ほどと同様に、Grid コンポーネントにコールバック プロップを追加するには、Grid.tsx の中の GridProps インターフェイスに次のように追加します:

export interface GridProps {
    ...
    onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
}

ここでも、新しいプロップをプロップの destructuring に追加する必要があります:

const { ...onNavigate } = props;

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

<DetailsList
    ...
    onItemInvoked={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, {
        ...
        onNavigate: this.onNavigate,
    }),

すべてのファイルを保存すると、テスト ハーネスがリロードされます。 Ctrl + Shift + I (または F12) を使用し、ファイルを開く (Ctrl + P) を使用してindex.ts を検索した場合、onNavigate メソッド内にブレークポイントを置くことができます。 行をダブルクリック (またはカーソル キーでハイライトして Enter を押すと)、DetailsListonNavigate のコールバックを呼び出すため、ブレークポイントがヒットします。

キャンバス データ グリッド 3。

_this への参照があるのは、この関数が矢印関数として定義されており、this のインスタンスをキャプチャするために JavaScript のクロージャにトランスパイルされているためです。

ローカライズの追加

先に進む前に、リソース文字列をコード コンポーネントに追加して、ページング、ソート、フィルターなどのメッセージにローカライズされた文字列を使用できるようにする必要があります。 以下の内容で新しいファイル CanvasGrid\strings\CanvasGrid.1033.resx を追加します:

<?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>Hightlight 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>

ヒント

resx ファイルを直接編集することはお勧めしません。 代わりに、Visual Studio リソース エディタ、または Visual Studio Code の拡張機能のどちらかを使用できます。

input/output プロパティと dataset および関連する property-set のリソース文字列があります。 これらは、開発者のブラウザ言語に基づいて、デザイン時に Power Apps Studio で使用されます。 getString を使ってランタイムに取得できるラベル文字列を追加することもできます。 詳しくは、ローカリゼーション API コンポーネントの実装を参照してください。

この新しいリソース ファイルは、resources 要素内の ControlManifest.Input.xml ファイルに追加する必要があります:

<resx path="strings/CanvasGrid.1033.resx" version="1.0.0" />

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

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

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

export interface GridProps {
    ...
    onSort: (name: string, desc: boolean) => void;
    onFilter: (name: string, filtered: boolean) => void;
}

続いて、これらの新しいプロップと resources の参照を (ソートやフィルターをする目的でローカライズされたラベルを取得できるように)、プロップスのデストラクションに追加します:

const { ...onSort, onFilter, resources } = props;

Fluent UI が提供する ContextualMenu コンポーネントを使用できるように、 Grid.tsx の先頭にいくつかのインポートを追加する必要があります。 パス ベースのインポートを使うことで、バンドルのサイズを小さくすることができます。

import {
  DirectionalHint,
  IContextualMenuProps,
} from "@fluentui/react/lib/components/ContextualMenu/ContextualMenu.types";
import { ContextualMenu } from "@fluentui/react/lib/components/ContextualMenu/ContextualMenu";

const [isComponentLoading, setIsLoading] = React.useState<boolean>(false); のすぐ下の Grid.tsx にコンテキスト メニュー レンダリング機能を追加します:

const [contextualMenuProps, setContextualMenuProps] = React.useState<IContextualMenuProps>();

const onContextualMenuDismissed = React.useCallback(() => {
    setContextualMenuProps(undefined);
}, [setContextualMenuProps]);

const getContextualMenuProps = React.useCallback(
    (column: IColumn, ev: React.MouseEvent<HTMLElement>): IContextualMenuProps => {
    const menuItems = [
    {
    key: 'aToZ',
    name: resources.getString('Label_SortAZ'),
      iconProps: { iconName: 'SortUp' },
          canCheck: true,
              checked: column.isSorted && !column.isSortedDescending,
                  disable: (column.data as ComponentFramework.PropertyHelper.DataSetApi.Column).disableSorting,
                      onClick: () => {
                          onSort(column.key, false);
                          setContextualMenuProps(undefined);
                          setIsLoading(true);
                      },
},
    {
        key: 'zToA',
            name: resources.getString('Label_SortZA'),
                iconProps: { iconName: 'SortDown' },
                    canCheck: true,
                        checked: column.isSorted && column.isSortedDescending,
                            disable: (column.data as ComponentFramework.PropertyHelper.DataSetApi.Column).disableSorting,
                                onClick: () => {
                                    onSort(column.key, true);
                                    setContextualMenuProps(undefined);
                                    setIsLoading(true);
                                },
    },
        {
            key: 'filter',
                name: resources.getString('Label_DoesNotContainData'),
                    iconProps: { iconName: 'Filter' },
                        canCheck: true,
                            checked: column.isFiltered,
                                onClick: () => {
                                    onFilter(column.key, column.isFiltered !== true);
                                    setContextualMenuProps(undefined);
                                    setIsLoading(true);
                                },
        },
            ];
return {
    items: menuItems,
    target: ev.currentTarget as HTMLElement,
    directionalHint: DirectionalHint.bottomLeftEdge,
    gapSpace: 10,
    isBeakVisible: true,
    onDismiss: onContextualMenuDismissed,
};
},
    [setIsLoading, onFilter, setContextualMenuProps],
        );

const onColumnContextMenu = React.useCallback(
    (column?: IColumn, ev?: React.MouseEvent<HTMLElement>) => {
        if (column && ev) {
            setContextualMenuProps(getContextualMenuProps(column, ev));
        }
    },
    [getContextualMenuProps, setContextualMenuProps],
);

const onColumnClick = React.useCallback(
    (ev: React.MouseEvent<HTMLElement>, column: IColumn) => {
        if (column && ev) {
            setContextualMenuProps(getContextualMenuProps(column, ev));
        }
    },
    [getContextualMenuProps, setContextualMenuProps],
);

以下が表示されます:

  1. contextualMenuProps の状態は、Fluent UI の ContextualMenu コンポーネントを使ってレンダリングされるコンテキスト メニューの可視性を制御する目的で使用されます。
  2. フィールドにデータが含まれていない値のみを表示するシンプルなフィルターを提供しています。 これを拡張して、追加のフィルタリングを提供できます。
  3. resources.getString は、ローカライズ可能なコンテキスト メニューのラベルを表示する目的で使用します。
  4. 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,
        onColumnContextMenu: onColumnContextMenu,
        onColumnClick: onColumnClick,
      } as IColumn;
    });
}, [columns, sorting, onColumnContextMenu, onColumnClick]);

コンテキスト メニューを表示するには、レンダリングされた出力に追加する必要があります。 返された出力の DetailsList コンポーネントの直下に以下を追加します:

{contextualMenuProps && <ContextualMenu {...contextualMenuProps} />}

並べ替えとフィルタリングの 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();
};

以下が表示されます:

  1. 並べ替えとフィルターは、sortingfiltering を使ってデータセットのプロパティに適用されます。
  2. 並べ替え列を変更する際には、並べ替え配列自体を置き換えるのではなく、既存の並べ替え定義をポップで削除する必要があります。
  3. 更新は、ソートやフィルターが適用された後に呼び出す必要があります。 フィルターと並べ替えが同時に適用された場合、更新は一度で済みます。

最後に、この 2 つのコールバックを Grid のレンダリング呼び出しに渡します:

ReactDOM.render(
    React.createElement(Grid, {
        ...
        onSort: this.onSort,
        onFilter: this.onFilter,
    }),

この時点では、テスト ハーネスは並べ替えやフィルターに対応していないため、テストを行うことはできません。 その後、pac pcf push を使ってデプロイし、キャンバス アプリに追加してテストできます。 必要であれば、このステップをスキップして、コード コンポーネントがキャンバス アプリ内でどのように見えるかを確認することができます。

FilteredRecordCount 出力のプロパティを更新する

グリッドは内部でレコードをフィルターできるようになったため、表示されたレコードの数をキャンバス アプリに報告することが重要です。 これは、「レコードなし」などのメッセージを表示させるためです。

ヒント

これをコードコンポーネント内で実装することも可能ですが、アプリ開発者の自由度を高めるためにも、ユーザーインターフェースはできるだけキャンバス アプリに任せることをお勧めします。

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

if (this.filteredRecordCount !== this.sortedRecordsIds.length) {
  this.filteredRecordCount = this.sortedRecordsIds.length;
  this.notifyOutputChanged();
}

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

public getOutputs(): IOutputs {
    return {
        FilteredRecordCount: this.filteredRecordCount,
    } as IOutputs;
}

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

大規模なデータセットの場合、キャンバス アプリはレコードを複数のレコードに分割します。 ページ ナビゲーション コントロールを表示するフッターを追加できます。 各ボタンは Fluent UI IconButton を使ってレンダリングされるため、Grid.tsx 内のインポートにこれを追加します:

import { IconButton } from "@fluentui/react/lib/components/Button/IconButton/IconButton";

また、Grid.tsx には、ScrollablePane を含む既存の Stack.Item の下に次の Stack.Item を追加します:

<Stack.Item>
    <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>
</Stack.Item>

以下が表示されます:

  1. Stack はフッターが DetailsList の下に重なるようにします。 grow属性は、グリッドが使用可能なスペースを埋めるように拡張する目的で使用されます。

  2. ページ インジケータ ラベルのフォーマットをリソース文字列 ("Page {0} ({1} Selected)") から読み込み、Grid.tsx の先頭に追加する必要がある簡単な関数を使ってフォーマットします。 これを別のファイルにして、コンポーネント間で共有すると便利です:

    function stringFormat(template: string, ...args: string[]): string {
      for (const k in args) {
        template = template.replace("{" + k + "}", args[k]);
      }
      return template;
    }
    
  3. ページングの IconButtons にアクセシビリティに使用する alt テキストを提供することができます。

  4. フッターのスタイルは、コード コンポーネントに追加された CSS ファイルを参照する CSS クラス名を使っても同様に適用できます。

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

GridProps インターフェースに以下を追加します:

export interface GridProps {
    ...
     loadFirstPage: () => void;
     loadNextPage: () => void;
     loadPreviousPage: () => void;
}

次に、これらの新しいプロップをリソースと一緒に、プロップの destructuring に追加します:

const { ...loadFirstPage, loadNextPage, loadPreviousPage } = props;

これらのコールバックは、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 のレンダリング コール内の GridProps インターフェースに渡されます。

ReactDOM.render(
    React.createElement(Grid, {
        ...
        loadFirstPage: this.loadFirstPage,
        loadNextPage: this.loadNextPage,
        loadPreviousPage: this.loadPreviousPage,
    }),

注意

現在、キャンバス データセットのページングで、currentPage がデータセットのページと同期しない問題があります。 この問題の修正が展開されています。 詳細については、外部フィルターを適用しても、キャンバス データセットのページングがリセットされないを参照してください。

フルスクリーン サポートの追加

コード コンポーネントは、フルスクリーン モードで表示する機能を提供します。 これは、画面サイズが小さい場合や、キャンバス アプリの画面内にコードコ ンポーネントのスペースが限られている場合に特に有効です。 Fluent UI Link コンポーネントを使うことができるため、Grid.tsx の上部にあるインポートに追加する必要があります:

import { Link } from "@fluentui/react/lib/components/Link/Link";

フル スクリーンのリンクを追加するには、ページングコントロールを含む既存の Stack に以下を追加します。 (これをネストされた Stack に必ず追加してください。ルート Stack には追加しないでください):

<Stack.Item grow align="center">
    {!isFullScreen && (
        <Link onClick={onFullScreen}>{resources.getString('Label_ShowFullScreen')}</Link>
    )}
</Stack.Item>

以下が表示されます:

  1. ローカライズに対応するラベルを表示するリソースを使用します。
  2. フルスクリーン モードが開いている場合、リンクは表示されません。 その代わりに、親アプリのコンテキストによって閉じるアイコンが自動的にレンダリングされます。

並び替えとフィルターのコールバック関数を提供するために、Grid.tsx の内部で GridProps インターフェースに追加する必要がある 2 つのプロップが存在します:

export interface GridProps {
    ...
    onFullScreen: () => void;
    isFullScreen: boolean;
}

これらの新しいプロップをリソースと一緒に、プロップの destructuring に追加します:

const { ...onFullScreen, isFullScreen } = props;

これらの新しいプロップを提供するために、index.ts 内部に以下のコールバック メソッドを追加します:

onFullScreen = (): void => {
  this.context.mode.setFullScreen(true);
};

setFullScreen への呼び出しにより、コードコンポーネントはフル スクリーンモードを開き、init メソッドで trackContainerResize(true) への呼び出しを行ったので、それに応じて allocatedHeightallocatedWidth を調整します。 フルスクリーン モードが開かれると、updateView が呼び出され、コンポーネントのレンダリングを新しいサイズに更新することができます。 updatedProperties には、発生しているトランジションに応じて fullscreen_open または fullscreen_close が入ります。

フルスクリーン モードの状態を保存するには、index.ts の内側の CanvaGrid クラスに新しいフィールドを追加します:

isFullScreen = false;

updateView メソッドに、以下を追加して、状態を追跡します:

if (context.updatedProperties.indexOf("fullscreen_close") > -1) {
  this.isFullScreen = false;
}
if (context.updatedProperties.indexOf("fullscreen_open") > -1) {
  this.isFullScreen = true;
}

以上で、コールバックと isFullScreen のフィールドを Grid のレンダリング プロップに渡すことができます:

ReactDOM.render(
    React.createElement(Grid, {
        ...
        isFullScreen: this.isFullScreen,
        onFullScreen: this.onFullScreen,
    }),

行の強調表示

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

DetailsList rでカスタム行レンダリングを追加するには、いくつかの追加インポートが必要となるため、Grid.tsx の先頭に以下を追加します:

import { IDetailsListProps } from "@fluentui/react/lib/components/DetailsList/DetailsList.types";
import { IDetailsRowStyles } from "@fluentui/react/lib/components/DetailsList/DetailsRow.types";
import { DetailsRow } from "@fluentui/react/lib/components/DetailsList/DetailsRow";

続いて、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;
};

以下が表示されます:

  1. 開発者は HighlightIndicator のエイリアスで指名したフィールドの値を item?.getValue('HighlightIndicator') を使って取得できます。
  2. HighlightIndicator フィールドの値と highlightValue フィールドの値が一致した場合 (コード コンポーネントの input プロパティで提供される) のみ、行に背景色を追加します。
  3. DetailsRow コンポーネントは、DetailsList が定義した列のレンダリングに使用されるコンポーネントです。 背景色以外の動作を変更する必要はありません。

updateView 内部のレンダリングで提供される highlightColorhighlightValue のために、追加プロップをいくつか追加する必要があることがわかります。 すでに GridProps のインターフェイスに追加しているため、あとはプロップの destructuring に追加するだけです:

const { ...highlightValue, highlightColor } = props;

以上で、onRenderRow メソッドを DetailsList プロップに渡すことができるようになりました:

<DetailsList
    ...
    onRenderRow={onRenderRow}
    ></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 を有効にする必要があります。 管理センター (admin.powerplatform.microsoft.com) を開き、ご利用の環境に移動します。 設定 > 製品 > 機能 に移動します。 キャンバス アプリ用 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. CanvaGrid1.Items プロパティを Search(Contacts,txtSearch.Text,"fullname") 更新します。 テキスト入力 をすると、連絡先がグリッドにフィルターされるのがわかります。

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

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

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

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

  19. フォームの Item プロパティを CanvasGrid1.Selected に設定します。 グリッド上の項目を選択すると、フォームには選択された項目が表示されるようになります。

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

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

  22. CanvasGrid1.OnSelect プロパティを Navigate(scrDetails) に設定します。 グリッドの行を選択するアクションを実行すると、項目が選択された状態でアプリが 2 画面目に遷移することがわかります。

デプロイ後のデバッグ

Ctrl + Shift + I を使用して開発ツールを開くと、キャンバス アプリ内で実行中のコード コンポーネントを簡単にデバッグできます。

Ctrl + P を選択し、Grid.tsx または Index.tsx を入力します。 続いてブレーク ポイントを設定し、コードをステップアップしていきます。

キャンバス アプリにおけるデバッグ。

コンポーネントにさらに変更を加える必要がある場合は、毎回デプロイする必要はありません。 その代わり、デバッグ コード コンポーネント で説明した手法でFiddler AutoResponder を作成し、npm start watch の実行中にローカル ファイル システムからファイルを読み込むようにします。

AutoResponder は、次のようになります:

REGEX:(.*?)((?'folder'css|html)(%252f|\/))?SampleNamespace\.CanvasGrid[\.\/](?'fname'[^?]*\.*)(.*?)$
C:\repos\CanvasGrid\out\controls\CanvasGrid\${folder}\${fname}

AutoResponder ルール。

また、Access-Control-Allow-Origin のヘッダーを追加するフィルターを有効にする必要があります。 詳細: Microsoft Dataverse にデプロイした後のデバッグ

AutoResponder ファイルを取り込むには、ブラウザの キャッシュを空にして最新の情報に更新 する必要があります。 読み込んだ後は、フィドラーがファイルにキャッシュ コントロール ヘッダを追加してキャッシュされないようにするため、ブラウザを更新するだけです。

変更の完了後は、マニフェストのパッチ バージョンを増分し、pac pcf push を使用して再デプロイできます。

これまでは、最適化されていない開発用のビルドをデプロイしていたので、実行時の動作は遅くなります。 CanvasGrid.pcfproj を編集することで、pac pcf push を使って最適化されたビルドをデプロイできます。 OutputPath の配下に、以下を追加します:

<PcfBuildMode>production</PcfBuildMode>

Microsoft Power Platform によるアプリケーション ライフサイクル管理 (ALM)
Power Apps Component Framework API の参照
最初のコンポーネントを作成する
コード コンポーネントのデバッグ

注意

ドキュメントの言語設定についてお聞かせください。 簡単な調査を行います。 (この調査は英語です)

この調査には約 7 分かかります。 個人データは収集されません (プライバシー ステートメント)。