Procedimiento para usar anotaciones para transformar árboles LINQ to XML en un estilo XSLT (LINQ to XML)
Se puede usar anotaciones para facilitar transformaciones de un árbol XML.
Algunos documentos XML están "centrados en documentos e incluyen contenido mixto". Con estos documentos, no necesariamente conoce la forma de los nodos secundarios de un elemento. Por ejemplo, un nodo que contiene texto puede tener el siguiente aspecto:
<text>A phrase with <b>bold</b> and <i>italic</i> text.</text>
Para cualquier nodo de texto dado puede haber cualquier número de elementos secundarios <b>
y <i>
. Este enfoque se extiende a otras situaciones diferentes, por ejemplo páginas que contienen diversos elementos secundarios, como párrafos normales, párrafos con viñetas y mapas de bits. Las celdas de una tabla pueden contener texto, listas desplegables o mapas de bits. Una de las características principal del XML centrado en documentos es que no se sabe qué elemento secundario puede tener cualquier elemento particular.
Si desea transformar elementos de un árbol en el que no conoce el elemento secundario de los elementos que desea transformar, entonces este enfoque que usa anotaciones en un enfoque efectivo.
El resumen del enfoque es:
- En primer lugar, anote elementos en el árbol con un elemento de sustitución.
- En segundo lugar, recorra en iteración todo el árbol, creando un nuevo árbol en el que se reemplaza cada elemento son su anotación. En los ejemplos de este artículo se implementa la iteración y la creación de un nuevo árbol de una función denominada
XForm
.
En detalle, el enfoque consiste en:
- Ejecutar una o más consultas LINQ to XML que devuelvan el conjunto de elementos que desea transformar de una forma a otra. Para cada elemento de la consulta, agregue un nuevo objeto XElement como anotación al elemento. Este nuevo elemento sustituirá el elemento anotado en el nuevo árbol transformado. Es un código sencillo de escribir, tal como lo demuestra el ejemplo.
- El nuevo elemento que se agrega como anotación puede contener nodos secundarios; puede formar un subárbol con cualquier forma que se desee.
- Existe una regla especial; si un nodo secundario del nuevo elemento está en un espacio de nombres diferente, un espacio de nombres creado con esa finalidad (en este ejemplo, el espacio de nombres es
http://www.microsoft.com/LinqToXmlTransform/2007
), entonces el elemento secundario no se copia al nuevo árbol. En su lugar, si el espacio de nombres es el espacio de nombres especial mencionado anteriormente y el nombre local del elemento esApplyTransforms
, los nodos secundarios del elemento del árbol de origen se recorren en iteración y se copian al nuevo árbol (con la excepción que los elementos secundarios anotados se transforman de acuerdo con esas reglas). - Esto es parecido a la especificación de transformaciones en XSL. La consulta que selecciona un conjunto de nodos es análoga a la expresión XPath para una plantilla. El código para crear el nuevo XElement que se guarda como anotación es análogo al constructor de secuencias en XSL y el elemento
ApplyTransforms
es análogo en función al elementoxsl:apply-templates
en XSL. - Una ventaja de este enfoque es que al formular consultas siempre se escriben consultas en el árbol de origen sin modificar. No hay que preocuparse de cómo afectan a las consultas que se están escribiendo las modificaciones del árbol.
Ejemplo: Cambio de nombre de todos los nodos de párrafo
En este ejemplo se cambia el nombre de todos los nodos Paragraph
a 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
Este ejemplo produce el siguiente resultado:
<Root>
<para>This is a sentence with <b>bold</b> and <i>italic</i> text.</para>
<para>More text.</para>
</Root>
Ejemplo: Calculo de promedios y sumas, e incorporación de los resultados como nuevos elementos al árbol
El siguiente ejemplo calcula la media y la suma de los elementos Data
y los agrega como nuevos elementos al árbol.
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
Este ejemplo produce el siguiente resultado:
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>
Ejemplo: Creación de un árbol transformado a partir del árbol original anotado
Una pequeña función, XForm
, crea un nuevo árbol transformado a partir del árbol original anotado. A continuación se muestra el pseudocódigo de esta función:
La función toma un XElement como argumento y devuelve un XElement.
Si un elemento tiene una anotación XElement, el valor de XElement devuelto tiene estas características:
- El nombre del nuevo XElement es el nombre del elemento de anotación.
- Todos los atributos se copian de la anotación en el nuevo nodo.
- Todos los nodos secundarios se copian de la anotación, con la excepción de que se reconoce el nodo especial xf:ApplyTransforms y los nodos secundarios del elemento de origen se iteran. Si el nodo secundario de origen no es un XElement, se copia en el nuevo árbol. Si el elemento secundario de origen es un XElement, se transforma llamando a esta función de forma recursiva.
De lo contrario, el XElement devuelto tiene estas características:
- El nombre del nuevo XElement es el nombre del elemento de origen.
- Todos los atributos se copian del elemento de origen al de destino.
- Todos los nodos secundarios se copian del elemento de origen.
- Si el nodo secundario de origen no es un XElement, se copia en el nuevo árbol. Si el elemento secundario de origen es un XElement, se transforma llamando a esta función de forma recursiva.
El código siguiente es para esta función:
// 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
Ejemplo: Mostrar XForm
en usos típicos de este tipo de transformación
En el ejemplo siguiente se incluye la función XForm
y algunos de los usos típicos de este tipo de transformación:
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
Este ejemplo produce el siguiente resultado:
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>