在缩进模式下使用 XmlWriter 时是否可以在特定属性上手动换行?
Is it possible to manually line break on a specific attribute when using XmlWriter in Indent mode?
XmlWriter
允许在使用 XmlWriter.Create
和 XmlWriterSettings
时配置缩进。
一般来说,我希望 Indent = true
和 NewLineOnAttributes = false
, 除了 在文件开头写 xmlns
命名空间声明时,其中为了便于阅读,我想在每个 xmlns
命名空间之间换行。
是否可以强制 XmlWriter
在写入特定属性后换行,否则遵循一般缩进规则?
我尝试将 WriteWhitespace
和 WriteRaw
与 \n
一起使用:
using System;
using System.Text;
using System.Xml;
namespace XmlWriterIndent
{
class Program
{
static void Main(string[] args)
{
var output = new StringBuilder();
using (var writer = XmlWriter.Create(output, new XmlWriterSettings { Indent = true, NewLineOnAttributes = true }))
{
writer.WriteStartDocument();
writer.WriteStartElement("Node");
writer.WriteAttributeString("key1", "value1");
writer.WriteAttributeString("key2", "value2");
writer.WriteAttributeString("xmlns", "n1", null, "scheme://mynamespace.com");
writer.WriteRaw("\n");
writer.WriteAttributeString("xmlns", "n2", null, "scheme://anothernamespace.com");
writer.WriteEndElement();
writer.WriteEndDocument();
}
var xml = output.ToString();
Console.WriteLine(xml);
}
}
}
不幸的是,这会引发异常,指出 XML 文档无效。
更新: 实际上,经过更仔细的检查,异常不在 WriteRaw
方法本身,而是在下面的 WriteAttributeString
调用中,因为我在所有命名空间的循环中调用这些方法。
看起来 WriteRaw
以某种方式将 XmlWriter
移动到元素内容状态。是否可以在不更改编写器状态的情况下使用 WriteRaw
或以某种方式在属性之间插入空格?
更新: 添加了独立示例。实际上,即使在使用 NewLineOnAttributes
时,看起来一般的命名空间声明也会被忽略,即所有属性都有新行 除了 命名空间声明,尽管是常规属性,但它们的处理方式有所不同。
不幸的是,我得出的结论是 XmlWriter
API 完全损坏了,因为无法对 XML 进行原始格式化,因为 WriteRaw
强制更改写入器状态。
查看referencesource 的实际源代码表明特殊的write 方法WriteIndent
用于处理XmlWriter
中的缩进。此方法具有不更改状态的特殊行为,但似乎无法访问它或底层数据流,因此如果不完全重新实现整个 XML 编写器,似乎无法解决此问题堆:
https://referencesource.microsoft.com/#System.Xml/System/Xml/Core/XmlEncodedRawTextWriter.cs,1739
XmlWriter
API 无法对 XML 属性进行原始格式化。 WriteRaw
是调用的适当方法,但是 XmlWriter.Create
返回的内部 XmlWellFormedWriter
总是在调用此方法时推进写入器状态,将 XML 状态机推进到内容.如果我们正在编写属性,这将完成元素的开始标记并移动到内容,即 而不是 我们要编写自定义缩进的地方。
几个内部 XmlWriter
classes 实现了更多低级 WriteRaw
方法,但似乎无法访问它们,因为 XmlWriter.Create
总是包装创建返回前有 XmlWellFormedWriter
个实例的作家。
因此,解决该问题的唯一方法是定义和实例化一个自定义 XmlWriter
class 控制 both 底层流 和基数XmlWriter
。这样我们就可以绕过 XmlWriter
API 并在需要进行自定义缩进时直接写入流。
当前解决方案有几个限制:
XmlWriter
实现不直接写入流,而是保留自己的内部缓冲区以提高效率。这意味着每当我们想要绕过 XmlWriter
时,我们需要调用 Flush
以确保我们的流处于正确的位置;
为了使缩进有意义,我们需要跟踪正确的缩进级别。对整个文档执行此操作是可能的,但很乏味。为简单起见,此解决方案仅格式化顶级 xmlns
声明;
为了完整性,我们需要将 Stream
和 TextWriter
作为可能的输出类型来处理。
最后,XmlWriter
抽象 class 有许多我们不关心的重写方法,但需要桥接到底层编写器。为了简洁起见,我省略了除相关覆盖之外的所有内容:
class XmlnsIndentedWriter : XmlWriter
{
bool isRootElement;
int indentLevel = -1;
readonly Stream stream;
readonly TextWriter textWriter;
readonly XmlWriter writer;
private XmlnsIndentedWriter(Stream output, XmlWriter baseWriter)
{
stream = output;
writer = baseWriter;
}
private XmlnsIndentedWriter(TextWriter output, XmlWriter baseWriter)
{
textWriter = output;
writer = baseWriter;
}
public static new XmlWriter Create(StringBuilder output, XmlWriterSettings settings)
{
var writer = XmlWriter.Create(output, settings);
return new XmlnsIndentedWriter(new StringWriter(output, CultureInfo.InvariantCulture), writer);
}
public static new XmlWriter Create(Stream stream, XmlWriterSettings settings)
{
var writer = XmlWriter.Create(stream, settings);
return new XmlnsIndentedWriter(stream, writer);
}
// snip: override all methods in the XmlWriter class
private void WriteRawText(string text)
{
writer.Flush();
if (stream != null)
{
// example only, this could be optimized with buffers, etc.
var buf = writer.Settings.Encoding.GetBytes(text);
stream.Write(buf, 0, buf.Length);
}
else if (textWriter != null)
{
textWriter.Write(text);
}
}
public override void WriteStartDocument()
{
isRootElement = true;
writer.WriteStartDocument();
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
if (isRootElement)
{
if (indentLevel < 0)
{
// initialize the indent level;
// length of local name + any control characters / prefixes, etc.
indentLevel = localName.Length + 1;
}
else
{
// do not track indent for the whole document;
// when second element starts, we are done
isRootElement = false;
indentLevel = -1;
}
}
writer.WriteStartElement(prefix, localName, ns);
}
public override void WriteEndAttribute()
{
writer.WriteEndAttribute();
if (indentLevel >= 0)
{
RawText(Environment.NewLine + new string(' ', indentLevel));
}
}
}
可以选择在每个属性之前或之后添加缩进。
这里我选择了后者,因为如果您还想缩进默认的 xmlns
声明,这似乎是唯一的选择。这个declaration是在writer state移动到content之后写出来的,不然好像没办法拦截了。
XmlWriter
允许在使用 XmlWriter.Create
和 XmlWriterSettings
时配置缩进。
一般来说,我希望 Indent = true
和 NewLineOnAttributes = false
, 除了 在文件开头写 xmlns
命名空间声明时,其中为了便于阅读,我想在每个 xmlns
命名空间之间换行。
是否可以强制 XmlWriter
在写入特定属性后换行,否则遵循一般缩进规则?
我尝试将 WriteWhitespace
和 WriteRaw
与 \n
一起使用:
using System;
using System.Text;
using System.Xml;
namespace XmlWriterIndent
{
class Program
{
static void Main(string[] args)
{
var output = new StringBuilder();
using (var writer = XmlWriter.Create(output, new XmlWriterSettings { Indent = true, NewLineOnAttributes = true }))
{
writer.WriteStartDocument();
writer.WriteStartElement("Node");
writer.WriteAttributeString("key1", "value1");
writer.WriteAttributeString("key2", "value2");
writer.WriteAttributeString("xmlns", "n1", null, "scheme://mynamespace.com");
writer.WriteRaw("\n");
writer.WriteAttributeString("xmlns", "n2", null, "scheme://anothernamespace.com");
writer.WriteEndElement();
writer.WriteEndDocument();
}
var xml = output.ToString();
Console.WriteLine(xml);
}
}
}
不幸的是,这会引发异常,指出 XML 文档无效。
更新: 实际上,经过更仔细的检查,异常不在 WriteRaw
方法本身,而是在下面的 WriteAttributeString
调用中,因为我在所有命名空间的循环中调用这些方法。
看起来 WriteRaw
以某种方式将 XmlWriter
移动到元素内容状态。是否可以在不更改编写器状态的情况下使用 WriteRaw
或以某种方式在属性之间插入空格?
更新: 添加了独立示例。实际上,即使在使用 NewLineOnAttributes
时,看起来一般的命名空间声明也会被忽略,即所有属性都有新行 除了 命名空间声明,尽管是常规属性,但它们的处理方式有所不同。
不幸的是,我得出的结论是 XmlWriter
API 完全损坏了,因为无法对 XML 进行原始格式化,因为 WriteRaw
强制更改写入器状态。
查看referencesource 的实际源代码表明特殊的write 方法WriteIndent
用于处理XmlWriter
中的缩进。此方法具有不更改状态的特殊行为,但似乎无法访问它或底层数据流,因此如果不完全重新实现整个 XML 编写器,似乎无法解决此问题堆:
https://referencesource.microsoft.com/#System.Xml/System/Xml/Core/XmlEncodedRawTextWriter.cs,1739
XmlWriter
API 无法对 XML 属性进行原始格式化。 WriteRaw
是调用的适当方法,但是 XmlWriter.Create
返回的内部 XmlWellFormedWriter
总是在调用此方法时推进写入器状态,将 XML 状态机推进到内容.如果我们正在编写属性,这将完成元素的开始标记并移动到内容,即 而不是 我们要编写自定义缩进的地方。
几个内部 XmlWriter
classes 实现了更多低级 WriteRaw
方法,但似乎无法访问它们,因为 XmlWriter.Create
总是包装创建返回前有 XmlWellFormedWriter
个实例的作家。
因此,解决该问题的唯一方法是定义和实例化一个自定义 XmlWriter
class 控制 both 底层流 和基数XmlWriter
。这样我们就可以绕过 XmlWriter
API 并在需要进行自定义缩进时直接写入流。
当前解决方案有几个限制:
XmlWriter
实现不直接写入流,而是保留自己的内部缓冲区以提高效率。这意味着每当我们想要绕过XmlWriter
时,我们需要调用Flush
以确保我们的流处于正确的位置;为了使缩进有意义,我们需要跟踪正确的缩进级别。对整个文档执行此操作是可能的,但很乏味。为简单起见,此解决方案仅格式化顶级
xmlns
声明;为了完整性,我们需要将
Stream
和TextWriter
作为可能的输出类型来处理。
最后,XmlWriter
抽象 class 有许多我们不关心的重写方法,但需要桥接到底层编写器。为了简洁起见,我省略了除相关覆盖之外的所有内容:
class XmlnsIndentedWriter : XmlWriter
{
bool isRootElement;
int indentLevel = -1;
readonly Stream stream;
readonly TextWriter textWriter;
readonly XmlWriter writer;
private XmlnsIndentedWriter(Stream output, XmlWriter baseWriter)
{
stream = output;
writer = baseWriter;
}
private XmlnsIndentedWriter(TextWriter output, XmlWriter baseWriter)
{
textWriter = output;
writer = baseWriter;
}
public static new XmlWriter Create(StringBuilder output, XmlWriterSettings settings)
{
var writer = XmlWriter.Create(output, settings);
return new XmlnsIndentedWriter(new StringWriter(output, CultureInfo.InvariantCulture), writer);
}
public static new XmlWriter Create(Stream stream, XmlWriterSettings settings)
{
var writer = XmlWriter.Create(stream, settings);
return new XmlnsIndentedWriter(stream, writer);
}
// snip: override all methods in the XmlWriter class
private void WriteRawText(string text)
{
writer.Flush();
if (stream != null)
{
// example only, this could be optimized with buffers, etc.
var buf = writer.Settings.Encoding.GetBytes(text);
stream.Write(buf, 0, buf.Length);
}
else if (textWriter != null)
{
textWriter.Write(text);
}
}
public override void WriteStartDocument()
{
isRootElement = true;
writer.WriteStartDocument();
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
if (isRootElement)
{
if (indentLevel < 0)
{
// initialize the indent level;
// length of local name + any control characters / prefixes, etc.
indentLevel = localName.Length + 1;
}
else
{
// do not track indent for the whole document;
// when second element starts, we are done
isRootElement = false;
indentLevel = -1;
}
}
writer.WriteStartElement(prefix, localName, ns);
}
public override void WriteEndAttribute()
{
writer.WriteEndAttribute();
if (indentLevel >= 0)
{
RawText(Environment.NewLine + new string(' ', indentLevel));
}
}
}
可以选择在每个属性之前或之后添加缩进。
这里我选择了后者,因为如果您还想缩进默认的 xmlns
声明,这似乎是唯一的选择。这个declaration是在writer state移动到content之后写出来的,不然好像没办法拦截了。