I am new to xpath in C# and I am trying to select an element that has two specific values. This is what the XML format looks like
<?xml version="1.0" encoding="utf-8"?>
<Manager>
<SSH>
<Tunnels>
<Port>
<Local>443</Local>
<Remote>443</Remote>
</Port>
<Port>
<Local>5432</Local>
<Remote>5432</Remote>
</Port>
<Port>
<Local>19</Local>
<Remote>21</Remote>
</Port>
<Port>
<Local>19</Local>
<Remote>22</Remote>
</Port>
</Tunnels>
</SSH>
</Manager>
I was trying to select a 'Port' that had the values from a previous form so i can delete that specific entry from the xml. This was the code i was using:
//remove children from selected
XmlNode _xmlTunnel = _xml.SelectSingleNode("/Manager/SSH/Tunnels/Port[Local=" + _local + "] | /Manager/SSH/Tunnels/Port[Remote=" + _remote + "]");
MessageBox.Show("Local " + sshList.SelectedItems[0].Text + " Remote " + sshList.SelectedItems[0].SubItems[1].Text +"\n\n" + _xmlTunnel.InnerText);
_xmlTunnel.RemoveAll();
//remove all empties
XmlNodeList emptyElements = _xml.SelectNodes(#"//*[not(node())]");
for (int i = emptyElements.Count -1; i >= 0; i--) {
emptyElements[ i ].ParentNode.RemoveChild(emptyElements[ i ]); }
This code works fine until I have two Ports with the same Local Value. It always selects the first element that it comes to (i.e. Local=19 and Remote=21, even if you try to select the node where Local=19 and Remote=22). I tried switching the xpath expression to 'and' instead of '|' in the SelectSingleNode method but that errors out with a "Expression must evaluate to a node-set". Which makes me think that I am evaluating out to a boolean when I use 'and'.
Is the better way to do this by a loop where a select the first element and loop until the second one matches? As I said before i do not have much experience with xpath/xml expressions in C#, perhaps there is a better way. If it helps I am using windows forms and .net 4.0, in this form the port values fill a two column list view in detail view.
You need to "and" 2 conditions on Port node like:
"/Manager/SSH/Tunnels/Port[Local=" + _local + " and Remote=" + _remote + "]"
In your case you are doing union of 2 sets where Local=19 and another where Remote=21.
Related
I am new to using LINQ and am trying to select a node from an XML document that i've transformed.
here is the raw code:
// create a LINQ xml doc
XDocument xdoc = XDocument.Parse(xTransformedDoc.OuterXml);
// get sibling elements to the shredding element
IEnumerable<XElement> xe = xdoc.Root.XPathSelectElements("//" + settings.ShredNode + "[1]/(following-sibling|preceding-sibling)[name() != '" + settings.ShredNode + "']");
Note: earlier in the code settings.ShredNode is set to "DocRouteDetail"
at first i thought it was a problem with the complexity of the xpath statement i am using however i've tried every combination i can think of and even rolled it back to absolute basics in the VS debugger:
xdoc.Root.XPathSelectElements(".")
xdoc.Root.XPathSelectElements("//DocRouteDetail")
etc
in all cases it returns back null and it generates an exception returning "Expression must evaluate to a node-set."
this is happening in .NET framework 4.0 in c#.
i have checked and the xdoc.Root variable is not null and my xml looks like the following (full document slimmed down for security reasons):
<DocFWImport xmlns:dtfn2="urn:my-scripts2">
<Header SendDateTime="2014-04-03T19:26:50" />
<Request>
<DocRouteDetail MessagePurpose="1002" ResourceKey="A" >
<DocStop StopNumber="0" Type="0" LocationType="DEPOT">
</DocStop>
<DocStop StopNumber="1" Type="3" LocationType="CUSTOMER" >
</DocStop>
<DocStop StopNumber="2" Type="0" LocationType="DEPOT">
</DocStop>
</DocRouteDetail>
<parmRouteTemplateKey>TEAM</parmRouteTemplateKey>
<DocRouteDetail MessagePurpose="1002" ResourceKey="B" >
<DocStop StopNumber="0" Type="0" LocationType="DEPOT">
</DocStop>
<DocStop StopNumber="1" Type="3" LocationType="CUSTOMER" >
</DocStop>
<DocStop StopNumber="2" Type="0" LocationType="DEPOT">
</DocStop>
</DocRouteDetail>
<parmRouteTemplateKey>SINGLE</parmRouteTemplateKey>
etc
</Request>
</DocFWImport>
it should have returned the 2 parmRouteTemplateKey elements.
It's not the XPathSelectElements's fault, your XPath query is invalid - namely the part with the | operator. The other basic queries you're mentioning do work. If they really don't, then there's some another error outside the code you've posted.
I'd write it as
//DocRouteDetail[1]/parent::*/child::*[name()!='DocRouteDetail']
that way it selects what you need.
As pointed by #pjotr, your XPath attempt is not valid. You can try to combine 2 XPaths using union operator (|) like this instead :
var xpath1 = "//" + settings.ShredNode
+ "[1]/following-sibling[name() != '" + settings.ShredNode + "']";
var xpath2 = "//" + settings.ShredNode
+ "[1]/preceding-sibling[name() != '" + settings.ShredNode + "']";
IEnumerable<XElement> xe =
xdoc.Root
.XPathSelectElements(xpath1 + " | " + xpath2);
Within an xml document I have the following
...
<TablixMembers>
<TablixMember>
<Group Name="Details" />
</TablixMember>
</TablixMembers>
</TablixMember>
<TablixHeader Name="MyField3" />
<TablixHeader Name="MyField2" />
</TablixMembers>
</TablixMember>
I have a for loop that looks for each element with a field name ("MyField3, MyField2, ...)
for (int g = 0; g < remainingtotals; g++)
{
groupTotals.Reverse();
ff.Add(new XElement(ns + "TablixHeader"));
ff.Descendants(ns + "TablixHeader").Last().SetAttributeValue("Name", groupTotals[g]);
string nName = GenerateUniqueName("Nsted");
while (ff.Descendants(ns + "TablixHeader").Any(n => (string)n.Attribute("Name") == groupTotals[g]))
{
var newnode = ff.Descendants(ns + "TablixHeader").Where(n => (string)n.Attribute("Name") == groupTotals[g]).Single();
newnode.ReplaceWith(new XElement(tGroupHeader));
}
groupTotals.Reverse();
}
Within this loop I have a List<string> groupTotals which holds the names of the fields.
It may not need to be in a while loop (I added this for testing) I have a query that looks for the specific field
var newnode = ff.Descendants(ns + "TablixHeader").Where(n => (string)n.Attribute("Name") == groupTotals[g]).Single();
newnode.ReplaceWith(new XElement(tGroupHeader));
This sets newnode equal to:
<TablixHeader Name="MyField3" xmlns="http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition" />
On the next line I am trying to replace this node with a new xml element structure
newnode.ReplaceWith(new XElement(tGroupHeader));
tGroupHeader is a memorystream that contains a large xml Element template that should be inserted and replace the current tablix node of the selected attribute name.
When this code runs the node does not get replaced. In fact nothing happens, I've set break points on the node, no xml exception is thrown it just steps right through it. I assume I am either calling the method incorrectly or the selecting of a single node is what is causing it to not replace the element.
I also thought it may be due to the injection of a large xml structure but I have also tested this using just a single element such as:
newnode.ReplaceWith(new XElement(ns + "Size"));// ns is the xml namespace for the schema I am using
this also did not result in the element being replaced.
Can anyone provide some insight into what I may be doing wrong?
-cheers
I finally found a work around for this it may not be the best approach but it was reliable. From what I could see when I attempted to query the node the element would be located but it would not update I think this was due in part to my attempt to inject and update based upon a .Descendants() query all within one method (this may not be right but it was all I could determine).
The work around approach that I used was instead of trying to navigate the xml structure for an element at the time of creating the element I added an ID attribute with a specific name.
ff.Add(new XElement(ns + "TablixMember"));
ff.Descendants(ns + "TablixMember").Last().SetAttributeValue("Name", groupTotals[g]);
ff.Descendants(ns + "TablixMember").Last().SetAttributeValue("ID", groupedItemTotals[g].GroupPosition.ToString());
This would then insert an XElement such as
<TablixMembers>
<TablixMember>
<TablixHeader xmlns="http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition">
<Size>1in</Size>
<CellContents>
<Textbox Name="GroupTotalPPPccc2973356bf4beaa92cb0409b37b8ef" ID="1">
<CanGrow>true</CanGrow>
<KeepTogether>true</KeepTogether>
<--end snip -->
Once I added the id it made querying for the node much simpler
var newnode = ff.Descendants(ns + "TablixMember").Where(n => (string)n.Attribute("ID") == groupedItemTotals[g].GroupPosition.ToString()).Single();
From here I could then updated the elements within the node using Replace(). ReplaceWith() SetAttribut() etc.
in this snippet I selected the node, removed all child elements and then injected a new structure
newnode.RemoveAll();
newnode.Add(new XElement(tGroupHeader),
new XElement(ns + "KeepWithGroup"));
newnode.Descendants(ns + "TablixHeader").Last().Descendants(ns + "Textbox").Last().SetAttributeValue("Name", nName);
Finally, since the ID attribute was not supported by the current schema I was using I performed some clean up to remove any unsupported attributes.
report.Descendants(ns + "TablixMember").Where(p => (string)p.Attribute("ID") != null).Attributes("ID").Remove();
report.Descendants(ns + "Textbox").Where(p => (string)p.Attribute("ID") != null).Attributes("ID").Remove();
I'd be interested to see other suggestions or ideas.
thanks
I am using XElement to create an XMLDocument which is used in a hierarchical WPF treeview. If I create a new element with :
x_element = new XElement("node",
new XElement("tree_id", strData[0]),
new XElement("sys_id", dpl.DepSysId),
new XElement("part_id", strData[8]),
new XElement("make", strData[6]),
new XElement("model", strData[5]),
new XElement("level", strData[2]));
I then need to add attributes to "node" so I tried:
XElement temp_el = x_element.Element("node"); // This is returning null
temp_el.SetAttributeValue("title", strData[7] + " " + strData[6] + " " + strData[5]);
temp_el.SetAttributeValue("canEdit", "False");
temp_el.SetAttributeValue("status", nStatus.ToString());
temp_el.SetAttributeValue("qty", strData[13]);
temp_el.SetAttributeValue("part", strData[8]);
In the above code temp_el is null, but I can see in the debugger that x_element contains the following :
<node>
<tree_id>82</tree_id>
<sys_id>82</sys_id>
<part_id>169</part_id>
<make>ST Panel</make>
<model>Logical Pure 16 tube Collector</model>
<level>0</level>
</node>
To work around this I have used the following:
foreach (XElement temp_el in x_element.DescendantsAndSelf())
{
if (temp_el.Name == "node")
{
temp_el.SetAttributeValue("title", strData[7] + " " + strData[6] + " " + strData[5]);
temp_el.SetAttributeValue("canEdit", "False");
temp_el.SetAttributeValue("status", nStatus.ToString());
temp_el.SetAttributeValue("qty", strData[13]);
temp_el.SetAttributeValue("part", strData[8]);
break;
}
}
Whilst the above works I am just curious as to why I am getting null returned. Is my workaround the best way of doing this?
Regards.
You defined your XElement like this:
x_element = new XElement("node", /* child nodes */);
Where "node" is the name of the XElement you are creating, and the following parameters are its children.
By using x_element.Node("node"), you are trying to get the child node named "node", and there isn't such a child node.
x_element itself is the node named "node".
DescendantsAndSelf worked because it includes x_element (hence "AndSelf"), but you don't need this either because you already have the node.
So you can change your second code snippet to this:
x_element.SetAttributeValue("title", strData[7] + " " + strData[6] + " " + strData[5]);
x_element.SetAttributeValue("canEdit", "False");
// etc.
(BTW, you can also add the Attributes in the constructor)
Because with your first temp_el,
XElement temp_el = x_element.Element("node");
You used to get nodes which is not treated to be an Element of x_element.
It was treated as its root. However, with the second one,
x_element.DescendantsAndSelf()`
You used this XElement Method which treat node itself as an element.
XContainer.Elements Method - Returns a collection of the child elements of this element or document, in document order.
XElement.DescendantsAndSelf Method - Returns a collection of elements that contain this element, and all descendant elements of this element, in document order.
To solve the issue I used Descendants(). Here is my code snippet
public void UpdateEnquiry([FromBody]XElement UpdatePurchaseOrder)
{
var obj = XElement.Parse(UpdatePurchaseOrder.ToString());
var ii = (from v in obj.Descendants() select new { v.Value }).ToList() ;
}
Im trying to parse an XML file containing all the uploaded videos on a certain channel. Im attempting to get tbe value of the URL attribute in one of the <media:content> nodes and put it in the ViewerLocation field. However there are several of them. My current code is this:
var videos = from xElem in xml.Descendants(atomNS + "entry")
select new YouTubeVideo()
{
Title = xElem.Element(atomNS + "title").Value,
Description = xElem.Element(atomNS + "content").Value,
DateUploaded = xElem.Element(atomNS + "published").Value,
ThumbnailLocation = xElem.Element(mediaNS + "group").Element(mediaNS + "content").Attribute("url").Value,
ViewerLocation = xElem.Element(mediaNS + "group").Element(mediaNS + "content").Attribute("url").Value
};
It gets me the first node in the XML for entry with the name <media:content> as you would expect. However, the first entry in the XML isn't what I want. I want the second.
Below is the relevant XML.
<!-- I currently get the value held in this node -->
<media:content
url='http://www.youtube.com/v/ZTUVgYoeN_b?f=gdata_standard...'
type='application/x-shockwave-flash' medium='video'
isDefault='true' expression='full' duration='215' yt:format='5'/>
<!-- What i actually want is this one -->
<media:content
url='rtsp://rtsp2.youtube.com/ChoLENy73bIAEQ1kgGDA==/0/0/0/video.3gp'
type='video/3gpp' medium='video'
expression='full' duration='215' yt:format='1'/>
<media:content
url='rtsp://rtsp2.youtube.com/ChoLENy73bIDRQ1kgGDA==/0/0/0/video.3gp'
type='video/3gpp' medium='video'
expression='full' duration='215' yt:format='6'/>
I want the second node because it has a type of 'video/3gpp'. How would I go about selecting that one? My logic would be
if attribute(type == "video/3gpp") get this value.
But i do not know how to express this in Linq.
Thanks,
Danny.
Probably something like;
where xElem.Element(atomNS + "content").Attribute("type").Value == "video/3gpp"
Edit: I didn't quite know how to expand and explain this one without assuming the OP had no knowledge of Linq. You want to make your original query;
from xElem in xml.Descendants(atomNS + "entry")
where xElem.Element(atomNS + "content").Attribute("type").Value == "video/3gpp"
select new YouTubeVideo() {
...
}
You can interrogate attributes of a node, just like you can look at the elements of the document. If there are multiple elements with that attribute, you could then (assuming you always want the first you find)..
( from xElem in xml.Descendants(atomNS + "entry")
where xElem.Element(atomNS + "content").Attribute("type").Value == "video/3gpp"
select new YouTubeVideo() {
...
}).First();
I changed the original post, as I believe the node you're querying is the Element(atomNS + "content"), not the top level xElem
Using XPath from this Xml Library (Just because I know how to use it) with associated Get methods:
string videoType = "video/3gpp";
XElement root = XElement.Load(file); // or .Parse(xmlstring)
var videos = root.XPath("//entry")
.Select(xElem => new YouTubeVideo()
{
Title = xElem.Get("title", "title"),
Description = xElem.Get("content", "content"),
DateUploaded = xElem.Get("published", "published"),
ThumbnailLocation = xElem.XGetElement("group/content[#type={0}]/url", "url", videoType),
ViewerLocation = xElem.XGetElement("group/content[#type={0}]/url", "url", videoType)
});
If the video type doesn't change, you can replace the XGetElement's with:
xElem.XGetElement("group/content[#type='video/3gpp']/url", "url")
Its a lot cleaner not having to specify namespaces using the library. There is the Microsoft's XPathSelectElements() and XPathSelectElement() you can look into, but they require you to specify the namespaces and don't have the nice Get methods imo. The caveat is that the library isn't a complete XPath implementation, but it does work with the above.
Struggling in vain to extract the value of the Status descendant from an XML file generated via the Azure REST API using XDocument (LINQ to XML). No issues extracting root elements using this method:
var hsname = xmldoc.Root.Element(ns + "ServiceName").Value;
Getting the descendants is proving to be a nightmare. Abbreviated XML file below - please help :-)
-<HostedService xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/windowsazure">
<Url>https://management.core.windows.net/subscriptionID/services/hostedservices/hostedservicename</Url>
<ServiceName><hostedservicename></ServiceName>
-<HostedServiceProperties>
<Description/>
<Location>South Central US</Location>
<Label>EEEEEEEEEEEEEEEEEE</Label>
</HostedServiceProperties>
-<Deployments>
-<Deployment>
<Name>DeploymentName</Name>
<DeploymentSlot>Production</DeploymentSlot>
<PrivateID>55555555555555555555</PrivateID>
<Status>Running</Status>
You haven't shown what you've tried... but I'd expect this to be fine:
string status = (string) xmldoc.Descendants(ns + "Status").FirstOrDefault();
That will give you a null value if there are no Status elements. You may want to use Single(), SingleOrDefault() etc depending on your requirements.
EDIT: Just to expand on the comment, you can make your code more robust in the face of other Status elements like this:
string status = (string) xmldoc.Descendants(ns + "HostedService")
.Descendants(ns + "ServiceName")
.Descendants(ns + "Deployments")
.Descendants(ns + "Deployment")
.Descendants(ns + "Status")
.FirstOrDefault();