次の方法で共有


SharePoint と Open XML

Open XML コンテンツ コントロールによる SharePoint からのドキュメント生成

Eric White

コード サンプルのダウンロード

ある部門のマネージャーが上司に書式を整えたステータス レポートを定期的に送信する必要があったり、チームのリーダーが多くの関係者に毎週ステータス レポートを送信する必要があったりというのはよくあることです。マネージャーでもチーム リーダーでも、組織内の他の関係者と共同作業を行う場合、ステータス情報を SharePoint リストで管理することができます。開発者にとって問題になるのは、リスト内の情報をステータス レポートなどのドキュメントに含める方法です。

Open XML は、Office 2007 の既定のファイル形式で、ISO 標準 の 1 つです (IS29500 に詳しく記載されています)。簡単に言えば、Open XML ファイルとは XML を含む Zip ファイルのことで、Open XML ドキュメントはプログラムから非常に簡単に生成または変更できます。必要なのは、Zip ファイルを開くライブラリと XML API だけです。Open XML と SharePoint のプログラミング機能を使用すれば、Open XML コンテンツ コントロールを利用して、レポートを担当するマネージャーやチーム リーダーが使用できる簡易ドキュメント生成システムを構築できます。今回のコラムでは、SharePoint リストを使用して、Open XML ワード プロセッサ ドキュメントの表にデータを設定するドキュメント生成システムを作成しながら、参考になるガイダンスとサンプル コードを紹介します。

サンプルの概要

このコラムで紹介するサンプルは、簡単な SharePoint Web パーツで、SharePoint リストに含まれるデータから Open XML ワード プロセッサ ドキュメントを生成します。今回は、図 1 に示すような 2 つのカスタム SharePoint リストを作成しました。これらのリストには表に挿入するデータが保持されています。


図 1 2 つのカスタム SharePoint リスト

また、テンプレートとなる Open XML ワード プロセッサ ドキュメントも作成し、生成するドキュメント用のデータのソースとなるリストと列を定義するコンテンツ コントロールを含めました。このコントロールは 図 2 のようになります。


図 2 コンテンツ コントロールを保持するテンプレート Open XML ドキュメント

最後に、特定のドキュメント ライブラリからテンプレート ドキュメントを取得し、取得したリストをユーザーに表示する Web パーツを作成しました。ユーザーは、リスト内の項目を選択してから [Generate Report] をクリックします。この Web パーツでは、Open XML ワード プロセッサ ドキュメントを作成し、作成したドキュメントを Reports ドキュメント ライブラリに配置後、ユーザーをそのライブラリにリダイレクトして、ユーザーがレポートを開けるようにします。この Web パーツを 図 3 に、Web パーツが生成するドキュメントを 図 4 に示します。


図 3 ユーザーがテンプレート ドキュメントを選択できる SharePoint Web パーツ

Open XML コンテンツ コントロール

SharePoint ソリューションの説明に入る前に、Open XML コンテンツ コントロールの基礎について説明しておきます。Open XML コンテンツ コントロールは、ワード プロセッサ ドキュメントに 1 つの機能を提供します。ユーザーはこの機能を利用して、コンテンツの内容を詳しく記述したり、そのコンテンツにメタデータを関連付けることができます。コンテンツ コントロールを使用するには、Microsoft Office Word 2007 で [開発] タブを有効にする必要があります (Office メニューの [Word のオプション] をクリックしてから、[Word のオプション] ダイアログ ボックスで [[開発] タブをリボンに表示する] チェック ボックスをオンにします)。

コンテンツ コントロールを挿入するには、テキストの一部を選択してから、図 5 に示すように、[開発] タブの [コントロール] 領域にある、テキスト コンテンツ コントロールを作成できるボタンをクリックします。


図 4 生成したレポートを含む Open XML ワード プロセッサ ドキュメント


図 5 テキスト コンテンツ コントロールを作成するボタンの使用

