Find element with specific attribute in xml? - c#

I have the following xml:
<?xml version="1.0" encoding="utf-8" ?>
<layout>
<menu name="Employees" url="Employees.aspx" admin="0">
</menu>
<menu name="Projects" url="Projects.aspx" admin="1">
</menu>
<menu name="Cases" url="Cases.aspx" admin="1">
</menu>
<menu name="CaseView" url="CaseView.aspx" admin="1" hidden="1" parent="Projects">
</menu>
<menu name="Management" url="" admin="1">
<item name="Groups" url="Groups.aspx" admin="1" parent="Management"/>
<item name="Statuses" url="Statuses.aspx" admin="1"/>
</menu>
</layout>
Here I have CaseView and Groups that both have a 'parent' attribute.
Currently I iterate like this:
IEnumerable<XElement> menus =
doc.Element("layout").Elements();
foreach (var menu in menus)
{
string name = menu.Attribute("name").Value;
string active = "";
string url = menu.Attribute("url").Value;
if(activePage == url)
{
active = "class=\"active\"";
}
...
What I want is:
if(activePage == url || ActiveIsChildOf(name, activePage))
{
active = "class=\"active\"";
}
Essentially this method needs to find if an element with activePage as its url attribute exists. If it does, see if it has a parent attribute; if it does, check if the parent == name.
Is there some way to find an element by attribute or something?
ex:
XElement e = doc.GetByAttribute("url",activePage)
Thanks

Since you are using Linq to XML, you can use Descendants method - it returns all child elements, not just the direct children. After that, you can use LINQ to filter the results.
XDocument doc;
string activePage;
var activeMenu = doc.Descendants("menu")
.FirstOrDefault(o => o.Attribute("url").Value == activePage);
You might need to check if o.Attribute("url") does not return null (it does when the attribute does not exist) if you cannot guarantee that the source XML does not have such attribute for all menu elements.
You can also skip the argument to Descendants() to check all elements - in your sample data that would allow you to check both menu and item elements. For example:
var activeMenu = doc.Descendants()
.Where(o => o.Name == "menu" || o.Name == "item")
.FirstOrDefault(o => o.Attribute("url").Value == activePage);

If xpath is too cryptic, you can use LINQ:
IEnumerable<XElement> hits =
(from el in XMLDoc.root.Elements("item")
where (string)el.Attribute("url") == activePage
select el);
or like this:
XElement xml = XElement.Load(file);
XElement xele = xml.Elements("item").FirstOrDefault(e => ((string)e.Attribute("url")) == activePage);
if(null != xele )
{
// do something with it
}
And you probably want it case-insensitive:
XElement xml = XElement.Load(file);
XElement xele = xml.Elements("item").FirstOrDefault(e => StringComparer.OrdinalIgnoreCase.Equals((string)e.Attribute("url") , activePage));
if(null != xele )
{
// do something with it
}
If you want both menu and item, use this:
XElement xml = XElement.Load(file);
XElement xele = xml.Elements().FirstOrDefault(e => StringComparer.OrdinalIgnoreCase.Equals((string)e.Attribute("url") , activePage));
if(null != xele )
{
// do something with it
}

You can simply use xPath. It's a query language for XML.
You can formulate something like this :
var xDoc = new XmlDocument();
xDoc.Load("XmlFile.xml");
//Fetch your node here
XmlNode = xDoc.SelectSingleNode(/layout/menu[#url='activepage'][1]);
It returns a set of node and the index 1 is to get the first node of the given set.
You can always use xDoc.SelectNodes if you want all the matching nodes.
Since you are using LINQ you can simply include System.Xml.XPath and select nodes with XPathSelectElement or XPathSelectElements.

You can do that with XPath:
doc.SelectNodes("//*[#url='" + activePage + "']")
It will return all document items that have activePage as url attribute.

A case insensitive search example, converting xml to a dictionary:
Dim expandos = XDocument.Parse(Request("Xml")).Root.Elements.Select(
Function(e)
Dim expando As Object = New ExpandoObject,
dic = e.Attributes.ToDictionary(Function(a) a.Name.LocalName, Function(a) a.Value,
StringComparer.InvariantCultureIgnoreCase)
expando.PedidoId = dic("PedidoId")
expando.FichaTecnicaModeloId = dic("FichaTecnicaModeloId")
expando.Comodo = dic("Comodo")
expando.Cliente = dic("Cliente")
Return expando
End Function)

Related

Fetch value from child's child nodes

I have xml structure as below and i try to fetch nodes using linq to xml. I am kind of stuck how to read child's child node also I will have to get all child2 node values as coma separated values. And also have to read any dynamic nodes present under child node.
Here are sample xml's.
File 1:
<parent>
<doc>
<order>testorder</order>
<preorder>yes</preorder>
<child>
<childs1>
<child2>abc</child2>
<child2>efg</child2>
</childs1>
<preview>current</preview>
<provision>enable</provision>
</child>
</doc>
</parent>
File 2 :
<parent>
<doc>
<order>testorder</order>
<preorder>yes</preorder>
<child>
<preview>current</preview>
<provision>enable</provision>
<other>abc</other>
</child>
</doc>
</parent>
My sudo code :
XDocument xdoc = XDocument.Load(file);
var customers =
from cust in xdoc.Descendants("doc")
select new
{
Title = cust.Element("order").Value,
preorder = cust.Element("preorder").Value,
innernode= from inner in cust.Elements("child")
select new {
site = (inner.Element("preview") != null) ? inner.Element("preview").Value : null,
node=(inner.Element("childs1") != null) ? string.Join(",", from seg in inner.Elements("child2") select seg.Value) :null,
Should store if any extra dynamic nodes are there ,
},
};
foreach(var item in customers)
{
// read all nodes
}
Your code to fetch child2 is trying to look in doc's descendants, but you want to look at childs1's descendants. As for dynamic fields you can do it by creating a dictionary to get any elements that don't match the hardcoded ones, as i did below.
var customers =
from cust in xdoc.Descendants("doc")
select new
{
Title = cust.Element("order").Value,
preorder = cust.Element("preorder").Value,
innernode = from inner in cust.Elements("child")
select new
{
site = (inner.Element("preview") != null) ? inner.Element("preview").Value : null,
node = (inner.Element("childs1") != null) ? string.Join(",", from seg in inner.Elements("childs1").Elements("child2") select seg.Value) : null,
dynamicElements = inner.Elements()?.Where(e => e.Name != "preview" && e.Name != "childs1")?.ToDictionary(e => e.Name, e => e.Value)
},
};

XML element removal, just need to delete one element not the parent

Basically what I am trying to do is remove a VSLOC from the list. I don't want to remove everything that belongs to it.
<?xml version="1.0"?>
<GarageNumbers>
<G554>
<id>G554</id>
<VSLOC>V002</VSLOC>
<VSLOC>V003</VSLOC>
<VSLOC>V002</VSLOC>
</G554>
<G566>
<id>G566</id>
<VSLOC>V002</VSLOC>
<VSLOC>V003</VSLOC>
<VSLOC>V002</VSLOC>
</G566>
<G572>
<id>G572</id>
<VSLOC>V001</VSLOC>
<VSLOC>V002</VSLOC>
</G572>
</GarageNumbers>
So, what I have setup is a combobox that I select a G# from which brings up all the VSLOC associated with it in a Listbox. What I need to do is to select a item from the list box and remove the line from the listbox and from the xml document using a button. I have all this setup but when I hit the button it deletes G554 and all the elements with in.
So if I want to select V002 from the list in G554 I want it to just remove that VSLOC with that innertext.
XmlDocument xDoc = new XmlDocument();
xDoc.Load(Application.StartupPath + "/xmlData.xml");
foreach (XmlNode xNode in xDoc.SelectNodes("GarageNumbers/G554"))
if (xNode.SelectSingleNode("VSLOC").InnerText == "V002")
xNode.ParentNode.RemoveChild(xNode);
xDoc.Save(Application.StartupPath + "/xmlData.xml");
You should be able to drill down to the desired elements then remove them. For example, assuming your XML is in an XElement, this approach would work:
string targetCategory = "G554";
string vsloc = "V002";
xml.Element(targetCategory)
.Elements("VSLOC")
.Where(e => e.Value == vsloc)
.Remove();
If you're using an XDocument then add the Root property: xml.Root
var xDoc = XDocument.Load(fname);
var node = xDoc.Descendants("VSLOC")
.Where(e => (string)e.Parent.Element("id") == "G554")
.FirstOrDefault();
if (node != null) node.Remove();
xDoc.Save(fname);

Locating XML node by child node value inside of it, and changing another value

Three part question.
Is it possible to locate a specific XML node by a child inside of it to retrieve other children of the parent? Example:
<House>
<Kitchen>
<Appliance>
<Name>Refrigerator</Name>
<Brand>Maytag</Brand>
<Model>F2039-39</Model>
</Appliance>
<Appliance>
<Name>Toaster</Name>
<Brand>Black and Decker</Brand>
<Model>B8d-k30</Model>
</Appliance>
</Kitchen>
</House>
So for this, I would like to locate the appropriate Appliance node by searching for "Refrigerator" or "Toaster", and retrieve the brand from it.
The second part of this question is this: Is this a stupid way to do it? Would using an attribute in the Appliance tag make this a lot easier? If so, how would I locate it that way?
As for the third part, once I locate the Appliance, how would I go about changing say, the Model, of that particular appliance?
Using XLinq, you can perform this query fairly naturally:
// Given:
// var xdoc = XDocument.Load(...);
// string applianceName = "Toaster";
// Find the appliance node who has a sub-element <Name> matching the appliance
var app = xdoc.Root
.Descendants("Appliance")
.SingleOrDefault(e => (string)e.Element("Name") == applianceName);
// If we've found one and it matches, make a change
if (app != null)
{
if (((string)app.Element("Model")).StartsWith("B8d-k30"))
{
app.Element("Model").Value = "B8d-k30 Mark II";
}
}
xdoc.Save(#"output.xml"); // save changes back to the document
Well if you are using XmlDocument
foreach(XmlNode applianceNode in
myDocument.DocumentElement.SelectNodes("Kitchen/Applicance[Name='Refrigerator']")
{
XmlNode modelNode = applicianceNode.SelectSingleNode("Model").InnerText = SomeOtherValue;
}
if you made the name tag an attribute (applicanceName) it would make little difference to this.
foreach(XmlNode applianceNode in
myDocument.DocumentElement.SelectNodes("Kitchen/Applicance[#applianceName='Refrigerator']")
{
// ...
}
string xml = #"<House>
<Kitchen>
<Appliance>
<Name>Refrigerator</Name>
<Brand>Maytag</Brand>
<Model>F2039-39</Model>
</Appliance>
<Appliance>
<Name>Toaster</Name>
<Brand>Black and Decker</Brand>
<Model>B8d-k30</Model>
</Appliance>
</Kitchen>
</House>";
XDocument xdoc = XDocument.Parse(xml);
string newModel = "B8d-k45";
var matchingElement = (from appliance in xdoc.Descendants("Appliance")
where appliance.Element("Name").Value == "Toaster"
select appliance).FirstOrDefault();
if (matchingElement != null)
{
matchingElement.Element("Model").Value = newModel;
}
Console.WriteLine(xdoc.ToString());
Necromancing.
Yes, it's even simpler with XPath, and works completely without Linq:
Just use .. to get to the parent node (on the second thought, Linq will be easier when using "ordinalignorecase")
public static void CreateNewHouse()
{
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
doc.XmlResolver = null;
doc.Load(#"d:\House.xml");
foreach (System.Xml.XmlNode modelNode in doc.DocumentElement
.SelectNodes("/House/Kitchen/Appliance/Name[text()='Refrigerator']/../Model"))
{
modelNode.InnerText = "A New Value";
}
doc.Save(#"d:\MyHouse.xml");
}
MyHouse.xml:
<House>
<Kitchen>
<Appliance>
<Name>Refrigerator</Name>
<Brand>Maytag</Brand>
<Model>A New Value</Model>
</Appliance>
<Appliance>
<Name>Toaster</Name>
<Brand>Black and Decker</Brand>
<Model>B8d-k30</Model>
</Appliance>
</Kitchen>
</House>
If you need it case-insensitive, replace text() with this:
translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')
(for ASCII/english-only) and of course change "Refrigerator" to lowercase ("refrigerator")
If the XML-document has a default-namespace, you need to supply it in Select*Node, e.g.
xnImageTag.SelectSingleNode("./dft:Source", nsmgr);
where
System.Xml.XmlNamespaceManager nsmgr = GetReportNamespaceManager(doc);
public static System.Xml.XmlNamespaceManager GetReportNamespaceManager(System.Xml.XmlDocument doc)
{
if (doc == null)
throw new ArgumentNullException("doc");
System.Xml.XmlNamespaceManager nsmgr = new System.Xml.XmlNamespaceManager(doc.NameTable);
// <Report xmlns="http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition" xmlns:rd="http://schemas.microsoft.com/SQLServer/reporting/reportdesigner">
if (doc.DocumentElement != null)
{
string strNamespace = doc.DocumentElement.NamespaceURI;
System.Console.WriteLine(strNamespace);
nsmgr.AddNamespace("dft", strNamespace);
return nsmgr;
}
nsmgr.AddNamespace("dft", "http://schemas.microsoft.com/sqlserver/reporting/2005/01/reportdefinition");
// nsmgr.AddNamespace("dft", "http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition");
return nsmgr;
} // End Function GetReportNamespaceManager

Linq to XML - Find an element

I am sure that this is basic and probably was asked before, but I am only starting using Linq to XML.
I have a simple XML that i need to read and write to.
<Documents>
...
<Document>
<GUID>09a1f55f-c248-44cd-9460-c0aab7c017c9-0</GUID>
<ArchiveTime>2012-05-15T14:27:58.5270023+02:00</ArchiveTime>
<ArchiveTimeUtc>2012-05-15T12:27:58.5270023Z</ArchiveTimeUtc>
<IndexDatas>
<IndexData>
<Name>Name1</Name>
<Value>Some value</Value>
<DataType>1</DataType>
<CreationTime>2012-05-15T14:27:39.6427753+02:00</CreationTime>
<CreationTimeUtc>2012-05-15T12:27:39.6427753Z</CreationTimeUtc>
</IndexData>
<IndexData>
<Name>Name2</Name>
<Value>Some value</Value>
<DataType>3</DataType>
<CreationTime>2012-05-15T14:27:39.6427753+02:00</CreationTime>
<CreationTimeUtc>2012-05-15T12:27:39.6427753Z</CreationTimeUtc>
</IndexData>
...
</IndexDatas>
</Document>
...
</Documents>
I have a "Documents" node that contains bunch of "Document" nodes.
I have GUID of the document and a "IndexData" name.
I need to find the document by GUID and check if it has "IndexData" with some name.
If it does not have it i need to add it.
Any help would be apreciated, as i have problem with reading and searching trough elements.
Currently I am trying to use (in C#):
IEnumerable<XElement> xmlDocuments = from c in XElement
.Load(filePath)
.Elements("Documents")
select c;
// fetch document
XElement documentElementToEdit = (from c in xmlDocuments where
(string)c.Element("GUID").Value == GUID select c).Single();
EDIT
xmlDocuments.Element("Documents").Elements("Document")
This returns no result, even tho xmlDocuments.Element("Documents") does. It looks like i cant get Document nodes from Documents node.
You can find those docs (docs without related name in index data) with below code, after that you could add your elements to the end of IndexData elements.
var relatedDocs = doc.Elements("Document")
.Where(x=>x.Element("GUID").Value == givenValue)
.Where(x=>!x.Element("IndexDatas")
.Elements("IndexData")
.Any(x=>x.Element("Name") == someValue);
This should work:
var x = XDocument.Load(filePath);
// guid in your sample xml is not a valid guid, so I changed it to a random valid one
var requiredGuid = new Guid("E61D174C-9048-438D-A532-17311F57ED9B");
var requiredName = "Name1";
var doc = x.Root
.Elements("Document")
.Where(d => (Guid)d.Element("GUID") == requiredGuid)
.FirstOrDefault();
if(doc != null)
{
var data = doc.Element("IndexDatas")
.Elements("IndexData")
.Where(d => (string)d.Element("Name") == requiredName)
.FirstOrDefault();
if(data != null)
{
// index data found
}
else
{
// index data not found
}
}
else
{
// document not found
}

How to get XElement's value and not value of all child-nodes?

Sample xml:
<parent>
<child>test1</child>
<child>test2</child>
</parent>
If I look for parent.Value where parent is XElement, I get "test1test2".
What I am expecting is "". (since there is no text/value for .
What property of XElement should I be looking for?
When looking for text data in the <parent> element you should look for child nodes that have NodeType properties equal to XmlNodeType.Text. These nodes will be of type XText. The following sample illustrates this:
var p = XElement
.Parse("<parent>Hello<child>test1</child>World<child>test2</child>!</parent>");
var textNodes = from c in p.Nodes()
where c.NodeType == XmlNodeType.Text
select (XText)c;
foreach (var t in textNodes)
{
Console.WriteLine(t.Value);
}
Update: if all you want is the first Text node, if any, here's an example using LINQ method calls instead of query comprehension syntax:
var firstTextNode = p.Nodes().OfType<XText>().FirstOrDefault();
if (firstTextNode != null)
{
var textValue = firstTextNode.Value;
...do something interesting with the value
}
Note: using First() or FirstOrDefault() will be more performant than Count() > 0 in this scenario. Count always enumerates the whole collection while FirstOrDefault() will only enumerate until a match is found.
It is amazing that a coder somewhere at Microsoft thought that returning all text values as a concatenated and undelimited string would be useful. Luckily, another MS developer wrote an XElement extension to return what they call the "Shallow Value" of the text node here. For those who get the willies from clicking on links, the function is below...
public static string ShallowValue(this XElement element)
{
return element
.Nodes()
.OfType<XText>()
.Aggregate(new StringBuilder(),
(s, c) => s.Append(c),
s => s.ToString());
}
And you call it like this, because it gives you all the whitespace too (or, come to think of it, you could trim it in the extension, whatever)
// element is a var in your code of type XElement ...
string myTextContent = element.ShallowValue().Trim();
You could concatenate the value of all XText nodes in parent:
XElement parent = XElement.Parse(
#"<parent>Hello<child>test1</child>World<child>test2</child>!</parent>");
string result = string.Concat(
parent.Nodes().OfType<XText>().Select(t => t.Value));
// result == "HelloWorld!"
For comparison:
// parent.Value == "Hellotest1Worldtest2!"
// (parent.HasElements ? "" : parent.Value) == ""
msdn says:
A String that contains all of the text content of this element. If there are multiple text nodes, they will be concatenated.
So the behaviour is to be expected.
You could solve your problem by doing:
string textContent = parent.HasElements ? "" : parent.Value;
// Create the XElement
XElement parent = XElement.Parse(
#"<parent>Hello<child>test1</child>World<child>test2</child>!</parent>");
// Make a copy
XElement temp=new XElement(parent);
// remove all elements but root
temp.RemoveNodes();
// now, do something with temp.value, e.g.
Console.WriteLine(temp.value);

Categories

Resources