鍋綿ブログ

C#・SharePoint・SharePoint Framework・Office365を中心に扱うブログです。

SharePoint Frameworkの動的データ利用方法

SharePoint Framework (以下SPFx) では、Webパーツや拡張機能間でデータのやり取りをするために「動的データ」という仕組みを利用します。動的データを作り出して他のWebパーツで利用できるように公開することは簡単なのですが、動的データの利用方法はリファレンスが見当たらなかったため、GitHub上のサンプルコードを読んで色々と試す必要がありました。備忘として残しておきます。

 

 

動的データを取得するための一番簡単な方法

動的データを取得するには、SPFxのライブラリ側で予め用意されているモジュールを利用し、Webパーツのプロパティで動的データを選択することが一番簡単です。この方法は以前ご紹介しています。

www.micknabewata.com

 

上記の方法は非常に簡単なのですが、少し捻った要件が出てくるとうまく行きません。以下の点が問題になります。

  • 利用可能な全ての動的データが選択肢に現れてしまい制限ができないこと
  • 文字列や数値ではなく自作の型で動的データを公開すると型内のプロパティまで選択しなければならないこと
  • Webパーツのプロパティからの選択ではなく固定の値を取得したい場合に対応できないこと

自前のコードで動的データを取得する方法

GitHubに公開したサンプルコードと併せてお読みください。

github.com

 

[コードの説明]

  • simpleObjectProvider Webパーツを配置すると動的データが作られます。
  • simpleStringViewer Webパーツを配置すると上記一番簡単な方法の問題点が実感できます。
  • customObjectViewer Webパーツが今回の本題です。以下で解説します。

 

[画面]

simpleObjectProviderとcustomObjectViewerを貼るとこうなります。

f:id:micknabewata:20190703151721p:plain

サンプルWebパーツ

上の色が付いているのがsimpleObjectProvider、下のJSON文字列がcustomObjectViewerです。simpleObjectProviderでの入力に従って、customObjectViewerの表示が動的に変わります。

当サンプルで扱う動的データの型

src/dynamicData/objectData.tsで定義しています。単純な文字列や数値ではなく、プロパティをいくつも持つ自作の型を2つ公開します。

import { IDynamicDataPropertyDefinition, IDynamicDataCallables } from '@microsoft/sp-dynamic-data';

/** プロパティID */
export const propertyId1 = 'objectData';
export const propertyId2 = 'objectData2';

/** 型情報 */
export interface ObjectType {
  /** 名前 */
  name : string;
  /** 数量 */
  ammount : number;
  /** 日付 */
  date : Date;
  /** オブジェクト */
  obj : SubObjectType;
}

/** サブ型情報 */
export interface SubObjectType {
    /** 名前 */
    subName : string;
    /** 数量 */
    subAmmount : number;
}

/** 構造化されたデータを公開する動的データクラス */
export default class ObjectData implements IDynamicDataCallables {

  /** 値1 */
  private _value1 : ObjectType;

  /** 値2 */
  private _value2 : ObjectType;

  /** 動的データの型定義 */
  public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
    return [
      {
        id: propertyId1,
        title: 'オブジェクト1'
      },
      {
        id: propertyId2,
        title: 'オブジェクト2'
      }
    ];
  }
  
  /** 値を取得 */
  public getPropertyValue(propId: string): ObjectType {
    switch (propId) {
        case propertyId1:
          return this._value1;
        case propertyId2:
            return this._value2;
    }

    throw new Error('プロパティIDが不正です。');
  }

  /** 値をセット */
  public setPropertyValue(propId: string, value : ObjectType)
  {
    switch (propId) {
      case propertyId1:
        this._value1 = value;
        break;
      case propertyId2:
        this._value2 = value;
        break;
      default :
        throw new Error('プロパティIDが不正です。');
    }
  }
}

動的データを提供するWebパーツ