コンテンツ コントロールではプロパティを設定して、タイトルに指定したり、タグに割り当てたりすることができます。プロパティを設定するには、コンテンツ コントロールの内部をクリックしてから、[開発] タブの [コントロール] 領域にある [プロパティ] をクリックします。その結果、タイトルやタグの設定に使用できるダイアログ ボックスが表示されます。

コンテンツ コントロールは、図 6 に示すように、Open XML マークアップの w:sdt 要素を使用します。コンテンツ コントロールの内容は、w:sdtContent 要素で定義します。この図では、w:alias 要素でコンテンツ コントロールのタイトルが、w:tag 要素でコンテンツ コントロールのタグがそれぞれ設定されていることも確認できます。

.NET Framework を使用した Open XML のプログラミング

Microsoft .NET Framework を使用して Open XML のプログラミングを行う場合、さまざまな方法があります。

  • System.IO.Packaging のクラスを使用する
  • XmlDocument、XmlParser、LINQ to XML といった .NET で使用できる XML プログラミング テクノロジを備えた Open XML SDK を使用する(お勧めは LINQ to XML です)。
  • Open XML SDK バージョン 2.0 の厳密に型指定されるオブジェクト モデルを使用する(このオブジェクト モデルを使ったプログラミング方法を紹介する記事は数多く見つかります)。

ここでは、LINQ to XML を含む Open XML SDK (バージョン 1.0 または 2.0) を使用します。Open XML SDK (英語版) は go.microsoft.com/fwlink/?LinkId=127912 からダウンロードできます。

コンテンツ コントロール関連の Open XML 機能の一部を ContentControlManager クラスにカプセル化すると便利です。この方法を使って問題に対処するときは、簡単なコンソール アプリケーションでカプセル化する Open XML 機能を開発できます。Open XML 機能のコーディングとデバッグが完了したら、ほとんど作業を必要としないで、その機能を SharePoint 機能に組み込むことができます。Open XML コードのデバッグ中に SharPoint 機能の配置すると、配置のオーバーヘッドにより若干時間がかかります。

ここで紹介する SharePoint ドキュメント生成のサンプルでは、特定のドキュメント ライブラリからテンプレート ドキュメントを取得するコード、コンテンツ コントロールに保持するドキュメントを照会するコード、および適切な SharePoint リストからのデータをドキュメントに設定するために、各コンテンツ コントロールに格納されたメタデータを使用するコードを作成します。

サンプルをダウンロードしてテンプレート Open XML ドキュメントを調べると、各表を囲む 1 つのコンテンツ コントロールと、各表の最終行の各セルに挿入された複数のコンテンツ コントロールを確認できます。表のセルに含まれる各コンテンツ コントロールのタグは、SharePoint リストの列名を指定します。わかりやすくするために、各コンテンツ コントロールのタイトルにはタグと同じ値を設定しています。コンテンツ コントロールは、挿入ポイントがコントロール内部にあるときにこのタイトルを表示します。

Open XML ドキュメントを生成する SharePoint 機能をコーディングするときは、まず、これらのコントロール コンテンツにドキュメントを照会するコードから作成します。照会の結果として、そのコンテンツ コントロールの構造を記述する XML ツリーが、各コントロールのタグと共に返されます。サンプル ドキュメントでこのコードを実行すると、次の XML が作成されます。

<ContentControls>

  <Table Name="Team Members">

    <Field Name="TeamMemberName" />

    <Field Name="Role" />

  </Table>

  <Table Name="Item List">

    <Field Name="ItemName" />

    <Field Name="Description" />

    <Field Name="EstimatedHours" />

    <Field Name="AssignedTo" />

  </Table>

</ContentControls>

この XML ドキュメントでは、コードから照会する必要のある SharePoint リストが示されます。リスト内の項目ごとに、特定の列の値を取得する必要があります。図 7 に示す Open XML ワード プロセッサ ドキュメントを紹介するコードは、返された XML を形成するための機能構成を使用する LINQ to XML クエリとして作成されています。

