ActiveReportsJSでつくる!PowerAppsの帳票コンポーネント(1)

アプリケーション開発の現場では、ノーコード・ローコードツールがもはや特別な存在ではなく、当たり前の選択肢となっています。Microsoftの「Power Apps」はその代表格として、さまざまな業界や規模の企業で活用され、業務効率化やデジタル化を支える重要なツールとなっています。

こうしたツールで作成されたアプリケーションによって、業務の自動化やデータ管理が進む一方で、請求書や納品書、業務報告書といった外部とのやり取りに利用する帳票は、BtoBやBtoCの場面で信頼関係を築くために欠かせない役割を果たしています。正確さや見やすさ、法的要件への対応が求められるこれらの帳票は、Power Appsの標準機能だけでは対応が難しい場合があります。

メシウスの「ActiveReportsJS(アクティブレポートJS)」は、日本の細やかな帳票開発ニーズに応えることができるJavaScript帳票ライブラリです。「Power Apps Component Framework(略称PCF)」を利用することで、Power Appsに組み込める帳票コンポーネントを作成することが可能です。

Power Apps x ActiveReportsJS

今回の記事では、ActiveReportsJSを活用し、Power Appsで利用できる帳票コンポーネントの作成方法を解説します。また、次回の記事では、作成したPower Apps帳票コンポーネントを活用して帳票アプリケーションを作成する方法を紹介しますので、ぜひあわせてご覧ください。

事前準備

今回は以下の開発環境を使用します。

上記に加え、ActiveReportsJS「V5.1J(v5.1.5)」を使用します。事前準備として、あらかじめ製品版、またはトライアル版をインストールしてください。トライアル版は無料で以下より入手可能です。

Power Apps帳票コンポーネントの作成

それでは、ActiveReportsJSを利用してPower Appsのコンポーネントを作成していきます。Power Appsで利用できるカスタムコンポーネントには、標準コンポーネントを組み合わせて作成する「コンポーネントライブラリ」と、PCFを利用してプログラムコードによって作成する「コードコンポーネント」があります。

コードコンポーネントは、TypeScript、CSSなどをWebフロントエンド技術を用いて開発することが可能で、JavaScriptライブラリも利用できるため、コンポーネントライブラリとくらべてより強固な機能をもつコンポーネントの作成が可能です。コードコンポーネントの作成方法については、以下の記事とスライドでも詳しく解説しています。こちらもあわせてご覧ください。

プロジェクト作成

それでは、早速コードコンポーネントの作成を行っていきます。まずはじめにコードコンポーネントのプロジェクトを作成します。プロジェクトの作成あたり、事前に任意のフォルダを作成しておきます。今回はフォルダ名を「PCFJSReportsViewer」というフォルダを作成しました。

続いて、以下のように作成したフォルダをVisual Studio Codeで開きます。さらに、ターミナルも起動しておきます。

ディレクトリの作成

その後、ターミナル上で以下のコマンドを実行し、PCFプロジェクト作成します。
今回はフレームワークにReactを使用するプロジェクトを作成していきます。

pac pcf init -n JSReportsViewer -ns ActiveReportsJS -t field -fw react -npm
プロジェクト作成1

コマンドを実行すると、次のようにプロジェクトフォルダが作成され、続いてコマンドオプション「-npm」に基づいて、npm installが実行されます。

プロジェクト作成2

ActiveReportsJSのインストール

PCFプロジェクトが作成されましたので、続いて、ActiveReportsJSのパッケージをインストールしてきます。

npm install @mescius/activereportsjs @mescius/activereportsjs-react @mescius/activereportsjs-i18n 
ActiveReportsJSのインストール

CSSファイルの追加

ここまでの手順で、プロジェクトの作成が終わりましたので、続いて必要なフォルダとファイルをプロジェクト上に追加していきます。

まず、以下のコマンドで、作成したプロジェクトフォルダへ移動します