/src/webparts/simpleObjectProvider/SimpleObjectProviderWebPart.tsです。入力欄を用意する段階でプロパティが多すぎたかと後悔しましたが(笑、コードとしては特に捻りはありません。

動的データを取得するWebパーツ

ここが本題です。
/src/webparts/customObjectViewer/CustomObjectViewerWebPart.tsです。

/src/webparts/simpleStringViewer/SimpleStringViewerWebPart.tsでやっているようにPropertyPaneDynamicFieldSetを利用する簡単なやり方ではなく、自前で動的データの一覧を取得・フィルタリングし、ドロップダウンの選択肢を作っています。

利用可能な動的データソースの取得

getAvailableDataSourcesメソッドで動的データソースの一覧をthis.context.dynamicDataProvider.getAvailableSources()の命令で取得します。全量としては標準のページコンテキストに加えて動的データを公開するWebパーツの分だけ動的データソースが取得できますが、今回のサンプルではその一部に絞り込んで取得できるようにしています。

/** 利用可能な動的データを取得 */
protected getAvailableDataSources(webpartNames: string[] = []): IDynamicDataSource[] {
  let ret: IDynamicDataSource[] = [];
  this.context.dynamicDataProvider.getAvailableSources().forEach((source) => {
    if (webpartNames && webpartNames.length > 0) {
      webpartNames.forEach((webpartName) => {
        if (source.metadata.title == webpartName) ret.push(source);
      });
    }
    else {
      ret.push(source);
    }
  });

  return ret;
}

 

今回のサンプルでは、上記メソッドで絞り込んだ動的データソースをWebパーツのプロパティで選択できるようにしています。以下のようにドロップダウン選択肢を作ります。

/** 利用可能な動的データを選択するためのドロップダウン選択肢を取得 */
protected getDynamicDataSourceDropdownOptions() : IPropertyPaneDropdownOption[] {
  let ret: IPropertyPaneDropdownOption[] = [];

  // SimpleObjectProvider Webパーツが提供する動的データを取得
  // (ページにWebパーツを2つ貼ることもできるのでここで選択させる)
  let webpartName = ['SimpleStringProvider', 'SimpleObjectProvider' ];
  let sources = this.getAvailableDataSources(webpartName);
  if (sources)
  {
    sources.forEach((source, i) => {
      ret.push({
        index: i,
        key: source.id,
        text: source.metadata.title,
        type: PropertyPaneDropdownOptionType.Normal
      });
    });
  }

  return ret;
}

 

で、Webパーツのプロパティを以下のように定義します。

/** Webパーツのプロパティ定義 */
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
  return {
    pages: [
      {
        groups: [
          {
            groupName: '動的データ接続',
            groupFields: [
              PropertyPaneDropdown('dynamicDataSource', {
                options: this.getDynamicDataSourceDropdownOptions(),
                label: 'データソース (Webパーツ)'
              })
            ]
          }
        ],
        displayGroupsAsAccordion : false
      }
    ]
  };
}

 

このプロパティで選択された値を受け取ってくれるように、プロパティ定義もお忘れなく。/src/webparts/customObjectViewer/ICustomObjectViewerWebPartProps.tsです。

/** CustomObjectViewerWebPartクラス プロパティ定義 */
export default interface ICustomObjectViewerWebPartProps {
    /** 選択中の動的データソースID */
    dynamicDataSource: string;
}

選択した動的データソース内の動的プロパティを取得

上記までで、最終的に取得するべき動的データソースのIDが得られました。「当サンプルで扱う動的データの型」の章でご紹介したように、動的データソース内では複数の動的プロパティを公開することが可能です。今回のサンプルではsrc/dynamicData/objectData.tsで定義したobjectDataとobjectData2が動的プロパティに当たります。これをユーザーが選択できるようにWebパーツのプロパティを構成します。

まず、以下のように動的データソース内の動的プロパティを列挙してドロップダウン選択肢として返します。

/** 選択中の動的データソースから利用可能なプロパティを選択するためのドロップダウン選択肢を取得 */
protected getDynamicPropertyDropdownOptions(): IPropertyPaneDropdownOption[] {
  let ret: IPropertyPaneDropdownOption[] = [];

  if (this.properties.dynamicDataSource && this.properties.dynamicDataSource.length > 0) {
    let source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSource);
    if (source) {
      source.getPropertyDefinitions().forEach((property, i) => {
        ret.push({
          index: i,
          key: property.id,
          text: property.title,
          type: PropertyPaneDropdownOptionType.Normal
        });
      });
    }
  }

  return ret;
}

 

で、Webパーツのプロパティにドロップダウンを追加します。

/** Webパーツのプロパティ定義 */
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
  return {
    pages: [
      {
        groups: [
          {
            groupName: '動的データ接続',
            groupFields: [
              PropertyPaneDropdown('dynamicDataSource', {
                options: this.getDynamicDataSourceDropdownOptions(),
                label: 'データソース (Webパーツ)'
              }),
              PropertyPaneDropdown('dynamicDataProperty', {
                options: this.getDynamicPropertyDropdownOptions(),
                label: 'プロパティ',
                disabled: !(this.properties.dynamicDataSource && this.properties.dynamicDataSource.length > 0)
              })
            ]
          }
        ],
        displayGroupsAsAccordion : false
      }
    ]
  };
}

 