機能構成を使用するには、コンストラクターを使用して XElement オブジェクトのインスタンスを作成します。このとき、コンストラクターには引数として LINQ to XML クエリを渡します。LINQ to XML クエリは軸メソッドを使用して適切な要素をドキュメントの本文に取得し、Enumerable.Select 拡張メソッドを使用して、クエリの結果から新たな XML を形成します。機能構成を理解するにはいささか学習が必要ですが、少し頭をめぐらせれば、ほんの少量のコードで実にたくさんのことを行えることがお分かりになるでしょう。

図 6 コンテンツ コントロールの Open XML マークアップ

<w:p>

<w:r>

<w:t xml:space="preserve">Not in content control. </w:t>

</w:r>

<w:sdt>

<w:sdtPr>

<w:alias w:val="Test"/>

<w:tag w:val="Test"/>

<w:id w:val="5118254"/>

<w:placeholder>

<w:docPart w:val="DefaultPlaceholder_22675703"/>

</w:placeholder>

</w:sdtPr>

<w:sdtContent>

<w:r>

<w:t>This is text in content control.</w:t>

</w:r>

</w:sdtContent>

</w:sdt>

<w:r>

<w:t xml:space="preserve"> Not in content control.</w:t>

</w:r>

</w:p>

XName オブジェクトと XNamespace オブジェクトの事前アトミック化

図 7 のコードでは、LINQ to XML の名前の "事前アトミック化" というアプローチを使用しています。これはやや奇妙な表現ですが、使用する要素や属性の修飾名に初期化される静的フィールドを含む静的クラス (図 8 参照) を作成することを表しています。

図 7 テンプレート ドキュメントのコンテンツ コントロール構造の取得

public static XElement GetContentControls(

WordprocessingDocument document)

{

XElement contentControls = new XElement("ContentControls",

document

.MainDocumentPart

.GetXDocument()

.Root

.Element(W.body)

.Elements(W.sdt)

.Select(tableContentControl =>

new XElement("Table",

new XAttribute("Name", (string)tableContentControl

.Element(W.sdtPr).Element(W.tag).Attribute(

W.val)),

tableContentControl

.Descendants(W.sdt)

.Select(fieldContentControl =>

new XElement("Field",

new XAttribute("Name",

(string)fieldContentControl

.Element(W.sdtPr)

.Element(W.tag)

.Attribute(W.val)

)

)

)

)

)

);

return contentControls;

}

この方法で、XName オブジェクトと XNamespace オブジェクトを初期化することにはそれなりの理由があります。LINQ to XML では XML の名前と名前空間をそれぞれ System.Xml.Linq.XName と System.Xml.Linq.XNamespace という 2 つのクラスに抽象化します。この 2 つのクラスのセマンティクスには、2 つの XName の修飾名 (名前空間 + ローカル名) が同じであれば、同じオブジェクトで表現されるという考え方があります。そのため、XName オブジェクトを高速に比較できます。特定の名前の XElement オブジェクトを選択する際に、コードでは文字列を比較するのではなく、オブジェクトを比較するだけです。XName オブジェクトを初期化するとき、LINQ to XML ではまずキャッシュ内を調べ、名前空間と名前が同じ XName オブジェクトが既に存在するかどうかを判断します。存在する場合、そのオブジェクトはキャッシュ内の既存の XName オブジェクトに初期化されます。存在しなければ、LINQ to XML によって新たなオブジェクトが初期化され、キャッシュに追加されます。ご想像のとおり、このプロセスが何回も繰り返されると、パフォーマンスの問題が生じる可能性があります。静的クラス内でオブジェクトを初期化することにより、このプロセスは 1 回しか実行されません。また、この手法を使用すれば、コード本体内で要素名や属性名を記述することによるスペル間違いの可能性が減少します。この手法を使用するもう 1 つのメリットは、IntelliSense のサポートが得られるため、LINQ to XML を使用する Open XML のプログラミングが容易になることです。