cd JSReportsViewer

続いて、VSCode上より、JSReportsViewerの配下にCSSフォルダを追加し、以下のCSSファイルを追加します。

#viewer-host {
    width: 100%;
    height: 100vh;
}
CSSファイルの追加

マニフェストファイルの更新

続いて、コンポーネントの名前、参照するCSSなどのリソースや、プロパティ定義などを行う為のマニフェストファイル「ControlManifest.Input.xml」を更新していきます。

CSSファイルの参照設定

まず、先ほど追加したCSSファイルと、ActiveReportsJSのパッケージ内に含まれているCSSファイルを以下のように、<resources>タグブロック内に追加します。

CSS参照の追加

プロパティ定義

続いて、作成する帳票コンポーネントに対し、帳票レイアウトと、帳票表示用データ、帳票ビューワーサイズを設定する為のプロパティを定義します。以下のように、サンプルとして定義されている、「sampleProperty」を書き換えて、「ReportLayout」、「ReportData」、「ViewerWidth」、「ViewerHeight」の4つのプロパティ定義を追加します。

プロパティ定義の追加

上記で変更を行った部分は、以下の強調箇所です。

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="ActiveReportsJS" constructor="JSReportsViewer" version="0.0.1" display-name-key="JSReportsViewer" description-key="" control-type="virtual" >
    <!--external-service-usage ノードは、このサードパーティPCFコントロールが外部サービスを使用しているかどうかを宣言します。使用している場合、このコントロールはプレミアムと見なされ、使用している外部ドメインも追加してください。
    外部サービスを使用していない場合、enabled="false" を設定し、以下にドメインを追加しないでください。"enabled" はデフォルトで false になります。
    例1:
      <external-service-usage enabled="true">
      <domain>www.Microsoft.com</domain>
      </external-service-usage>
    例2:
      <external-service-usage enabled="false">
      </external-service-usage>
    -->
    <external-service-usage enabled="false">
      <!--外部ドメインを追加するにはコメントを解除してください
      <domain></domain>
      <domain></domain>
      -->
    </external-service-usage>
    <!-- property ノードは、コントロールがCDSから期待する特定の設定可能なデータを識別します -->
    <property name="ReportLayout" display-name-key ="レポートレイアウト" description-key="帳票表示を行うレポートレイアウトを設定します。" of-type ="Multiple" usage="bound" required="true"/>
    <property name="ReportData" display-name-key ="レポート表示用データ" description-key="レポート表示用のデータを設定します。" of-type ="Multiple" usage="bound" required="false"/>
    <property name="ViewerWidth" display-name-key="ビューワの横幅" description-key="ビューワの横幅を指定します。" of-type="SingleLine.Text" pfx-default-value ='"100%"' usage="input" required="true"/>  
    <property name="ViewerHeight" display-name-key="ビューワの高さ" description-key="ビューワの高さを指定します。" of-type="SingleLine.Text" pfx-default-value ='"100%"' usage="input" required="true"/>
    <!--
      Property ノードの of-type 属性は of-type-group 属性にすることができます。
      例:
      <type-group name="numbers">
      <type>Whole.None</type>
      <type>Currency</type>
      <type>FP</type>
      <type>Decimal</type>
      </type-group>
      <property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
    -->
    <resources>
      <code path="index.ts" order="1"/>
      <platform-library name="React" version="16.14.0" />
      <platform-library name="Fluent" version="9.46.2" />
      <css path="css/app.css" order="2" />
      <css path="../node_modules/@mescius/activereportsjs/styles/ar-js-ui.css" order="3" />
      <css path="../node_modules/@mescius/activereportsjs/styles/ar-js-viewer.css" order="4" />            
      <!-- 追加のリソースを追加するにはコメントを解除してください
      <css path="css/JSReportsViewer.css" order="1" />
      <resx path="strings/JSReportsViewer.1033.resx" version="1.0.0" />
      -->
    </resources>
    <!-- 指定されたAPIを有効にするにはコメントを解除してください
    <feature-usage>
      <uses-feature name="Device.captureAudio" required="true" />
      <uses-feature name="Device.captureImage" required="true" />
      <uses-feature name="Device.captureVideo" required="true" />
      <uses-feature name="Device.getBarcodeValue" required="true" />
      <uses-feature name="Device.getCurrentPosition" required="true" />
      <uses-feature name="Device.pickFile" required="true" />
      <uses-feature name="Utility" required="true" />
      <uses-feature name="WebAPI" required="true" />
    </feature-usage>
    -->
  </control>
