XPath select node with namespace
Asked Answered
K

6

69

Its a .vbproj and looks like this

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <ProjectGuid>15a7ee82-9020-4fda-a7fb-85a61664692d</ProjectGuid>

all i want to get is the ProjectGuid but it does not work when a namespace is there...

 Dim xmlDoc As New XmlDocument()
 Dim filePath As String = Path.Combine(mDirectory, name + "\" + name + ".vbproj")
 xmlDoc.Load(filePath)
 Dim value As Object = xmlDoc.SelectNodes("/Project/PropertyGroup/ProjectGuid")

what can i do to fix this?

Katey answered 11/2, 2009 at 11:49 Comment(2)
Two problems with annakata's solution: 1. It is ugly, 2. In this case it can be used but will provide wrong results if a 'ProjectGuid' element belongs to more than one namespace and we want the elements only from a single namespace. Solutions using the NamespaceManager are betterEnplane
The XPath engine must be provided with the right static context containing the bindings between prefixes and NS URIs for use when evaluating expressions or you won't be able to reference stuff inside namespaces. This is what @Teun does.Ildaile
A
48

The best way to do things like this (IMHO) is to create a namespace manager. This can be used calling SelectNodes to indicate which namespace URLs are connected to which prefixes. I normally set up a static property that returns an adequate instance like this (it's C#, you'll have to translate):

private static XmlNamespaceManager _nsMgr;
public static XmlNamespaceManager NsMgr
{
  get
  {
    if (_nsMgr == null)
    {
      _nsMgr = new XmlNamespaceManager(new NameTable());
      _nsMgr.AddNamespace("msb", "http://schemas.microsoft.com/developer/msbuild/2003");
    }
    return _nsMgr;
  }
}

I include only one namespace here, but you could have multiple. Then you can select from the document like this:

Dim value As Object = xmlDoc.SelectNodes("/msb:Project/msb:PropertyGroup/msb:ProjectGuid", NsMgr)

Note that all of the elements are in the specified namespace.

Ana answered 11/2, 2009 at 12:14 Comment(3)
you dont need to create a new XmlDocument to get a XmlNameTable. you can use nsMgr = new XmlNamespaceManager(new NameTable());Cardie
Ah, thanks. I never found out how to do that. Was new NameTable() already possible in .NET 1.0?Ana
It is amazing how much time it can save in the long run to use namespaces correctly in the first place.Tien
I
68

I'd probably be inclined to go with Bartek's* namespace solution, but a general xpath solution is:

//*[local-name()='ProjectGuid']

**since Bartek's answer has disappeared, I recommend Teun's (which is actually more thorough)*

Insphere answered 11/2, 2009 at 12:5 Comment(8)
Agreed, although this becomes a real PITA when you have to go more than a couple of levels deep. It does work, though. :)Gouge
quite, which is why I'd go with Barteks - the only thing stopping me there is if I don't know the namespace beforehand or can't guarantee it, in which case I'd probably wash the entire doc first, but saying so will only get me stalker downvotes :)Insphere
Two problems with this: 1. It is ugly, 2. In this case it can be used but will provide wrong results if a 'ProjectGuid' element belongs to more than one namespace and we want the elements only from a single namespace. Solutions using the NamespaceManager are better.Enplane
I downvote something that's completely wrong, not something that's a solution, although not the best oneEnplane
It would probably be more robust to mention that this is a hacked together solution, and why...Tien
However, it works this is not a performant way of doing it. The correct way would be to load the xpath selector with namespaces.Kilauea
nice catch, this is a good way to not declare boring and expensive namespacesPetrapetracca
This is great, declaring all these namespaces makes XPath more trouble than it's worth IMO.Spelaean
A
48

The best way to do things like this (IMHO) is to create a namespace manager. This can be used calling SelectNodes to indicate which namespace URLs are connected to which prefixes. I normally set up a static property that returns an adequate instance like this (it's C#, you'll have to translate):

private static XmlNamespaceManager _nsMgr;
public static XmlNamespaceManager NsMgr
{
  get
  {
    if (_nsMgr == null)
    {
      _nsMgr = new XmlNamespaceManager(new NameTable());
      _nsMgr.AddNamespace("msb", "http://schemas.microsoft.com/developer/msbuild/2003");
    }
    return _nsMgr;
  }
}

I include only one namespace here, but you could have multiple. Then you can select from the document like this:

Dim value As Object = xmlDoc.SelectNodes("/msb:Project/msb:PropertyGroup/msb:ProjectGuid", NsMgr)

Note that all of the elements are in the specified namespace.

Ana answered 11/2, 2009 at 12:14 Comment(3)
you dont need to create a new XmlDocument to get a XmlNameTable. you can use nsMgr = new XmlNamespaceManager(new NameTable());Cardie
Ah, thanks. I never found out how to do that. Was new NameTable() already possible in .NET 1.0?Ana
It is amazing how much time it can save in the long run to use namespaces correctly in the first place.Tien
J
29

This problem has been here several times already.