図 8 XName オブジェクトと XNamespace オブジェクトを事前アトミック化するための静的フィールドを含む静的クラス

public static class W

{

public static XNamespace w =

"https://schemas.openxmlformats.org/wordprocessingml/2006/main";

public static XName body = w + "body";

public static XName sdt = w + "sdt";

public static XName sdtPr = w + "sdtPr";

public static XName tag = w + "tag";

public static XName val = w + "val";

public static XName sdtContent = w + "sdtContent";

public static XName tbl = w + "tbl";

public static XName tr = w + "tr";

public static XName tc = w + "tc";

public static XName p = w + "p";

public static XName r = w + "r";

public static XName t = w + "t";

public static XName rPr = w + "rPr";

public static XName highlight = w + "highlight";

public static XName pPr = w + "pPr";

public static XName color = w + "color";

public static XName sz = w + "sz";

public static XName szCs = w + "szCs";

}

拡張メソッドの GetXDocument と PutXDocument

このコラムで紹介するサンプルには、プログラミングを容易にし、パフォーマンスを向上するための、ちょっとした技法がもう 1 つ使用されています。Open XML SDK には、ドキュメントの一部として注釈を配置する機能があります。つまり、任意の .NET Framework オブジェクトを OpenXmlPart オブジェクトにアタッチしておき、後からアタッチしたオブジェクトの型を指定して取得することができます。

ここでは 2 つの拡張メソッド GetXDocument と PutXDocument を定義します。これらの拡張メソッドは、注釈を使用して、OpenXmlPart から XML をシリアル化解除する処理を最小限に抑えます。GetXDocument が呼び出されると、まず、XDocument 型の注釈が OpenXmlPart に存在するかどうかを調べます。注釈が存在すれば、それを返します。存在しなければ、OpenXmlPart から XDocument を設定し、OpenXmlPart に注釈をつけて、新しい XDocument を返します。

PutXDocument 拡張メソッドでも、XDocument 型の注釈が存在するかどうかを調べます。注釈が存在すると、(GetXDocument を呼び出した後に変更されていると想定して) XDocument を OpenXMLPart に書き戻します。この拡張メソッドの GetXDocument と PutXDocument を図 9 に示します。図 7 で示した GetContentControls メソッドで GetXDocument 拡張メソッドを使用しているのがわかります。

図 9 XML のシリアル化解除を最小限に抑えるために Open XML SDK の注釈を使用する拡張メソッド

public static class AssembleDocumentLocalExtensions

{

public static XDocument GetXDocument(this OpenXmlPart part)

{

XDocument xdoc = part.Annotation<XDocument>();

if (xdoc != null)

return xdoc;

using (Stream str = part.GetStream())

using (StreamReader streamReader = new StreamReader(str))

using (XmlReader xr = XmlReader.Create(streamReader))

xdoc = XDocument.Load(xr);

part.AddAnnotation(xdoc);

return xdoc;

}

public static void PutXDocument(this OpenXmlPart part)

{

XDocument xdoc = part.GetXDocument();

if (xdoc != null)

{

// Serialize the XDocument object back to the package.

using (XmlWriter xw =

XmlWriter.Create(part.GetStream

(FileMode.Create, FileAccess.Write)))

{

xdoc.Save(xw);

}

}

}

}

コンテンツ コントロールからデータへの置換

これで、表とセル内のコンテンツ コントロールの構造を返すメソッドができました。次は、SharePoint リストから取得し、表に挿入する特定のデータを含む Open XML ドキュメントを作成するメソッド (SetContentControls) が必要です。ここで定義するこのメソッドは、引数として XML ツリーを受け取ります。この XML ツリーを 図 10 に、この XML ツリーを渡したときに SetContentControls によって作成されるドキュメントを 図 11 に示します。

図 10 ワード プロセッサ ドキュメントの表に挿入するデータを含む XML ツリー