</manifest>

※ コメントは日本語へ翻訳しています。

マニフェストファイルの更新後、型定義ファイル「ManifestTypes.d.ts」を更新する為、以下のコマンドをターミナル上で実行し、一度ビルドします。

npm run build

ビルドを行うと、以下のように型定義ファイルが更新されます。

型定義ファイルの更新

tsxファイルの更新

続いて、帳票コンポーネントを実装する「tsx」ファイルを更新していきます。まずファイル名を「HelloWorld」から「ReportsViewer」へ変更します。その後ファイル内を以下のように書き換えます。

import * as React from 'react';
import { Viewer } from '@mescius/activereportsjs-react';
import "@mescius/activereportsjs/pdfexport";
import "@mescius/activereportsjs/htmlexport";
import "@mescius/activereportsjs/xlsxexport";
import "@mescius/activereportsjs/tabulardataexport";
import "@mescius/activereportsjs-i18n";
import * as Core from "@mescius/activereportsjs/core";

// レポートビューアーのプロパティを定義するインターフェース
export interface IReportsViewerProps {
  ReportLayout?:string | null;
  ReportData?:string | null;
  ViewerWidth?: string;  
  ViewerHeight?: string;  
}

// レポートビューアーのクラス
export class ReportsViewer extends React.Component<IReportsViewerProps> {
  viewerRef: React.RefObject<Viewer> = React.createRef();

  /**
   * 接続文字列を生成する関数。
   * @param data - JSON形式のデータ
   * @returns 接続文字列
   */
  private getConnectionString(data: string | null): string {
      if (data === null) {
          console.warn('データがnullのため、空の接続文字列を返します。');
          return "jsondata={}";
      }
      try {
          return "jsondata=" + JSON.stringify(data);
      } catch (error) {
          console.error('JSON.stringifyでエラーが発生しました:', error);
          return "jsondata={}";
      }
  }
  
  /**
   * レポートを更新する関数。
   * Viewerにレポートレイアウト、レポートデータを設定し、表示を更新します。
   */
  private setReport(): void {
      if (this.viewerRef.current && this.props.ReportLayout) {
          const report = JSON.parse(JSON.stringify(this.props.ReportLayout));
          const reportData = this.props.ReportData ?? null;
          console.log("setReport:",report);
          this.setReportDataConnection(report, reportData);
          this.viewerRef.current.Viewer.open(report);
      }
  }

  /**
   * レポートのデータソースの接続プロパティを更新します。
   * データソースがJSONを使用し、接続文字列に"jsondata"が含まれている場合、
   * 提供されたレポートデータで接続文字列を更新します。
   * 条件が満たされない場合や、レポートに有効な接続プロパティがない場合は警告をログに記録します。
   *
   * @param report - データソースと接続プロパティを含むレポートオブジェクト。
   * @param reportData - 接続文字列の更新に使用するデータ。
   */
  private setReportDataConnection(report: { DataSources?: { ConnectionProperties?: { DataProvider?: string; ConnectString?: string } }[] } | null, reportData: string | null): void {
    try {
        if (report?.DataSources?.[0]?.ConnectionProperties) {
            const connectionProperties = report.DataSources[0].ConnectionProperties;
  
            if (
                connectionProperties?.DataProvider?.includes("JSON") &&
                connectionProperties?.ConnectString?.includes("jsondata")
            ) {
                connectionProperties.ConnectString = this.getConnectionString(reportData);
            } else {
                console.warn("接続プロパティが必要な条件を満たしていません。");
            }
        } else {
            console.warn("レポートに有効な接続プロパティがありません。");
        }
          
    } catch (error) {
        console.error("接続プロパティの更新中にエラーが発生しました:", error);        
    }
  }
  