選択された値を受け取るプロパティも/src/webparts/customObjectViewer/ICustomObjectViewerWebPartProps.tsに追加します。

/** CustomObjectViewerWebPartクラス プロパティ定義 */
export default interface ICustomObjectViewerWebPartProps {
    /** 選択中の動的データソースID */
    dynamicDataSource: string;

    /** 選択中の動的データプロパティ */
    dynamicDataProperty: string;
}

動的プロパティの値を取得

ここまでで、動的データソースのIDと動的プロパティのIDが取得できるようになります。次は動的プロパティから値を取得します。

/** 選択中の動的データソースとプロパティから値を取得 */
protected getDynamicPropertyValues(): any {
  let ret: any;

  if (this.properties.dynamicDataSource && this.properties.dynamicDataSource.length > 0 && this.properties.dynamicDataProperty && this.properties.dynamicDataProperty.length > 0) {
    let source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSource);
    if (source) {
      ret = source.getPropertyValue(this.properties.dynamicDataProperty);
    }
  }

  return ret;
}

 

これをrenderメソッドから呼び出して描画用のコンポーネントに渡してあげます。

/** 描画 */
public render(): void {
  ReactDom.render(
    React.createElement(
      CustomObjectViewer,
      {
        dynamicData: this.getDynamicPropertyValues()
      }
    ), 
    this.domElement);
}

 

今回のサンプルでは渡された値をJSON.stringifyして表示しているだけなので、整形も何もしていません。anyのまま渡しています。雑!

/src/webparts/customObjectViewer/components/CustomObjectViewer.tsx

import * as React from 'react';
import styles from './CustomObjectViewer.module.scss';
import { ICustomObjectViewerProps } from './ICustomObjectViewerProps';

/** 動的データ表示Webパーツ */
export default class CustomObjectViewer extends React.Component<ICustomObjectViewerProps, {}> {
  public render(): React.ReactElement<ICustomObjectViewerProps> {
    return (
      <div className={ styles.customObjectViewer }>
        {
          (this.props.dynamicData)? 
            JSON.stringify(this.props.dynamicData) : 
            ''
        }
      </div>
    );
  }
}

動的プロパティの値の変更をキャッチするリスナーを登録

さて上記までのコードで動的プロパティの値を表示できるようになりましたが、このままだと動的プロパティの値が変わったときにWebパーツの描画を更新してくれません。というわけでリスナーを登録します。登録後、別の動的プロパティを選んだりWebパーツを削除した時にはリスナーの解除も行う必要があります。(やらないとJavaScriptエラーが起きて画面が固まります)

まず、登録と登録解除は以下のように行います。

private dynamicData: DynamicProperty<any>;

/** 動的データの変更を捕まえるリスナーを登録 */
protected registerDynamicDataReference() {
  if (this.properties.dynamicDataSource
    && this.properties.dynamicDataSource.length > 0
    && this.properties.dynamicDataProperty
    && this.properties.dynamicDataProperty.length > 0)
  {
    // 当クラスのフィールドに持っている動的プロパティの初期化(1回だけ)
    if (!this.dynamicData) {
      this.dynamicData = new DynamicProperty<any>(this.context.dynamicDataProvider, this.render);
      this.dynamicData.register(this.render);
    }

    // 動的プロパティの関連付けを設定(都度)
    this.dynamicData.setReference(`${this.properties.dynamicDataSource}:${this.properties.dynamicDataProperty}`);
  }
  else
  {
    // 情報が足りない場合には登録解除を行う
    this.unregisterDynamicDataReference();
  }
}

/** 動的データの変更を捕まえるリスナーを登録解除 */
protected unregisterDynamicDataReference() {
  if (this.dynamicData) this.dynamicData.unregister(this.render);
}

 

で、renderメソッドとonDisposeメソッドでそれぞれ呼び出します。

/** 描画 */
public render(): void {
  this.registerDynamicDataReference();

  ReactDom.render(
    React.createElement(
      CustomObjectViewer,
      {
        dynamicData: this.getDynamicPropertyValues()
      }
    ), 
    this.domElement);
}

/** Webパーツの破棄イベント */
protected onDispose(): void {
  this.unregisterDynamicDataReference();
  ReactDom.unmountComponentAtNode(this.domElement);
}

 

これで動的データが更新された時にWebパーツが再描画されるようになります。
今回はシンプルにほとんどの処理をWebパーツのクラス内で実施しましたが、実運用に回すコードでは可読性を考慮してクラスを分けるなど工夫してください。

 

以上、参考になれば幸いです。