<ContentControls>

<Table Name="Team Members">

<Field Name="TeamMemberName" />

<Field Name="Role" />

<Row>

<Field Name="TeamMemberName" Value="Bob" />

<Field Name="Role" Value="Developer" />

</Row>

<Row>

<Field Name="TeamMemberName" Value="Susan" />

<Field Name="Role" Value="Program Manager" />

</Row>

<Row>

<Field Name="TeamMemberName" Value="Jack" />

<Field Name="Role" Value="Test" />

</Row>

</Table>

<Table Name="Item List">

<Field Name="ItemName" />

<Field Name="Description" />

<Field Name="EstimatedHours" />

<Field Name="AssignedTo" />

<Row>

<Field Name="ItemName" Value="Learn SharePoint 2010" />

<Field Name="Description" Value="This should be fun!" />

<Field Name="EstimatedHours" Value="80" />

<Field Name="AssignedTo" Value=”All” />

</Row>

<Row>

<Field Name="ItemName" Value=

"Finalize Import Module Specification" />

<Field Name="Description" Value="Make sure to handle all document

formats." />

<Field Name="EstimatedHours" Value="35" />

<Field Name="AssignedTo" Value=”Susan" />

</Row>

<Row>

<Field Name="ItemName" Value="Write Test Plan" />

<Field Name=”Description" Value=

"Include regression testing items." />

<Field Name="EstimatedHours" Value="20" />

<Field Name="AssignedTo" Value="Jack" />

</Row>

</Table>

</ContentControls>


図 11 生成後のドキュメント

コンテンツ コントロールを含んでいた 1 行が、メソッドに引数として渡された XML ツリーからのデータをそれぞれ含む複数の行に置き換えられているのがわかります。XML ツリーを使用して Open XML マークアップを操作するコードにデータを渡すことで、SharePoint オブジェクト モデルを使用するコードと Open XML コードを適切に分離することができます。

新しいドキュメントを組み立てるコードでは、ユーザーが表に適用している書式をそのまま使用します。たとえば、各行に交互に異なる色が割り当てられるように表を構成した場合、あるいは列に背景色を設定した場合、新しく生成されるドキュメントにはその書式変更が適用されます。

ContentControlManager サンプルをダウンロードして調べてみれば、コンテンツ コントロールを含む行のコピーを取得して、それをプロトタイプ行として保存しているコードがあるのがわかります。

// Determine the element for the row that contains the content controls.

// This is the prototype for the rows that the code will generate from data.

XElement prototypeRow = tableContentControl

    .Descendants(W.sdt)

    .Ancestors(W.tr)

    .FirstOrDefault();

このコードでは、次に、SharePoint リストから取得した項目ごとにプロトタイプ行のコピーを作成し、SharePoint リストからのデータでコピーした行を変更して、ドキュメントに挿入されるコレクションに追加しています。

新しい行のリストを作成したら、以下に示すように、リストからプロトタイプ行を削除し、新しく作成した行のコレクションを挿入します。

XElement tableElement = prototypeRow.Ancestors(W.tbl).First();

prototypeRow.Remove();

tableElement.Add(newRows);

SharePoint 機能の作成

このサンプルのビルドには、Windows SharePoint Services 3.0, v1.3 向けの Visual Studio 2008 拡張機能 (英語版) の 2009 年 2 月版 CTP リリースを使用しました。このサンプルは、WSS の 32 ビット版でも 64 ビット版でもビルドして実行しました(Kirk Evans が、これらの拡張機能の使用方法に関する優れた Web キャスト (英語) をいくつか公開しています)。