  /**
   * コンポーネントのマウント時に呼び出されるReactライフサイクルメソッド。
   * 初期化処理を行います。
   */
  public componentDidMount(): void {
      console.log("componentDidMount:", this.props.ReportLayout);
      this.setReport();
  }
  
  /**
   * コンポーネントの更新時に呼び出されるReactライフサイクルメソッド。
   * プロパティの変更に応じて処理を更新します。
   */
  public componentDidUpdate(prevProps: IReportsViewerProps): void {
      console.log("componentDidUpdate:", this.props.ReportLayout);
      if (this.props.ReportLayout !== prevProps.ReportLayout || this.props.ReportData !== prevProps.ReportData) {
          this.setReport();
      }
  }
  
  /**
   * コンポーネントの描画を行うReactメソッド。
   * ビューアーのスタイルを設定し、表示を行います。
   */
  public render(): React.ReactNode {
      const viewerStyle = {
          width: this.props.ViewerWidth || '100%',
          height: this.props.ViewerHeight || '100%'
      };
  
      return (
          <div id="viewer-host" style={viewerStyle}>
              <Viewer ref={this.viewerRef} language='ja'/>
          </div>
      )
  }
}

index.tsファイルの更新

「tsx」ファイルの変更にあわせて、「index.ts」ファイルも以下の強調表示箇所を更新します。

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { ReportsViewer, IReportsViewerProps } from "./ReportsViewer";
import * as React from "react";

/**
 * JSON文字列を安全にパースし、結果のオブジェクトを返します。
 * パースに失敗した場合、警告メッセージをログに記録し、nullを返します。
 *
 * @template T - パースされたオブジェクトの期待される型。
 * @param {string} json - パースするJSON文字列。
 * @returns {T | null} - パースされた型Tのオブジェクト、またはパースに失敗した場合はnull。
 */
const safeParseJSON = <T>(json: string): T | null => {
    try {
        return JSON.parse(json) as T;
    } catch {
        console.warn('JSONのパースに失敗しました。');
        return null;
    }
};

export class JSReportsViewer implements ComponentFramework.ReactControl<IInputs, IOutputs> {
    private theComponent: ComponentFramework.ReactControl<IInputs, IOutputs>;
    private notifyOutputChanged: () => void;

    protected props: IReportsViewerProps = {
        ReportLayout: null,
        ReportData: null,
        ViewerWidth: '100%',
        ViewerHeight: '100%'
    };
        
    /**
     * 空のコンストラクタ。
     */
    constructor() { }

    /**
     * コントロールインスタンスを初期化するために使用されます。ここでリモートサーバー呼び出しやその他の初期化アクションを開始できます。
     * データセットの値はここでは初期化されません。updateView を使用してください。
     * @param context Context Object を介してコントロールで利用可能なすべてのプロパティバッグ。カスタマイザーによって設定された値がマニフェストで定義されたプロパティ名にマッピングされ、ユーティリティ関数も含まれます。
     * @param notifyOutputChanged コントロールが非同期で取得可能な新しい出力を持っていることをフレームワークに通知するためのコールバックメソッド。
     * @param state 単一ユーザーの1つのセッションで永続化するデータ。Mode インターフェースで 'setControlState' を呼び出すことで、コントロールのライフサイクルの任意の時点で設定できます。
     */
    public init(
        context: ComponentFramework.Context<IInputs>,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary
    ): void {
        this.notifyOutputChanged = notifyOutputChanged;
        this.props.ReportLayout = safeParseJSON(context.parameters.ReportLayout.raw ?? '');
        this.props.ReportData =safeParseJSON(context.parameters.ReportData.raw??'');
        this.props.ViewerWidth =context.parameters.ViewerWidth.raw??'100%';
        this.props.ViewerHeight =context.parameters.ViewerHeight.raw??'100%';      
    }

