重写XMLDocument以使用名称空间前缀

时间:2015-06-13 12:07:55

标签: c# xml xml-namespaces

我有一个XMLDocument,当我保存到文件时,会在大多数元素上重复命名空间,如

<Test>
    <Test xmlns="http://example.com/schema1">

      <Name xmlns="http://example.com/schema2">xyz</Name>
      <AddressInfo xmlns="http://example.com/schema2">
        <Address>address</Address>
        <ZipCode>zzzz</ZipCode>
      </AddressInfo>
       ...

是否可以修改此文件,以便在整个文档中使用名称空间前缀,例如

<Test xmlns="http://example.com/schema1" xmlns:p="http://example.com/schema2"  >

 <p:Name>xyz</p:Name>
 <p:AddressInfo">
   <p:Address>address</p:Address>
   <p:ZipCode>zzzz</p:ZipCode>
 </p:AddressInfo>        
 ...

我尝试过添加

   doc.DocumentElement.SetAttribute("xmlns:p", "http://example.com/schema2");

但是虽然这会将名称空间添加到标题中,但文件的主体不会改变。

3 个答案:

答案 0 :(得分:1)

使用LINQ to XML执行此操作的一种方法是将名称空间声明添加到根目录中,然后删除所有现有名称,如下所示:

var doc = XDocument.Parse(xml);

var existingDeclarations = doc.Descendants()
    .SelectMany(e => e.Attributes().Where(a => a.IsNamespaceDeclaration))
    .ToList();

doc.Root.Add(new XAttribute(XNamespace.Xmlns + "p", "http://example.com/schema2"));

existingDeclarations.Remove();

我不知道使用XmlDocument这么简单的解决方案,但我总是建议使用LINQ to XML,除非你有充分的理由不这样做。

答案 1 :(得分:1)

您可以简单地change XmlElement.Prefix property value

doc.DocumentElement.SetAttribute("xmlns:p", "http://example.com/schema2");
//xpath for selecting all elements in specific namespace :
var xpath = "//*[namespace-uri()='http://example.com/schema2']";
foreach(XmlElement node in doc.SelectNodes(xpath))
{
    node.Prefix = "p";
}
doc.Save("path_to_file.xml");

答案 2 :(得分:0)

我找到了这个问题,并且解决了我试图解决的问题,但是我需要一个更通用的解决方案,因此我在下面开发了扩展方法。它的灵感来自查尔斯·马格(Charles Mager)的答案,但是正如您所看到的,它远远超出了这个范围。坦白说,我很后悔被这个问题困扰,因为很难弄清楚,但其他人可能会从中受益。

您可能会忽略'valueChangingAttribNames'参数的事务。我不得不处理它,因为System.Runtime.Serialization.DataContractSerializer会生成i:type属性,这些属性会将名称空间前缀嵌入到属性值中。

以下是显示扩展方法用法的代码段:

XName valueChangingAttribName = "{http://www.w3.org/2001/XMLSchema-instance}type";
var xDoc = XDocument.Load(stream);
xDoc.Root.ConsolidateNamespaces(new List<XName> { valueChangingAttribName });