Either you work with namespace-agnostic XPath expressions (not recommended for its clumsiness and the potential for false positive matches - <msb:ProjectGuid> and <foo:ProjectGuid> are the same for this expression):

//*[local-name() = 'ProjectGuid']

or you do the right thing and use a XmlNamespaceManager to register the namespace URI so you can include a namespace prefix in your XPath:

Dim xmlDoc As New XmlDocument()
xmlDoc.Load(Path.Combine(mDirectory, name, name + ".vbproj"))

Dim nsmgr As New XmlNamespaceManager(xmlDoc.NameTable)
nsmgr.AddNamespace("msb", "http://schemas.microsoft.com/developer/msbuild/2003")

Dim xpath As String = "/msb:Project/msb:PropertyGroup/msb:ProjectGuid"
Dim value As Object = xmlDoc.SelectNodes(xpath, nsmgr)
Junia answered 11/2, 2009 at 12:19 Comment(0)
D
4

You need just to register this XML namespaces and associate with a prefix, to make the query work. Create and pass a namespace manager as second parameter when selecting the nodes:

Dim ns As New XmlNamespaceManager ( xmlDoc.NameTable )
ns.AddNamespace ( "msbuild", "http://schemas.microsoft.com/developer/msbuild/2003" )
Dim value As Object = xmlDoc.SelectNodes("/msbuild:Project/msbuild:PropertyGroup/msbuild:ProjectGuid", ns)
Dappled answered 11/2, 2009 at 12:20 Comment(0)
N
0

One way is to use extensions + NameSpaceManager.
Code is in VB but is realy easy to translate to C#.

Imports System.Xml
Imports System.Runtime.CompilerServices

Public Module Extensions_XmlHelper

    'XmlDocument Extension for SelectSingleNode
    <Extension()>
    Public Function _SelectSingleNode(ByVal XmlDoc As XmlDocument, xpath As String) As XmlNode
        If XmlDoc Is Nothing Then Return Nothing

        Dim nsMgr As XmlNamespaceManager = GetDefaultXmlNamespaceManager(XmlDoc, "x")
        Return XmlDoc.SelectSingleNode(GetNewXPath(xpath, "x"), nsMgr)
    End Function

    'XmlDocument Extension for SelectNodes
    <Extension()>
    Public Function _SelectNodes(ByVal XmlDoc As XmlDocument, xpath As String) As XmlNodeList
        If XmlDoc Is Nothing Then Return Nothing

        Dim nsMgr As XmlNamespaceManager = GetDefaultXmlNamespaceManager(XmlDoc, "x")
        Return XmlDoc.SelectNodes(GetNewXPath(xpath, "x"), nsMgr)
    End Function


    Private Function GetDefaultXmlNamespaceManager(ByVal XmlDoc As XmlDocument, DefaultNamespacePrefix As String) As XmlNamespaceManager
        Dim nsMgr As New XmlNamespaceManager(XmlDoc.NameTable)
        nsMgr.AddNamespace(DefaultNamespacePrefix, XmlDoc.DocumentElement.NamespaceURI)
        Return nsMgr
    End Function

    Private Function GetNewXPath(xpath As String, DefaultNamespacePrefix As String) As String
        'Methode 1: The easy way
        Return xpath.Replace("/", "/" + DefaultNamespacePrefix + ":")

        ''Methode 2: Does not change the nodes with existing namespace prefix
        'Dim Nodes() As String = xpath.Split("/"c)
        'For i As Integer = 0 To Nodes.Length - 1
        '    'If xpath starts with "/", don't add DefaultNamespacePrefix to the first empty node (before "/")
        '    If String.IsNullOrEmpty(Nodes(i)) Then Continue For
        '    'Ignore existing namespaces prefixes
        '    If Nodes(i).Contains(":"c) Then Continue For
        '    'Add DefaultNamespacePrefix
        '    Nodes(i) = DefaultNamespacePrefix + ":" + Nodes(i)
        'Next
        ''Create and return then new xpath
        'Return String.Join("/", Nodes)
    End Function

End Module

And to use it:

Imports Extensions_XmlHelper

......
Dim FileXMLTextReader As New XmlTextReader(".....")
FileXMLTextReader.WhitespaceHandling = WhitespaceHandling.None
Dim xmlDoc As XmlDocument = xmlDoc.Load(FileXMLTextReader)
FileXMLTextReader.Close()
......
Dim MyNode As XmlNode = xmlDoc._SelectSingleNode("/Document/FirstLevelNode/SecondLevelNode")

Dim MyNode As XmlNodeList = xmlDoc._SelectNodes("/Document/FirstLevelNode/SecondLevelNode")

......
Negrete answered 11/4, 2016 at 13:37 Comment(0)
C
-8

Why not use the // to ignore the namespace:

Dim value As Object = xmlDoc.SelectNodes("//ProjectGuid")

// acts as wild card to follow through everything between the root and the next node name specified(i.e ProjectGuid)

Cantoris answered 11/2, 2009 at 11:56 Comment(1)
doesn't actually work - yes this says look for any ProjectGuids anywhere, but it still wants them within the default namespaceInsphere

© 2022 - 2024 — McMap. All rights reserved.