    /**
     * プロパティバッグ内の任意の値が変更されたときに呼び出されます。これには、フィールド値、データセット、コンテナの高さや幅、オフライン状態、ラベルや可視性などのコントロールメタデータ値が含まれます。
     * @param context Context Object を介してコントロールで利用可能なすべてのプロパティバッグ。カスタマイザーによって設定された値がマニフェストで定義された名前にマッピングされ、ユーティリティ関数も含まれます。
     * @returns コントロールのルート React 要素
     */
    public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
        this.props.ReportLayout = safeParseJSON(context.parameters.ReportLayout.raw ?? '');
        this.props.ReportData =safeParseJSON(context.parameters.ReportData.raw??'');
        this.props.ViewerWidth =context.parameters.ViewerWidth.raw??'100%';
        this.props.ViewerHeight =context.parameters.ViewerHeight.raw??'100%';      
        return React.createElement(
            ReportsViewer, this.props
        );
    }

    /**
     * コントロールが新しいデータを受け取る前にフレームワークによって呼び出されます。
     * @returns マニフェストで定義された命名規則に基づくオブジェクト。"bound" または "output" としてマークされたプロパティに期待されるオブジェクト
     */
    public getOutputs(): IOutputs {
        return { };
    }

    /**
     * コントロールがDOMツリーから削除されるときに呼び出されます。この呼び出しを使用してクリーンアップを行うべきです。
     * 例: 保留中のリモート呼び出しのキャンセル、リスナーの削除など。
     */
    public destroy(): void {
        // クリーンアップ処理を追加
        this.theComponent = null!;
        this.notifyOutputChanged = null!;
        this.props = null!;
    }
}

※ コメントは日本語へ翻訳しています。

ローカル環境での動作確認

ここまででコードの実装は終わりとなりますので、ローカル環境にて動作確認を行ってみます。以下のコマンドを実行すると、Webブラウザが起動し、作成したコンポーネントの動作を確認できます。

npm start

作成したプロパティ、「ReportLayout」、「ReportData」に帳票レイアウトと帳票表示データの設定が必要となるため、それぞれ以下のデータを設定します。

プロパティ設定値
ReportLayoutgithubに公開中の「Invoice_green.rdlx-json」のJSON文字列
ReportData[ { "ID": 1, "BillNo": "WS-DF502", "SlipNo": "GB465", "CustomerID": 1, "CustomerName": "PCF食品", "Products": "コーヒー 250 ml", "Number": 100, "UnitPrice": 100, "Date": "2020-01-05T00:00:00" }, { "ID": 2, "BillNo": "WS-DF502", "SlipNo": "GB465", "CustomerID": 1, "CustomerName": "PCF食品", "Products": "紅茶 350 ml", "Number": 300, "UnitPrice": 120, "Date": "2020-01-05T00:00:00" }, { "ID": 3, "BillNo": "WS-DF502", "SlipNo": "DK055", "CustomerID": 1, "CustomerName": "PCF食品", "Products": "炭酸飲料 (オレンジ) 350 ml", "Number": 200, "UnitPrice": 120, "Date": "2020-01-09T00:00:00" } ]

Webブラウザ上に、コンポーネントが配置され、帳票レイアウト、帳票表示用データそれぞれをプロパティに設定することで、帳票が表示されることが確認できました。

Power Apps環境での動作確認

ライセンス設定