一个小警告:对于前缀冲突的情况,我设计了将字母添加到现有前缀的方法。如果您有超过26种不同的名称空间,而所有这些名称空间都被分配了相同的前缀,这让我感到震惊。但是,如果您遇到类似情况,则需要修改我的生成唯一前缀的方法。

    /// <summary>
    /// This method finds all namespace declarations on the descendants of the given root XElement
    /// and moves them from the decendant elements to the root element. It is thus possible to 
    /// make the XML string much smaller and more human-readable in cases when there are many
    /// redundant declarations of the same namespace.
    /// </summary>
    /// <param name="xRoot">The element whose descendants are to have their namespaces declarations
    /// removed. Also, the element that is to contain the consolidated namespace declarations</param>
    /// <param name="valueChangingAttribNames">A list of the names of XAttributes whose value
    /// must be updated to reflect changes to the namespace prefixes.
    /// This is designed to handle cases like d7p1 in the example:
    ///     xmlns:d7p1="http://www.w3.org/2001/XMLSchema" i:type="d7p1:int"
    /// generated by System.Runtime.Serialization.DataContractSerializer. In order to treat this
    /// example, the XName of the i:type attribute should be included in the list.  If the value
    /// of the 'valueChangingAttribNames' parameter is null, then no such changes are made to the
    /// values of XAttributes.  The default is null.
    /// </param>
    public static void ConsolidateNamespaces(this XElement xRoot,
                            List<XName> valueChangingAttribNames = null)
    {
        String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        // Find all XAttributes that represent namespace declarations
        var nsAttributes = xRoot.Descendants().SelectMany(e => e.Attributes())
                    .Where(a => a.IsNamespaceDeclaration).ToList();
        // create groupings by common prefix
        var prefixGroups = nsAttributes.GroupBy(a => a.Name.LocalName);

        // Each time an XAttribute is resolved, we remove it from the list.
        // Loop until all XAttributes are resolved and removed from the list.
        while (nsAttributes.Any())
        {
            // Inner loop repeats until no XAttributes can be resolved without dealing with conflicting
            // prefixes (i.e. same prefix refers to different namespace in different XElements)
            while (nsAttributes.Any())
            {
                // Loop through each XAttribute
                foreach (var attr in nsAttributes.ToList())
                {
                    // If the root already contains a declaration of the same namespace
                    // (regardless of whether the prefix matches between the root and the descendant)
                    if (xRoot.Attributes().Any(a => a.Value == attr.Value))
                    {
                        // We have only to remove the XAttribute from the descendant, and Xml.Linq
                        // framework automatically rectifies prefix on the descendant if necessary.
                        TransferNamespaceToRoot(nsAttributes, xRoot, null, attr,
                                                    valueChangingAttribNames);
                    }
                }
                // Take the first remaining prefix group where there is no prefix name conflict
                var prefixGroup = prefixGroups
                          .FirstOrDefault(g => g.Select(a => a.Value).Distinct().Count() == 1);
                // If no such groups remain, then we're done with the inner loop
                if (prefixGroup == null) break;
                // The list of XAttributes that have matching prefix & namespace
                var attribs = prefixGroup.ToList();
                for (int j = 0; j < attribs.Count; j++)
                    // The first XAttribute must be added to the root, and the rest simply need
                    // to be removed from the descendant.
                    TransferNamespaceToRoot(nsAttributes, xRoot,
                                 (j == 0 ? attribs[j] : null), attribs[j], valueChangingAttribNames);
            }
            // Take the first remaining prefix group. We know there is a prefix name conflict since
            // this group was not processed in the above loop.
            var conflictGroup = prefixGroups.FirstOrDefault();
            if (conflictGroup == null) break;
            // The processing of the previous loop should guarantee that all namespaces in
            // this group are not yet declared in the root.  Each of the conflicting prefixes
            // must be renamed.  If we try to add the existing prefix to the root for any one
            // of the namespaces in this group, then the Xml.Linq framework will incorrectly match
            // the other prefixes in this group to that namespace.
            foreach (var attr in conflictGroup.ToList())
            {
                // If the root already contains a declaration of the same namespace
                // (could only be a previously-processed XAttribute from this same conflict group)
                if (xRoot.Attributes().Any(a => a.Value == attr.Value))
                {
                    // We have only to remove the XAttribute from the descendant, and Xml.Linq
                    // framework automatically rectifies prefix on the descendant if necessary.
                    TransferNamespaceToRoot(nsAttributes, xRoot, null, attr, valueChangingAttribNames);
                    continue;
                }
                // Find a new prefix that doesn't conflict with any existing prefixes
                String newPrefix = attr.Name.LocalName + "_A";
                for (int i = 1; xRoot.Attributes().Any(a => a.Name.LocalName == newPrefix); i++)
                    newPrefix = attr.Name.LocalName + "_" + letters[i];
                // Add the namespace declaration to the root, using the new prefix
                XNamespace ns = attr.Value;
                var newAttr = new XAttribute(XNamespace.Xmlns + newPrefix, attr.Value);
                TransferNamespaceToRoot(nsAttributes, xRoot, newAttr, attr, valueChangingAttribNames);
            }
        }

    }
    /// <summary>
    /// This private method is designed as a helper to public method ConsolidateNamespaces.
    /// The method is designed to remove a given XAttribute from a descendant XElement,
    /// and add a given XAttribute to the root XElement.  The XAttribute to add to the root
    /// may be the same as the item to remove, different than the item to remove, or none,
    /// as appropriate.
    /// </summary>
    /// <param name="nsAttributes">The list of XAttributes that the caller is to process.
    /// This method is designed to remove 'attrToRemove' from the list.</param>
    /// <param name="xRoot">The root XElement to which 'attrToAdd' is added.</param>
    /// <param name="attrToAdd">The XAttribute that is to be added to 'xRoot'.  This may be
    /// the same as or different than 'attrToRemove', or it may be null if the caller does
    /// not need to add anything to the root.</param>
    /// <param name="attrToRemove">The XAttribute that is to be removed from a descendant XElement
    /// and removed from 'nsAttributes'</param>
    /// <param name="valueChangingAttribNames">A list of the names of XAttributes whose value
    /// must be updated to reflect changes to the namespace prefixes.
    /// This is designed to handle cases like d7p1 in the example:
    ///     xmlns:d7p1="http://www.w3.org/2001/XMLSchema" i:type="d7p1:int"
    /// generated by System.Runtime.Serialization.DataContractSerializer. In order to treat this
    /// example, the XName of the i:type attribute should be included in the list.  If the value
    /// of the 'valueChangingAttribNames' parameter is null, then no such changes are made to the
    /// values of XAttributes.  The default is null.
    /// </param>
    private static void TransferNamespaceToRoot(List<XAttribute> nsAttributes, XElement xRoot,
                            XAttribute attrToAdd, XAttribute attrToRemove,
                            List<XName> valueChangingAttribNames)
    {
        if (valueChangingAttribNames != null)
        {
            // Change the value of any 'value changing attributes' that incorporate the prefix.
            // This is designed to handle cases like d7p1 in the example:
            //     <d2p1:Value xmlns:d7p1="http://www.w3.org/2001/XMLSchema" i:type="d7p1:int">
            // that are generated by System.Runtime.Serialization.DataContractSerializer.
            // The Xml.Linq framework does not rectify such cases where the prefix is
            // within the XAttribute vaue.

            String oldPrefix = attrToRemove.Name.LocalName;
            String newPrefix = oldPrefix;
            // If no XAttribute is to be added to the root
            if (attrToAdd == null)
            {
                // find the existing XAttribute in the root for the namespace,
                // and use the prefix that it declares.
                var srcAttr = xRoot.Attributes()
                        .FirstOrDefault(a => a.IsNamespaceDeclaration && a.Value == attrToRemove.Value);
                if (srcAttr != null)
                    newPrefix = srcAttr.Name.LocalName;
            }
            else
                // If a new XAttribute is to be added to the root, then use the prefix it declares
                newPrefix = attrToAdd.Name.LocalName;

            if (oldPrefix != newPrefix)
            {
                foreach (XName attribName in valueChangingAttribNames)
                {
                    // Do string replacement of the prefix in the XAttribute value
                    var vcAttrib = attrToRemove.Parent.Attribute(attribName);
                    vcAttrib.Value = vcAttrib.Value.Replace(attrToRemove.Name.LocalName, newPrefix);
                }
            }
        }

        // Add the XAttribute to the root element
        if (attrToAdd != null)
            xRoot.Add(attrToAdd);
        // Remove the namespace declaration from the descendant
        attrToRemove.Remove();
        // remove the XAttribute from the list of XAttributes to be processed
        nsAttributes.Remove(attrToRemove);
    }