Ответ 1
Ключом к решению этой проблемы является запись рекурсивной функции, которая выполняет итерацию через дерево XML, записывая различные элементы и атрибуты специально созданным объектам XmlWriter. Существует внешний объект XmlWriter, который записывает отступы XML и внутренний объект XmlWriter, который записывает неизолированные XML.
Рекурсивная функция изначально использует "внешний" XmlWriter, записывая отступы XML, пока не увидит элемент TextBlock. Когда он сталкивается с элементом TextBlock, он создает "внутренний" объект XmlWriter, записывая ему дочерние элементы элемента TextBlock. Он также записывает пробел в "внутренний" XmlWriter.
Когда "внутренний" объект XmlWriter заканчивается написанием элемента TextBlock, текст, который написал автор, записывается в "внешний" XmlWriter с использованием метода WriteRaw.
Преимущества этого подхода заключаются в том, что пост-обработка XML отсутствует. Крайне сложно выполнить пост-обработку XML и быть уверенным, что вы правильно обрабатывали все случаи, включая произвольный текст в узлах CData и т.д. Весь XML написан с использованием только класса XmlWriter, тем самым гарантируя, что это всегда будет писать допустимый XML, Единственным исключением является специально созданное белое пространство, которое записывается с использованием метода WriteRaw, который обеспечивает желаемое поведение отступов.
Один из ключевых моментов заключается в том, что для внутреннего уровня XmlWriter для уровня соответствия соответствует значение ConformanceLevel.Fragment, потому что "внутренний" XmlWriter должен писать XML, который не имеет корневого элемента.
Для достижения желаемого форматирования элементов Run (т.е. смежные элементы Run не имеют между ними незначительного пробела), код использует метод расширения GroupAdjacent. Некоторое время назад я пишу сообщение в блоге метод расширения GroupAdjacent для VB.
Когда вы запускаете код с использованием указанного образца XML, он выводит:
<Canvas>
<Grid>
<TextBlock>
<Run Text="r" /><Run Text="u" /><Run Text="n" />
</TextBlock>
<TextBlock>
<Run Text="far a" /><Run Text="way" /><Run Text=" from me" />
</TextBlock>
</Grid>
<Grid>
<TextBlock>
<Run Text="I" /><Run Text=" " /><Run Text="want" />
<LineBreak />
</TextBlock>
<TextBlock>
<LineBreak />
<Run Text="...thi" /><Run Text="s to" />
<LineBreak />
<Run Text=" work" />
</TextBlock>
</Grid>
</Canvas>
Ниже приведен полный список примерной программы VB.NET. Кроме того, я написал сообщение в блоге Пользовательское форматирование XML с использованием LINQ to XML, в котором представлен эквивалентный код С#.
`
Imports System.Text
Imports System.Xml
Public Class GroupOfAdjacent(Of TElement, TKey)
Implements IEnumerable(Of TElement)
Private _key As TKey
Private _groupList As List(Of TElement)
Public Property GroupList() As List(Of TElement)
Get
Return _groupList
End Get
Set(ByVal value As List(Of TElement))
_groupList = value
End Set
End Property
Public ReadOnly Property Key() As TKey
Get
Return _key
End Get
End Property
Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of TElement) _
Implements System.Collections.Generic.IEnumerable(Of TElement).GetEnumerator
Return _groupList.GetEnumerator
End Function
Public Function GetEnumerator1() As System.Collections.IEnumerator _
Implements System.Collections.IEnumerable.GetEnumerator
Return _groupList.GetEnumerator
End Function
Public Sub New(ByVal key As TKey)
_key = key
_groupList = New List(Of TElement)
End Sub
End Class
Module Module1
<System.Runtime.CompilerServices.Extension()> _
Public Function GroupAdjacent(Of TElement, TKey)(ByVal source As IEnumerable(Of TElement), _
ByVal keySelector As Func(Of TElement, TKey)) As List(Of GroupOfAdjacent(Of TElement, TKey))
Dim lastKey As TKey = Nothing
Dim currentGroup As GroupOfAdjacent(Of TElement, TKey) = Nothing
Dim allGroups As List(Of GroupOfAdjacent(Of TElement, TKey)) = New List(Of GroupOfAdjacent(Of TElement, TKey))()
For Each item In source
Dim thisKey As TKey = keySelector(item)
If lastKey IsNot Nothing And Not thisKey.Equals(lastKey) Then
allGroups.Add(currentGroup)
End If
If Not thisKey.Equals(lastKey) Then
currentGroup = New GroupOfAdjacent(Of TElement, TKey)(keySelector(item))
End If
currentGroup.GroupList.Add(item)
lastKey = thisKey
Next
If lastKey IsNot Nothing Then
allGroups.Add(currentGroup)
End If
Return allGroups
End Function
Public Sub WriteStartElement(ByVal writer As XmlWriter, ByVal e As XElement)
Dim ns As XNamespace = e.Name.Namespace
writer.WriteStartElement(e.GetPrefixOfNamespace(ns), _
e.Name.LocalName, ns.NamespaceName)
For Each a In e.Attributes
ns = a.Name.Namespace
Dim localName As String = a.Name.LocalName
Dim namespaceName As String = ns.NamespaceName
writer.WriteAttributeString( _
e.GetPrefixOfNamespace(ns), _
localName, _
IIf(namespaceName.Length = 0 And localName = "xmlns", _
XNamespace.Xmlns.NamespaceName, namespaceName),
a.Value)
Next
End Sub
Public Sub WriteElement(ByVal writer As XmlWriter, ByVal e As XElement)
If (e.Name = "TextBlock") Then
WriteStartElement(writer, e)
writer.WriteRaw(Environment.NewLine)
' Create an XML writer that outputs no insignificant white space so that we can
' write to it and explicitly control white space.
Dim settings As XmlWriterSettings = New XmlWriterSettings()
settings.Indent = False
settings.OmitXmlDeclaration = True
settings.ConformanceLevel = ConformanceLevel.Fragment
Dim sb As StringBuilder = New StringBuilder()
Using newXmlWriter As XmlWriter = XmlWriter.Create(sb, settings)
' Group adjacent runs so that they can be output with no whitespace between them
Dim groupedRuns = e.Nodes().GroupAdjacent( _
Function(n) As Boolean?
If TypeOf n Is XElement Then
Dim element As XElement = n
If element.Name = "Run" Then
Return True
End If
Return False
End If
Return False
End Function)
For Each g In groupedRuns
If g.Key = True Then
' Write white space so that the line of Run elements is properly indented.
newXmlWriter.WriteRaw("".PadRight((e.Ancestors().Count() + 1) * 2))
For Each run In g
run.WriteTo(newXmlWriter)
Next
newXmlWriter.WriteRaw(Environment.NewLine)
Else
For Each g2 In g
' Write some white space so that each child element is properly indented.
newXmlWriter.WriteRaw("".PadRight((e.Ancestors().Count() + 1) * 2))
g2.WriteTo(newXmlWriter)
newXmlWriter.WriteRaw(Environment.NewLine)
Next
End If
Next
End Using
writer.WriteRaw(sb.ToString())
writer.WriteRaw("".PadRight(e.Ancestors().Count() * 2))
writer.WriteEndElement()
Else
WriteStartElement(writer, e)
For Each n In e.Nodes
If TypeOf n Is XElement Then
Dim element = n
WriteElement(writer, element)
Continue For
End If
n.WriteTo(writer)
Next
writer.WriteEndElement()
End If
End Sub
Function ToStringWithCustomWhiteSpace(ByVal element As XElement) As String
' Create XmlWriter that indents.
Dim settings As XmlWriterSettings = New XmlWriterSettings()
settings.Indent = True
settings.OmitXmlDeclaration = True
Dim sb As StringBuilder = New StringBuilder()
Using xmlWriter As XmlWriter = xmlWriter.Create(sb, settings)
WriteElement(xmlWriter, element)
End Using
Return sb.ToString()
End Function
Sub Main()
Dim myXML As XElement = _
<Canvas>
<Grid>
<TextBlock>
<Run Text='r'/>
<Run Text='u'/>
<Run Text='n'/>
</TextBlock>
<TextBlock>
<Run Text='far a'/>
<Run Text='way'/>
<Run Text=' from me'/>
</TextBlock>
</Grid>
<Grid>
<TextBlock>
<Run Text='I'/>
<Run Text=' '/>
<Run Text='want'/>
<LineBreak/>
</TextBlock>
<TextBlock>
<LineBreak/>
<Run Text='...thi'/>
<Run Text= to'/>
<LineBreak/>
<Run Text=' work'/>
</TextBlock>
</Grid>
</Canvas>
Console.Write(ToStringWithCustomWhiteSpace(myXML))
Console.ReadLine()
End Sub
End Module
`