ローカル環境での動作確認が行えましたので、続いてPower Apps環境での動作確認も行っていきたいと思います。Power Apps環境での動作確認するために、まず、ActiveReportsJSのライセンス設定を行う必要があります。

ActiveReportsJSのライセンスは「開発ライセンス」のほか、運用環境で動作させるために「配布ライセン」が必要となります。

配布ライセンスは、基本的に1つのプライマリドメインか1つのサブドメインでアプリを配布できるものです。しかし、Power Appsでは、アプリケーションの開発や運用でドメインが変わるほか、さまざまなドメイン環境で構築されているため、通常の配布ライセンスでは対応できません。別途、特別なライセンス契約が必要になりますので、営業までお問い合わせください。

ライセンスキーを取得後、「ReportsViewer.tsx」のコードに設定します。以下の強調表示された箇所が該当部分です。
※ ローカル環境での動作確認は、トライアル版(ライセンスキー未設定)でも可能ですが、Power Apps環境で動作確認する場合は、ライセンスキーの設定が必須となります。

import * as React from 'react';
import { Viewer } from '@mescius/activereportsjs-react';
import "@mescius/activereportsjs/pdfexport";
import "@mescius/activereportsjs/htmlexport";
import "@mescius/activereportsjs/xlsxexport";
import "@mescius/activereportsjs/tabulardataexport";
import "@mescius/activereportsjs-i18n";
import * as Core from "@mescius/activereportsjs/core";

//配布ライセンスキーを設定(トライアル版で利用する場合は、以下のコードは不要です)
Core.setLicenseKey("YOUR LICENSE KEY");

// レポートビューアーのプロパティを定義するインターフェース
export interface IReportsViewerProps {
  ReportLayout?:string | null;
  ReportData?:string | null;
  ViewerWidth?: string;  
  ViewerHeight?: string;  
}

// レポートビューアーのクラス
export class ReportsViewer extends React.Component<IReportsViewerProps> {
  viewerRef: React.RefObject<Viewer> = React.createRef();

// 以下、省略

コンポーネントのアップロード

ライセンス設定を行うと、Power Apps環境でも動作が可能となりますので、コンポーネントをアップロードします。今回はPower Platform CLI コマンド「pcf push」を使ってアップロードを行います。

まず、pcf pushコマンドを実行する前に、以下のコマンドで接続先の認証プロファイルの作成を行います。

pac auth create --environment 接続先環境URLを指定

つづいて、以下のコマンドで対象の環境へアップロードを行います。

pac pcf push --publisher-prefix arj #発行者を表すカスタマイズの接頭辞の値

複数の認証プロファイルがある場合、必要に応じpac auth listコマンドおよび、pac auth selectコマンドを使用してプロファイルの選択を行ってください。コンポネントのアップロードに関する詳しい内容は以下の資料でも解説しています。

コマンド実行後、以下のように、「完了」となれば、コンポーネントのアップロードは成功です。

pcf push

動作確認

さいごに、Power Apps環境で動作確認を行います。次の動画のように、空のキャンバスアプリを作成し、コードコンポーネントをインポート後、キャンバス上に配置します。さらに、プロパティにデータを設定するためのテキストボックスを配置し、そのテキストボックスに帳票レイアウトや帳票表示用データの文字列を設定します。

設定後、帳票コンポーネント内に帳票が正しく表示されることを確認できました。

さいごに

今回の記事では、「ActiveReportsJS」と「Power Apps Component Framework」を使用し、Power Apps上で利用可能な帳票コンポーネントの作成方法について解説しました。作成したコードは、以下のGitHub上に公開していますので、こちらもご確認ください。

次回の記事では、SharePoint Online上に配置した帳票レイアウトファイルと、Sharepointリストを使用して、Power Apps上で帳票アプリケーションを作成する方法について解説していきます。こちらも是非ご覧ください。

製品サイトでは、今回ご紹介したActiveReportsJSの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。

また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。

\  この記事をシェアする  /