I'm working on a project that requires me to build a game's rooms, items and NPC in a separate database. I've chosen XML, but something prevents me from properly parsing the XML in my C# code. What am I doing wrong?
My errors are these:
System.xml.xmlnode does not contain a definition for HasAttribute
(this goes for GetAttribute as well) and no extension method accepting 'HasAttribute' accepting a first argument of type System.Xml.XmlNode ?
This also goes for GetParentNode, and my very last line
string isMoveableStr = xmlRoom.GetAttribute("isMoveable");
somehow goes:
the name xmlRoom does not exist in the current context
Here's the method:
public void loadFromFile()
{
XmlDocument xmlDoc = new XmlDocument(); // create an xml document object in memory.
xmlDoc.Load("gamedata.xml"); // load the XML document from the specified file into the object in memory.
// Get rooms, NPCs, and items.
XmlNodeList xmlRooms = xmlDoc.GetElementsByTagName("room");
XmlNodeList xmlNPCs = xmlDoc.GetElementsByTagName("npc");
XmlNodeList xmlItems = xmlDoc.GetElementsByTagName("item");
foreach(XmlNode xmlRoom in xmlRooms) { // defaults for room:
string roomID = "";
string roomDescription = "this a standard room, nothing special about it.";
if( !xmlRoom.HasAttribute("ID") ) //http://msdn.microsoft.com/en-us/library/acwfyhc7.aspx
{
Console.WriteLine("A room was in the xml file without an ID attribute. Correct this to use the room");
continue; //skips remaining code in loop
} else {
roomID = xmlRoom.GetAttribute("id"); //http://msdn.microsoft.com/en-us/library/acwfyhc7.aspx
}
if( xmlRoom.hasAttribute("description") )
{
roomDescription = xmlRoom.GetAttribute("description");
}
Room myRoom = new Room(roomDescription, roomID); //creates a room
rooms.Add(myRoom); //adds to list with all rooms in game ;)
} foreach(XmlNode xmlNPC in xmlNPCs)
{ bool isMoveable = false;
if( !xmlNPC.hasAttribute("id") )
{
Console.WriteLine("A NPC was in the xml file, without an id attribute, correct this to spawn the npc");
continue; //skips remaining code in loop
}
XmlNode inRoom = xmlNPC.getParentNode();
string roomID = inRoom.GetAttribute("id");
if( xmlNPC.hasAttribute("isMoveable") )
{
string isMoveableStr = xmlRoom.GetAttribute("isMoveable");
if( isMoveableStr == "true" )
isMoveable = true;
}
}
}
System.Xml.XmlElement has the function you are looking for. You are getting XMLNode's. You will need to cast the nodes to XmlElement to get that function.
xmlElement = (System.Xml.XmlElement)xmlRoom;
This is not specifically germane to your question, but a response to #ChaosPandion's suggestion and your question in the comments, here is your code example using Linq to XML:
var xdoc = XDocument.Load("gamedata.xml");
var xRooms = xdoc.Descendants("room");
List<Room> rooms;
//If an element doesn't have a given attribute, the Attribute method will return null for that attribute
//Here we first check if any rooms are missing the ID attribute
if (xRooms.Any( xRoom => (string)xRoom.Attribute("ID") == null )) {
Console.WriteLine("A room was in the xml file without an ID attribute...");
} else {
rooms = (
from xRoom in xRooms
select new Room(
xRoom.Attribute("description") ?? "this a standard room, nothing special about it.",
(int)xRoom.Attribute("ID")
)
).ToList();
}
var xNPCs = xdoc.Descendants("npc");
if (xNPCs.Any( xNPC => (string)xNPC.Attribute("id") == null )) {
Console.WriteLine("A NPC was in the xml file, without an id attribute, correct this to spawn the npc");
} else {
var npcs = (
from xNPC in xNPCs
let inRoom = xNPC.Parent
select new {
xNPC,
inRoom,
isMoveable = (string)xNPC.Attribute("isMoveable") != null &&
(string)inRoom.Attribute("isMoveable") == true
}
).ToList();
}
Then you can use a simple foreach on the npcs collection:
foreach (var npc in npcs) {
Console.WriteLine(inRoom.Attribute("ID"));
Console.WriteLine(npc.IsMoveable);
}
OTOH since this code makes use of the Descendants method, which returns an collection of XElement (the type corresponding to an XML element) and not of XNode (the type corresponding to an XML node), the whole issue of a node object not having attributes is neatly sidestepped.
XmlNode does not have methods HasAttribute or GetAttribute. If you look at the MSDN entry for XmlNode, you can see the methods it has available.
http://msdn.microsoft.com/en-us/library/system.xml.xmlnode.aspx
If you use XmlNode.Attributes["ATTRIBUTE_NAME"] or in your case xmlRoom.Attributes["ID"], you should be able to find the attribute you're looking for. That is, if you would like to continue using XmlNodes.
The following link has an example of how to retrieve attributes by name from an XmlNode:
http://msdn.microsoft.com/en-us/library/1b823yx9.aspx
Related
Whilst trying to minimise the memory footprint of an XML parsing program, specifically avoiding the loading hundreds of megabytes using XElement.Load(), I came across articles suggesting using the older XmlReader e.g. here.
I need to internally reconstruct each major element as an XElement to avoid major refactoring. However, I discovered that if my source elements are directly adjoining, this approach skips every 2nd element.
I've torn down the problem to this unit-test (MSTest2 with FluentAssertions):
[DataTestMethod]
[DataRow("<data><entry>1</entry><entry>2</entry><entry>3</entry><entry>4</entry></data>")]
[DataRow("<data><entry>1</entry> <entry>2</entry> <entry>3</entry> <entry>4</entry></data>")]
public void XmlReaderCount(string input)
{
var sr = new StringReader(input);
var xml = XmlReader.Create(sr);
xml.MoveToContent();
var data = new List<string>();
while (xml.Read())
{
if (xml.LocalName == "entry" && xml.NodeType == XmlNodeType.Element)
{
var element = (XElement)System.Xml.Linq.XNode.ReadFrom(xml);
data.Add(element.Value);
}
}
data.Should()
.HaveCount(4);
}
The first (data-driven) test fails with:
Expected collection to contain 4 item(s), but found 2.
As it puts 1 and 3 into the data collection. It does loop 4 times, but every other element has an xml.NodeType of Text, not Element. The second test (with spaces between the </entry> and <entry> passes by processing all 4.
In my real world example, I can't easily change the source. I do already have a solution, inspired by another StackOverflow question so I can do the following, but it seems strange - is something wrong?
[DataTestMethod]
[DataRow("<data><entry>1</entry><entry>2</entry><entry>3</entry><entry>4</entry></data>")]
[DataRow("<data><entry>1</entry> <entry>2</entry> <entry>3</entry> <entry>4</entry></data>")]
public void XmlReaderCountSubtree(string input)
{
var data = new List<string>();
var sr = new StringReader(input);
var xml = XmlReader.Create(sr);
xml.MoveToContent();
while (xml.Read())
{
if (xml.LocalName == "entry" && xml.NodeType == XmlNodeType.Element)
{
using (var subtree = xml.ReadSubtree())
{
subtree.MoveToContent();
var content = subtree.ReadOuterXml();
var element = XElement.Parse(content);
data.Add(element.Value);
}
}
}
data.Should()
.HaveCount(4);
}
When you call ReadFrom(xml) , the state of xml is changed. Its cursor is moved forward to the next element. Your code then moves on to while (xml.Read()) and so ignores that new element completely.
With the second data set, the ignored (and uninspected) elements are the whitespace nodes so you get away with it. But basically, you reading algorithm is wrong.
A fix for your first approach, not pretty but it works:
xml.Read();
while (! xml.EOF)
{
if (xml.LocalName == "entry" && xml.NodeType == XmlNodeType.Element)
{
//using (var subtree = xml.ReadSubtree())
{
var element = (XElement)XNode.ReadFrom(xml);
data.Add(element.Value);
}
}
else
{
xml.Read();
}
}
I was trying to use a LINQ query that will iterate through an xml document. However I wanted to either use an OR statement or a string.toLower() to make sure it will always get the data it needs
I currently have:
// read all <item> tags and put the in an array.
XDocument xml = XDocument.Parse(xmlData);
var newItems = (from story in xml.Descendants("item")
select new Feed
{
Title = ((string) story.Element("title")),
Link = ((string) story.Element("link")),
Description = ((string) story.Element("description")),
PublishDate = ((string) story.Element("pubDate")),
}
).Take(20).ToList();
what I still want to change:
(E.G.) Title = ((string)story.Element("title")) needs to search case insensitive.
from story in xml.Descendants("item") select new Feed needs to search in item as well as in entry (both case-insensitive).
PS: as I am iterating through the an RSS document I cannot directly access the XML document.
thanks for input.
You can aways create extension methods for that. Here is the class that I normally use:
public static class XElementExtensions {
public static bool EqualsIgnoreCase(this XName name1, XName name2) {
return name1.Namespace == name2.Namespace &&
name1.LocalName.Equals(name2.LocalName, StringComparison.OrdinalIgnoreCase);
}
public static XElement GetChild(this XElement e, XName name) {
return e.EnumerateChildren(name).FirstOrDefault();
}
public static IEnumerable<XElement> EnumerateChildren(this XElement e, XName name) {
return e.Elements().Where(i = > i.Name.EqualsIgnoreCase(name));
}
}
Then, you can change your code to something like this:
var newItems = (from story in xml.Root.EnumerateChildren("item")
select new Feed
{
Title = ((string) story.GetChild("title")),
Link = ((string) story.GetChild("link")),
Description = ((string) story.GetChild("description")),
PublishDate = ((string) story.GetChild("pubDate")),
}).Take(20).ToList();
XML is typically defined by a schema - it should have a fixed format for the element names - so title isn't the same as TiTlE in XML terms. I don't think it's possible to do what you want, using .Element
I'm modifying .docx documents with DocumentFormat.OpenXml library. I know element ordering is important, otherwise the document will not pass schema validation and might result a document that can't be opened in Word.
Now I need to add a DocumentProtection element to DocumentSettingsPart. And I need to insert this child element in the right place inside of a parent.
The schema looks like this:
There are quite a lot of possible ordering of child elements. At the moment I'm adding this element like this:
var documentProtection = new DocumentProtection()
{
// do the configuration
};
DocumentSettingsPart settings = doc.MainDocumentPart.DocumentSettingsPart;
var rootElement = settings.RootElement;
var prevElement =
rootElement.GetFirstChild<DoNotTrackFormatting>() ??
rootElement.GetFirstChild<DoNotTrackMoves>() ??
rootElement.GetFirstChild<TrackRevisions>() ??
rootElement.GetFirstChild<RevisionView>() ??
rootElement.GetFirstChild<DocumentType>() ??
rootElement.GetFirstChild<StylePaneSortMethods>() ??
// SNIP
rootElement.GetFirstChild<Zoom>() ??
rootElement.GetFirstChild<View>() ??
(OpenXmlLeafElement)rootElement.GetFirstChild<WriteProtection>();
rootElement.InsertAfter(documentProtection, prevElement);
I.e. I'm trying to find if any possible element that should go before mine already exists in the document. And then insert DocumentProtection after that element. And given amount of elements this list gets pretty boring.
Is there a better way to add DocumentProtection so it is schema compliant and does not involve enumeration of all possible elements?
There isn't a nice way to achieve what you want. You'll have to tinker with the collection and you're responsible for keeping the order correct.
Using ILSpy on the Settings class you'll find that the implementors used a helper method SetElement<T> on the base class that takes a position and an instance to insert.
Unfortunately that helper method is marked internal so we can't leverage it if you try to subclass Settings. Instead I re-implemented the needed functionality so you'll have a subclass of Settings that does offer a property for DocumentProtection but uses the re-implemented solution to find the correct location to insert the node:
SettingsExt
public class SettingsExt: Settings
{
// contruct based on XML
public SettingsExt(string outerXml)
:base(outerXml)
{
// empty
}
public DocumentProtection DocumentProtection
{
// get is easy
get
{
return this.GetFirstChild<DocumentProtection>();
}
// reimplemented SetElement based on
// reversed engineered Settings class
set
{
// eleTagNames is a static string[] declared later
// it holds all the names of the elements in the right order
int sequenceNumber = eleTagNames
.Select((s, i) => new { s= s, idx = i })
.Where(s => s.s == "documentProtection")
.Select((s) => s.idx)
.First();
OpenXmlElement openXmlElement = this.FirstChild;
OpenXmlElement refChild = null;
while (openXmlElement != null)
{
// a bit naive
int currentSequence = eleTagNames
.Select((s, i) => new { s = s, idx = i })
.Where(s => s.s == openXmlElement.LocalName)
.Select((s) => s.idx)
.First(); ;
if (currentSequence == sequenceNumber)
{
if (openXmlElement is DocumentProtection)
{
refChild = openXmlElement.PreviousSibling();
this.RemoveChild<OpenXmlElement>(openXmlElement);
break;
}
refChild = openXmlElement;
}
else
{
if (currentSequence > sequenceNumber)
{
break;
}
refChild = openXmlElement;
}
openXmlElement = openXmlElement.NextSibling();
}
if (value != null)
{
this.InsertAfter(value, refChild);
}
}
}
// order of elements in the sequence!
static readonly string[] eleTagNames = new string[]
{
"writeProtection",
"view",
"zoom",
"removePersonalInformation",
"removeDateAndTime",
"doNotDisplayPageBoundaries",
"displayBackgroundShape",
"printPostScriptOverText",
"printFractionalCharacterWidth",
"printFormsData",
"embedTrueTypeFonts",
"embedSystemFonts",
"saveSubsetFonts",
"saveFormsData",
"mirrorMargins",
"alignBordersAndEdges",
"bordersDoNotSurroundHeader",
"bordersDoNotSurroundFooter",
"gutterAtTop",
"hideSpellingErrors",
"hideGrammaticalErrors",
"activeWritingStyle",
"proofState",
"formsDesign",
"attachedTemplate",
"linkStyles",
"stylePaneFormatFilter",
"stylePaneSortMethod",
"documentType",
"mailMerge",
"revisionView",
"trackRevisions",
"doNotTrackMoves",
"doNotTrackFormatting",
"documentProtection",
"autoFormatOverride",
"styleLockTheme",
"styleLockQFSet",
"defaultTabStop",
"autoHyphenation",
"consecutiveHyphenLimit",
"hyphenationZone",
"doNotHyphenateCaps",
"showEnvelope",
"summaryLength",
"clickAndTypeStyle",
"defaultTableStyle",
"evenAndOddHeaders",
"bookFoldRevPrinting",
"bookFoldPrinting",
"bookFoldPrintingSheets",
"drawingGridHorizontalSpacing",
"drawingGridVerticalSpacing",
"displayHorizontalDrawingGridEvery",
"displayVerticalDrawingGridEvery",
"doNotUseMarginsForDrawingGridOrigin",
"drawingGridHorizontalOrigin",
"drawingGridVerticalOrigin",
"doNotShadeFormData",
"noPunctuationKerning",
"characterSpacingControl",
"printTwoOnOne",
"strictFirstAndLastChars",
"noLineBreaksAfter",
"noLineBreaksBefore",
"savePreviewPicture",
"doNotValidateAgainstSchema",
"saveInvalidXml",
"ignoreMixedContent",
"alwaysShowPlaceholderText",
"doNotDemarcateInvalidXml",
"saveXmlDataOnly",
"useXSLTWhenSaving",
"saveThroughXslt",
"showXMLTags",
"alwaysMergeEmptyNamespace",
"updateFields",
"hdrShapeDefaults",
"footnotePr",
"endnotePr",
"compat",
"docVars",
"rsids",
"mathPr",
"uiCompat97To2003",
"attachedSchema",
"themeFontLang",
"clrSchemeMapping",
"doNotIncludeSubdocsInStats",
"doNotAutoCompressPictures",
"forceUpgrade",
"captions",
"readModeInkLockDown",
"smartTagType",
"schemaLibrary",
"shapeDefaults",
"doNotEmbedSmartTags",
"decimalSymbol",
"listSeparator",
"docId",
"discardImageEditingData",
"defaultImageDpi",
"conflictMode"};
}
A typical usage scenario with this class is as follows:
using (var doc = WordprocessingDocument.Open(#"c:\tmp\test.docx", true))
{
var documentProtection = new DocumentProtection()
{
Formatting = DocumentFormat.OpenXml.OnOffValue.FromBoolean(true)
};
DocumentSettingsPart settings = doc.MainDocumentPart.DocumentSettingsPart;
// instantiate our ExtendedSettings class based on the
// original Settings
var extset = new SettingsExt(settings.Settings.OuterXml);
// new or existing?
if (extset.DocumentProtection == null)
{
extset.DocumentProtection = documentProtection;
}
else
{
// replace existing values
}
// this is key to make sure our own DOMTree is saved!
// don't forget this
settings.Settings = extset;
}
Im trying to read a certain attributes from following xml file (as console program)
http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml
As you see inside 'forecast' element there are multiple 'time' elements. What I want is to pick certain 'time' element and then pick given thing inside of it (lets say 'symbol') and print all/any attributes it has.
I want to be able to control which 'time' element I pick and which attributes I want to print.
This far all I have managed to do is to print every 'time' element and their attributes and also I managed to print every attribute inside of given 'time' element. But I just can't figure how to control it.
With the following code, I can print everything inside the first 'time' element. Item(0) is the index of element and the for loop makes sure that I don't get empty lines. As you can see from xml file, some 'time' elements has different amount of attributes inside of them so I guess I need to call them By name insted of index.
static void xmlReader()
{
int i;
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(parseLink());
foreach (XmlNode xmlNode in xmlDoc.DocumentElement.GetElementsByTagName("time").Item(0))
for (i = 0; i < xmlNode.Attributes.Count; i++)
{
Console.WriteLine(xmlNode.Attributes[i].Value);
}
Console.ReadKey();
}
Use Linq2Xml, it's much easier and convenient.
public static void Main()
{
var forecast = XDocument.Load(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml")
.Root
.Element("forecast");
foreach (var time in forecast.Elements("time")
.Where(e => e.Element("clouds")
.Attribute("value")
.Value == "overcast clouds"))
{
Console.WriteLine(time.Element("symbol").Attribute("name").Value);
}
}
Using XDocument is a bit easier here...
private static void XmlOutput()
{
var filePathAndName = #"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml";
var xmlDoc = XDocument.Load(filePathAndName);
// Get list of Nodes matching the "time" name or any other filters you wish.
var nodes = xmlDoc.Descendants().Where(nd => nd.Name.LocalName == "time");
// Filter the node list to only those where the date is as specified (and any other filters you wish).
// This could be done in the initial linq query - just making it clearer here.
nodes = nodes.Where(nd => nd.Attributes().Any(cnd => cnd.Name.LocalName == "from" && cnd.Value.Contains("2015-03-07")));
foreach (XElement element in nodes)
{
// For each descendant node where named "symbol"...
foreach(var node in element.Descendants().Where(nd => nd.Name.LocalName == "symbol"))
{
// Write out these particular attributes ("number" and "name")
string output = "";
output += node.Attributes().FirstOrDefault(nd => nd.Name.LocalName == "number").Value;
output += ", " + node.Attributes().FirstOrDefault(nd => nd.Name.LocalName == "name").Value;
Console.WriteLine(output);
}
}
Console.ReadKey();
}
Similar #aush's response, but with some formatting
var doc = XDocument.Load(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml");
var forecastEl = doc.Root.Element(XName.Get("forecast"));
var timeNodes = from e in forecastEl.Elements("time")
where e.Element("symbol")
.Attribute(XName.Get("name"))
.Value == "light snow"
select e;
var colFormat = "{0,-20} {1,-20} {2,-30}";
Console.WriteLine(colFormat, "TimeFrom", "TimeTo", "SymbolName");
foreach(var time in timeNodes)
Console.WriteLine(colFormat
, time.Attribute("from").Value
, time.Attribute("to").Value
, time.Element("symbol").Attribute("name").Value);
Results:
TimeFrom TimeTo SymbolName
2015-03-07T12:00:00 2015-03-07T15:00:00 light snow
2015-03-07T15:00:00 2015-03-07T18:00:00 light snow
You can use XmlReader (tutorial) as flowing,
Here I get from attribute from time element and name attribute sybol element.. And they are for same Time element. Also added extracting example for value of cloud
using (XmlReader reader = XmlReader.Create(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml"))
{
// Walk through all Elements
while (reader.Read())
{
// If we meet time element ; go inside and walk
if (reader.Name == "time")
{
Console.WriteLine("A new TIME ");
// Extract from attribute
String from = reader["from"];
if (from != null)
{
Console.WriteLine("\t From : " + from);
}
// Now walk through all elements inside same Time element
// Here I use do-while ; what we check is End element of time : </time> .. when we walk till we meet </time> we are inside children of same Time
// That mean we start from <time> and walk till we meet </time>
do
{
reader.Read();
if (reader.Name == "symbol")
{
// You can use this approach for any Attribute in symbol Element
String name = reader["name"];
if (name != null)
{
Console.WriteLine("\t Symbol name :" + name);
}
}
if (reader.Name == "clouds")
{
String clouds = reader["value"];
if (clouds != null)
{
Console.WriteLine("\t\t Clouds value : " + clouds);
}
}
} while (reader.NodeType != XmlNodeType.EndElement && reader.Name != "time");
}
}
}
Simply try this on your Console program..
Using my xml library here, you can search for an actual DateTime value like this:
XElement root = XElement.Load(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml");
var search = DateTime.Parse("2015-03-07T21:00:00");
XElement time = root.XPathElement("//time[#from >= {0} and #to > {1}]", search, search);
var value = time.ToString();
I don't know why I'm having so much trouble with this, but I'm hoping someone can get me pointed in the right direction.
I have these few lines of code :
var xDoc = new XmlDocument();
xDoc.LoadXml(xelementVar.ToString());
if (xDoc.ChildNodes[0].HasChildNodes)
{
for (int i = 0; i < xDoc.ChildNodes[0].ChildNodes.Count; i++)
{
var sFormatId = xDoc.ChildNodes[0].ChildNodes[i].Attributes["formatID"].Value;
// Do some stuff
}
// Do some more stuff
}
The problem is that the xDoc I'm getting doesn't always have the formatID node, so I end up getting a null reference exception, although 99% of the time it works perfectly fine.
My question :
How can I check if the formatID node exists before I try to read the Value out of it?
if a node does not exist, it returns null.
if (xDoc.ChildNodes[0].ChildNode[i].Attributes["formatID"] != null)
sFormatId = xDoc.ChildNodes[0].ChildNodes[i].Attributes["formatID"].Value;
of you can do it a shortcut way
var sFormatId = xDoc.ChildNodes[0].ChildNodes[i].Attributes["formatID"] != null ? xDoc.ChildNodes[0].ChildNodes[i].Attributes["formatID"].Value : "formatID not exist";
The format is like this.
var variable = condition ? A : B;
this is basically saying that if the condition is true, then variable = A, otherwise, variable = B.
Could you use DefaultIfEmpty()?
E.g
var sFormatId = xDoc.ChildNodes[0].ChildNodes[i].Attributes["formatID"]
.Value.DefaultIfEmpty("not found").Single();
Or as others have suggested, check that the attribute is not null:
if (xDoc.ChildNodes[0].ChildNodes[i].Attributes["formatID"] != null)
You can also do this:
if (xDoc.ChildNodes[0].HasChildNodes)
{
foreach (XmlNode item in xDoc.ChildNodes[0].ChildNodes)
{
string sFormatId;
if(item.Attributes["formatID"] != null)
sFormatId = item.Attributes["formatID"].Value;
// Do some stuff
}
}
you can check that like this
if(null != xDoc.ChildNodes[0].ChildNode[i].Attributes["formatID"])
I think a cleaner way to do this would be:
var xDoc = new XmlDocument();
xDoc.LoadXml(xelementVar.ToString());
foreach(XmlNode formatId in xDoc.SelectNodes("/*/*/#formatID"))
{
string formatIdVal = formatId.Value; // guaranteed to be non-null
// do stuff with formatIdVal
}
In most case we face issues because an XPath does not exists, It returns null and our code breaks because of the InnerText.
You can only check XPath exists or not and it returns null when does not exist.
if(XMLDoc.SelectSingleNode("XPath") <> null)
ErrorCode = XMLDoc.SelectSingleNode("XPath").InnerText