注釈を使用して XSLT スタイルの LINQ to XML ツリーを変換する (LINQ to XML)
注釈を使用することで、XML ツリーの変換が容易になります。
XML ドキュメントには、"ドキュメント中心の混合コンテンツ" があります。このようなドキュメントでは、必ずしも要素の子ノードの構造を把握する必要はありません。 たとえば、テキストを含んでいるノードは次のようになります。
<text>A phrase with <b>bold</b> and <i>italic</i> text.</text>
どのテキスト ノードにも、任意の数の <b>
と <i>
が子要素として存在する可能性があります。 この方法は、通常の段落、箇条書きの段落、およびビットマップのさまざまな子要素を含むページなどのその他の多数の状況におよびます。 テーブルのセルには、テキスト、ドロップダウン リスト、またはビットマップが含まれている場合があります。 ドキュメント中心の XML の主要な特性の 1 つは、特定の要素がどの子要素を持つかがわからない点です。
ツリー内の要素を変換するとき、その要素の子について詳しく理解している必要がない場合は、注釈を使用するこの方法が効果的です。
この方法の概要は次のとおりです。
- 最初に、ツリー内の要素に置換要素を使用して注釈を付けます。
- 2 番目に、ツリー全体を反復処理して、各要素をその注釈で置換する新しいツリーを作成します。 この記事の例では、
XForm
という名前の関数で新しいツリーの反復処理と作成を実装しています。
この方法の詳細な構成は次のとおりです。
- 構造を変換する一連の要素を返す 1 つ以上の LINQ to XML クエリを実行します。 クエリ内の要素ごとに、新しい XElement オブジェクトをその要素に対する注釈として追加します。 変換後の新しいツリーでは、注釈付きの要素がこの新しい要素で置き換えられます。 例で示すように、このコードは簡単に記述できます。
- 注釈として追加する新しい要素には、新しい子ノードを含めることができます。これでは、希望の構造のサブツリーを形成できます。
- 新しい要素の子ノードが、この目的のために作成された別の名前空間にある場合 (この例で名前空間は
http://www.microsoft.com/LinqToXmlTransform/2007
)、その子要素は新しいツリーにはコピーされないという特別な規則があります。 名前空間が上記の特別な名前空間で、かつ要素のローカル名がApplyTransforms
である場合は、代わりに、ソース ツリー内の要素の子ノードは反復処理され、新しいツリーにコピーされます (例外として、注釈付きの子要素自体はここで示す規則に従って変換されます)。 - これは、XSL での変換の仕様にある程度似ています。 一連のノードを選択するクエリは、テンプレートの XPath 式に似ています。 注釈として保存される新しい XElement を作成するコードは、XSL のシーケンス コンストラクターに似ています。また、
ApplyTransforms
要素は、XSL のxsl:apply-templates
要素と機能的に似ています。 - この方法の利点の 1 つは、クエリを作成するときに、常に未変更のソース ツリーに対してクエリを記述する点です。 ツリーに対する変更が記述中のクエリに与える影響を考慮する必要がありません。
例: すべての段落ノードを名前変更する
この例では、すべての Paragraph
ノードが para
に変更されます。
XNamespace xf = "http://www.microsoft.com/LinqToXmlTransform/2007";
XName at = xf + "ApplyTransforms";
XElement root = XElement.Parse(@"
<Root>
<Paragraph>This is a sentence with <b>bold</b> and <i>italic</i> text.</Paragraph>
<Paragraph>More text.</Paragraph>
</Root>");
// replace Paragraph with para
foreach (var el in root.Descendants("Paragraph"))
el.AddAnnotation(
new XElement("para",
// same idea as xsl:apply-templates
new XElement(xf + "ApplyTransforms")
)
);
// The XForm method, shown later in this article, accomplishes the transform
XElement newRoot = XForm(root);
Console.WriteLine(newRoot);
Imports <xmlns:xf="http://www.microsoft.com/LinqToXmlTransform/2007">
Module Module1
Dim at As XName = GetXmlNamespace(xf) + "ApplyTransforms"
Sub Main()
Dim root As XElement = _
<Root>
<Paragraph>This is a sentence with <b>bold</b> and <i>italic</i> text.</Paragraph>
<Paragraph>More text.</Paragraph>
</Root>
' Replace Paragraph with p.
For Each el In root...<Paragraph>
' same idea as xsl:apply-templates
el.AddAnnotation( _
<para>
<<%= at %>></>
</para>)
Next
' The XForm function, shown later in this article, accomplishes the transform
Dim newRoot As XElement = XForm(root)
Console.WriteLine(newRoot)
End Sub
End Module
この例を実行すると、次の出力が生成されます。
<Root>
<para>This is a sentence with <b>bold</b> and <i>italic</i> text.</para>
<para>More text.</para>
</Root>
例: 平均と合計を計算し、それらを新しい要素としてツリーに追加する
次の例では、Data
要素の平均と合計を計算し、それらをツリーに新しい要素として追加します。
XNamespace xf = "http://www.microsoft.com/LinqToXmlTransform/2007";
XName at = xf + "ApplyTransforms";
XElement data = new XElement("Root",
new XElement("Data", 20),
new XElement("Data", 10),
new XElement("Data", 3)
);
// while adding annotations, you can query the source tree all you want,
// as the tree isn't mutated while annotating.
var avg = data.Elements("Data").Select(z => (Decimal)z).Average();
data.AddAnnotation(
new XElement("Root",
new XElement(xf + "ApplyTransforms"),
new XElement("Average", $"{avg:F4}"),
new XElement("Sum",
data
.Elements("Data")
.Select(z => (int)z)
.Sum()
)
)
);
Console.WriteLine("Before Transform");
Console.WriteLine("----------------");
Console.WriteLine(data);
Console.WriteLine();
Console.WriteLine();
// The XForm method, shown later in this article, accomplishes the transform
XElement newData = XForm(data);
Console.WriteLine("After Transform");
Console.WriteLine("----------------");
Console.WriteLine(newData);
Imports <xmlns:xf="http://www.microsoft.com/LinqToXmlTransform/2007">
Module Module1
Dim at As XName = GetXmlNamespace(xf) + "ApplyTransforms"
Sub Main()
Dim data As XElement = _
<Root>
<Data>20</Data>
<Data>10</Data>
<Data>3</Data>
</Root>
' While adding annotations, you can query the source tree all you want,
' as the tree isn't mutated while annotating.
data.AddAnnotation( _
<Root>
<<%= at %>/>
<Average>
<%= _
String.Format("{0:F4}", _
data.Elements("Data") _
.Select(Function(z) CDec(z)).Average()) _
%>
</Average>
<Sum>
<%= _
data.Elements("Data").Select(Function(z) CInt(z)).Sum() _
%>
</Sum>
</Root> _
)
Console.WriteLine("Before Transform")
Console.WriteLine("----------------")
Console.WriteLine(data)
Console.WriteLine(vbNewLine)
' The XForm function, shown later in this article, accomplishes the transform
Dim newData As XElement = XForm(data)
Console.WriteLine("After Transform")
Console.WriteLine("----------------")
Console.WriteLine(newData)
End Sub
End Module
この例を実行すると、次の出力が生成されます。
Before Transform
----------------
<Root>
<Data>20</Data>
<Data>10</Data>
<Data>3</Data>
</Root>
After Transform
----------------
<Root>
<Data>20</Data>
<Data>10</Data>
<Data>3</Data>
<Average>11.0000</Average>
<Sum>33</Sum>
</Root>
例: 元の注釈付きツリーから変換された新しいツリーを作成する
小さな関数 XForm
によって、元の注釈付きツリーから変換された新しいツリーが作成されます。 この関数の擬似コードは次のとおりです。
この関数は、引数として XElement を受け取り、XElement を返します。
要素に XElement の注釈がある場合、返される XElement には次の特性があります。
- 新しい XElement の名前は、注釈要素の名前です。
- すべての属性が注釈から新しいノードにコピーされます。
- すべての子ノードが注釈からコピーされます (特殊なノード xf:ApplyTransforms が認識されて、ソース要素の子ノードが反復処理される場合を除く)。 ソースの子ノードが XElement ではない場合、それが新しいツリーにコピーされます。 ソースの子が XElement である場合、それは、この関数を再帰的に呼び出すことによって変換されます。
それ以外の場合、返される XElement には次の特性があります。
- 新しい XElement の名前は、ソース要素の名前です。
- すべての属性がソース要素からターゲットの要素にコピーされます。
- すべての子ノードがソース要素からコピーされます。
- ソースの子ノードが XElement ではない場合、それが新しいツリーにコピーされます。 ソースの子が XElement である場合、それは、この関数を再帰的に呼び出すことによって変換されます。
この関数のコードは次のとおりです。
// Build a transformed XML tree per the annotations
static XElement XForm(XElement source)
{
XNamespace xf = "http://www.microsoft.com/LinqToXmlTransform/2007";
XName at = xf + "ApplyTransforms";
if (source.Annotation<XElement>() != null)
{
XElement anno = source.Annotation<XElement>();
return new XElement(anno.Name,
anno.Attributes(),
anno
.Nodes()
.Select(
(XNode n) =>
{
XElement annoEl = n as XElement;
if (annoEl != null)
{
if (annoEl.Name == at)
return (object)(
source.Nodes()
.Select(
(XNode n2) =>
{
XElement e2 = n2 as XElement;
if (e2 == null)
return n2;
else
return XForm(e2);
}
)
);
else
return n;
}
else
return n;
}
)
);
}
else
{
return new XElement(source.Name,
source.Attributes(),
source
.Nodes()
.Select(n =>
{
XElement el = n as XElement;
if (el == null)
return n;
else
return XForm(el);
}
)
);
}
}
' Build a transformed XML tree per the annotations.
Function XForm(ByVal source As XElement) As XElement
If source.Annotation(Of XElement)() IsNot Nothing Then
Dim anno As XElement = source.Annotation(Of XElement)()
Return _
<<%= anno.Name.ToString() %>>
<%= anno.Attributes() %>
<%= anno.Nodes().Select(Function(n As XNode) _
GetSubNodes(n, source)) %>
</>
Else
Return _
<<%= source.Name %>>
<%= source.Attributes() %>
<%= source.Nodes().Select(Function(n) GetExpandedNodes(n)) %>
</>
End If
End Function
Private Function GetSubNodes(ByVal n As XNode, ByVal s As XElement) As Object
Dim annoEl As XElement = TryCast(n, XElement)
If annoEl IsNot Nothing Then
If annoEl.Name = at Then
Return s.Nodes().Select(Function(n2 As XNode) GetExpandedNodes(n2))
End If
End If
Return n
End Function
Private Function GetExpandedNodes(ByVal n2 As XNode) As XNode
Dim e2 As XElement = TryCast(n2, XElement)
If e2 Is Nothing Then
Return n2
Else
Return XForm(e2)
End If
End Function
例: この種の変換における XForm
の一般的な使用方法
次の例では、XForm
関数と、この種の変換の一般的な使用例をいくつか示します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
class Program
{
static XNamespace xf = "http://www.microsoft.com/LinqToXmlTransform/2007";
static XName at = xf + "ApplyTransforms";
// Build a transformed XML tree per the annotations
static XElement XForm(XElement source)
{
if (source.Annotation<XElement>() != null)
{
XElement anno = source.Annotation<XElement>();
return new XElement(anno.Name,
anno.Attributes(),
anno
.Nodes()
.Select(
(XNode n) =>
{
XElement annoEl = n as XElement;
if (annoEl != null)
{
if (annoEl.Name == at)
return (object)(
source.Nodes()
.Select(
(XNode n2) =>
{
XElement e2 = n2 as XElement;
if (e2 == null)
return n2;
else
return XForm(e2);
}
)
);
else
return n;
}
else
return n;
}
)
);
}
else
{
return new XElement(source.Name,
source.Attributes(),
source
.Nodes()
.Select(n =>
{
XElement el = n as XElement;
if (el == null)
return n;
else
return XForm(el);
}
)
);
}
}
static void Main(string[] args)
{
XElement root = new XElement("Root",
new XComment("A comment"),
new XAttribute("Att1", 123),
new XElement("Child", 1),
new XElement("Child", 2),
new XElement("Other",
new XElement("GC", 3),
new XElement("GC", 4)
),
XElement.Parse(
"<SomeMixedContent>This is <i>an</i> element that " +
"<b>has</b> some mixed content</SomeMixedContent>"),
new XElement("AnUnchangedElement", 42)
);
// each of the following serves the same semantic purpose as
// XSLT templates and sequence constructors
// replace Child with NewChild
foreach (var el in root.Elements("Child"))
el.AddAnnotation(new XElement("NewChild", (string)el));
// replace first GC with GrandChild, add an attribute
foreach (var el in root.Descendants("GC").Take(1))
el.AddAnnotation(
new XElement("GrandChild",
new XAttribute("ANewAttribute", 999),
(string)el
)
);
// replace Other with NewOther, add new child elements around original content
foreach (var el in root.Elements("Other"))
el.AddAnnotation(
new XElement("NewOther",
new XElement("MyNewChild", 1),
// same idea as xsl:apply-templates
new XElement(xf + "ApplyTransforms"),
new XElement("ChildThatComesAfter")
)
);
// change name of element that has mixed content
root.Descendants("SomeMixedContent").First().AddAnnotation(
new XElement("MixedContent",
new XElement(xf + "ApplyTransforms")
)
);
// replace <b> with <Bold>
foreach (var el in root.Descendants("b"))
el.AddAnnotation(
new XElement("Bold",
new XElement(xf + "ApplyTransforms")
)
);
// replace <i> with <Italic>
foreach (var el in root.Descendants("i"))
el.AddAnnotation(
new XElement("Italic",
new XElement(xf + "ApplyTransforms")
)
);
Console.WriteLine("Before Transform");
Console.WriteLine("----------------");
Console.WriteLine(root);
Console.WriteLine();
Console.WriteLine();
XElement newRoot = XForm(root);
Console.WriteLine("After Transform");
Console.WriteLine("----------------");
Console.WriteLine(newRoot);
}
}
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Xml
Imports System.Xml.Linq
Imports <xmlns:xf="http://www.microsoft.com/LinqToXmlTransform/2007">
Module Module1
Dim at As XName = GetXmlNamespace(xf) + "ApplyTransforms"
' Build a transformed XML tree per the annotations.
Function XForm(ByVal source As XElement) As XElement
If source.Annotation(Of XElement)() IsNot Nothing Then
Dim anno As XElement = source.Annotation(Of XElement)()
Return _
<<%= anno.Name.ToString() %>>
<%= anno.Attributes() %>
<%= anno.Nodes().Select(Function(n As XNode) _
GetSubNodes(n, source)) %>
</>
Else
Return _
<<%= source.Name %>>
<%= source.Attributes() %>
<%= source.Nodes().Select(Function(n) GetExpandedNodes(n)) %>
</>
End If
End Function
Private Function GetSubNodes(ByVal n As XNode, ByVal s As XElement) As Object
Dim annoEl As XElement = TryCast(n, XElement)
If annoEl IsNot Nothing Then
If annoEl.Name = at Then
Return s.Nodes().Select(Function(n2 As XNode) GetExpandedNodes(n2))
End If
End If
Return n
End Function
Private Function GetExpandedNodes(ByVal n2 As XNode) As XNode
Dim e2 As XElement = TryCast(n2, XElement)
If e2 Is Nothing Then
Return n2
Else
Return XForm(e2)
End If
End Function
Sub Main()
Dim root As XElement = _
<Root Att1='123'>
<!--A comment-->
<Child>1</Child>
<Child>2</Child>
<Other>
<GC>3</GC>
<GC>4</GC>
</Other>
<SomeMixedContent>This is <i>an</i> element that <b>has</b> some mixed content</SomeMixedContent>
<AnUnchangedElement>42</AnUnchangedElement>
</Root>
' Each of the following serves the same semantic purpose as
' XSLT templates and sequence constructors.
' Replace Child with NewChild.
For Each el In root.<Child>
el.AddAnnotation(<NewChild><%= CStr(el) %></NewChild>)
Next
' Replace first GC with GrandChild, add an attribute.
For Each el In root...<GC>.Take(1)
el.AddAnnotation(<GrandChild ANewAttribute='999'><%= CStr(el) %></GrandChild>)
Next
' Replace Other with NewOther, add new child elements around original content.
For Each el In root.<Other>
el.AddAnnotation( _
<NewOther>
<MyNewChild>1</MyNewChild>
<<%= at %>></>
<ChildThatComesAfter/>
</NewOther>)
Next
' Change name of element that has mixed content.
root...<SomeMixedContent>(0).AddAnnotation( _
<MixedContent><<%= at %>></></MixedContent>)
' Replace <b> with <Bold>.
For Each el In root...<b>
el.AddAnnotation(<Bold><<%= at %>></></Bold>)
Next
' Replace <i> with <Italic>.
For Each el In root...<i>
el.AddAnnotation(<Italic><<%= at %>></></Italic>)
Next
Console.WriteLine("Before Transform")
Console.WriteLine("----------------")
Console.WriteLine(root)
Console.WriteLine(vbNewLine)
Dim newRoot As XElement = XForm(root)
Console.WriteLine("After Transform")
Console.WriteLine("----------------")
Console.WriteLine(newRoot)
End Sub
End Module
この例を実行すると、次の出力が生成されます。
Before Transform
----------------
<Root Att1="123">
<!--A comment-->
<Child>1</Child>
<Child>2</Child>
<Other>
<GC>3</GC>
<GC>4</GC>
</Other>
<SomeMixedContent>This is <i>an</i> element that <b>has</b> some mixed content</SomeMixedContent>
<AnUnchangedElement>42</AnUnchangedElement>
</Root>
After Transform
----------------
<Root Att1="123">
<!--A comment-->
<NewChild>1</NewChild>
<NewChild>2</NewChild>
<NewOther>
<MyNewChild>1</MyNewChild>
<GrandChild ANewAttribute="999">3</GrandChild>
<GC>4</GC>
<ChildThatComesAfter />
</NewOther>
<MixedContent>This is <Italic>an</Italic> element that <Bold>has</Bold> some mixed content</MixedContent>
<AnUnchangedElement>42</AnUnchangedElement>
</Root>
.NET