サンプルには、Web パーツのコントロールを作成するコードが含まれています。SharePoint Web パーツをビルドした経験があれば、コードはかなりわかりやすいものです。ユーザーが [Generate Report] をクリックすると、コードから CreateReport メソッドが呼び出されます。このメソッドでは、コンテンツ コントロールのタグで構成されているとおりに SharePoint リストのデータを使用して、テンプレート ドキュメントから新しい Open XML ワード プロセッサ ドキュメントを組み立てます。CreateReport メソッドのコードについて注意すべき点がいくつかあります。SharePoint のドキュメント ライブラリに含まれるファイルは、バイト配列として返されます。Open XML SDK を使用してドキュメントを開き、変更できるように、このバイト配列をメモリ ストリームに変換する必要があります。MemoryStream のコンストラクターの 1 つにバイト配列を受け取るものがあるため、このコンストラクターを使用することを考えるでしょう。しかし、このコンストラクターで作成されるメモリ ストリームはサイズ変更できません。Open XML SDK では、サイズ変更可能なメモリ ストリームが必要です。解決策は、図 12 に示すように、既定のコンストラクターで MemoryStream を作成後、SharePoint からのバイト配列を作成した MemoryStream に書き込むことです。

図 12 SharePoint からのバイト配列の MemoryStream への書き込み

private ModifyDocumentResults CreateReport(SPFile file, Label message)

{

byte[] byteArray = file.OpenBinary();

using (MemoryStream mem = new MemoryStream())

{

mem.Write(byteArray, 0, (int)byteArray.Length);

try

{

using (WordprocessingDocument wordDoc =

WordprocessingDocument.Open(mem, true))

{

// Get the content control structure from the template

// document.

XElement contentControlStructure =

ContentControlManager.GetContentControls(wordDoc);

// Retrive data from SharePoint,

constructing the XML tree to

// pass to the ContentControlManager.SetContentControls

// method.

...

}

}

}

}

残りのコードは簡単です。SharePoint オブジェクト モデルを使用して、ドキュメント ライブラリとライブラリのコンテンツを取得し、リストを取得して、リスト内の行ごとに列の値を取得します。次に、ContentControlManager.SetContentControls に渡す XML ツリーを組み立ててから、SetContentControls を呼び出します。

コードでは、生成するレポート ドキュメントの名前を Report-yyyy-mm-dd の形式で組み立てます。同じ名前のレポートが既に存在する場合は、既存の生成済みレポートと混同しないように、レポート名に番号を付加します。たとえば、Report-2009-08-01.docx が既に存在すれば、Report-2009-8-2 (1).docx にレポートを書き込みます。

容易なカスタマイズ

おそらく、独自のニーズに合うようにこのサンプルをカスタマイズしたいと考えるでしょう。考えられる機能拡張の 1 つは、テンプレート ドキュメントの本文に含まれるコンテンツ コントロール用に、SharePoint に格納された特定のドキュメントから定型コンテンツを取り出せるようにすることです。この場合、定型テキストを含むドキュメントの名前をコンテンツ テキストにテキストとして配置するようなコードを作成できます。

また、このサンプルでは、TemplateReports と Reports ドキュメント ライブラリの名前をハードコーディングしています。この情報を SharePoint リストで指定すれば、こうした制約を取り除くことができます。コードで認識する必要があるのはこの構成リストの名前のみになります。TemplateReports と Reports ドキュメント ライブラリの名前は、構成リスト内のデータから決定されます。

SharePoint は、組織のメンバーの共同作業を容易にする優れたテクノロジです。Open XML は、ドキュメントの生成方法を変える新たなテクノロジです。この 2 つのテクノロジを組み合わせて使用すると、メンバーがドキュメントを使用して新たな方法で共同作業できるアプリケーションを作成できます。

Eric White は、Office Open XML ファイル形式、Office、および SharePoint を専門とするマイクロソフトのライターです。2005 年にマイクロソフトに入社する前は、クロスプラットフォーム グリッド ウィジェットの開発と販売を行っている PowerVista Software 社を皮切りに、長年にわたって開発者として活躍していました。カスタム コントロールや GDI+ 開発に関する書籍を執筆しています。blogs.msdn.com/ericwhite で彼のブログをご覧いただけます。