This post is geared more towards beginners. If you want to skip to the examples goto the “Practical Examples“ heading. Source code is at the bottom of the post 📌
I usually consider working with XML in statically typed languages to be a form of penance. Thankfully once you understand it, working with XML in C# is not too bad. There are 8 friends you need to become familiar with when using the System.Xml.Linq library, and a handful of getters and functions.
XDocument
Load()
Declaration
FirstNode
XDeclaration
Encoding
Version
XElement
Name
Attribute()
Descendants()
Value
XNode
XAttribute
Name
Value
XName
ToString()
IEnumerable<XElement>
IEnumerable<XAttribute>
Lets look at our sample data.
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="40" height="15" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx" />
<layer id="1" name="Tile Layer 1" width="40" height="15">
<data encoding="csv">0,0,0
0,0,0
1,0,1
</data>
</layer>
</map>
The astute among you might notice that this is the TMX format1 for the Tiled Map editor. I’m learning C# by making a small Monogame2 because leetcode, and project Euler get old. But why don’t you use TiledCS3 or MonoGame.Extended4? I’ll be honest with you I’m too smooth brained to understand how to use them.
Setting up to shave the Yak 🐃
Our first stop will be the XDocument
class overview5 in the .NET documentation by Microsoft. This will form the basis of our understanding. There is a note about XDocument in the docs that says…
In many circumstances, you can work directly with XElement
I want to grab <?xml version="1.0" encoding="UTF-8"?>
which is called the declaration, so I’ll stick with XDocument, but the rest of our work will be with XElements. The first thing we will need to do is load in our data so we can work with it. That’s simple enough. Firing up Visual Studio 2022 in a console app we are greeted with this…
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
I quickly copy over my TMX file to the project, and I’m left with this project structure
The nice thing about .NET 6 is that you don’t need to write the namespace→class→main boiler plate for program.cs as
it’s implied. So we can just add this to our program.cs
file.
using System.Xml.Linq;
XDocument doc = XDocument.Load("map01.tmx");
But wait! If you’re a noob like me you’ll get this error
What FileNotFoundException
? It’s right next to my program.cs
file! Turns out there are two ways to solve this. One way is to right click your map01.tmx
file in the solution explorer, and then choose copy full path
. Then change the path of XDocument.Load()
to…
XDocument doc = XDocument.Load(@"C:\Users\decre\source\repos\XmlPractice\map01.tmx");
By the way the little “@” prefixing the string means don’t treat “\” as an escape character.
The other way is to look carefully at where .NET is looking for the file. It’s looking for the tmx file where XmlPractice.exe
is located, which is actually a few folders down in bin\Debug\net6.0\.
If you open file explorer at that location…
You’ll see map01.tmx
is nowhere to be found. But if you go back to the XmlPractice
folder you will see that file. If you run the program in release mode instead of being in \bin\Debug\net6.0\
your exe will be in \bin\Release\net6.0\
. To find the file regardless of whether we run in release or debug mode, we can use a relative path to the file from our exe using the string @"..\..\..\map01.tmx”.
“..\” just means go up one directory and look in there. So three “..\”
just mean look in the third directory from the exe to find the file.
Practical Examples 🎯
Now that we have some background we can look at how to get various components of our XML.
How do I get the declaration?
Now that we have the document properly loaded it’s time to start extracting data. XML files start with a declaration giving you its encoding and version. In our file it looks like this…
<?xml version="1.0" encoding="UTF-8"?>
To get the declaration we ask for it by calling the Declaration getter.
XDeclaration dec = doc.Declaration;
This captures the block of XML in the variable named dec
. Version
is an attribute of the <xml> declaration tag and “1.0” is the value of the attribute version
. So if we wanted to get the value of version (“1.0”) we could just do
string version = dec.Version;
likewise to get the encoding we can do dec.Encoding
. That handles the declaration :).
How do we get the Root node?
XElement root = (XElement)doc.FirstNode;
The root node is the first node after the declaration from which all other XML tags descend. We get this by using the FirstNode
property of our XDocument
. We cast doc.FirstNode
to an XElement
because FirstNode
returns a generic XNode
which is the base class for a lot of the other XClasses, and we can’t really do much with just an XNode
. But an XElement
on the other hand has a few useful properties. To get the name of the root we can just type
XName rootName = root.Name;
which will return “map” as a XName
object. XNames
are the names of tags in XML. Our XName
can be printed out using Console.WriteLine(rootName)
or coerced into a string with its ToString()
property. That’s usually all you need from an XName
.
How do I get the attributes of the Root Node?
As you can see the root node has 11 attributes associated with it.
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="40" height="15" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
our root variable is an XElement
and so has some properties that will allows us to access the values in it’s interior. There is the Attribute()
method which will allow us to specify the single attribute that we want. When we call it with the name of an attribute in our root tag, we get the attribute back.
XAttribute ortho = root.Attribute("orientation");
Now we have an XAttribute
called ortho
, which corresponds to
orientation="orthogonal"
XAttributes
have two useful properties. Name
and Value
. To get at the name and the value of our orientation attribute we can just do
XName orthoName = ortho.Name;
string orthoValue = ortho.Value;
Pay attention to the fact that ortho.Value
returns a string and not an XName
.
Getting the rest of the attributes and their values
Now that we know how to handle one attribute lets handle them all.
Dictionary<string, string> rootData = new();
foreach (XAttribute attrib in root.Attributes())
{
rootData.Add(attrib.Name.ToString(), attrib.Value);
}
root.Attributes()
returns and IEnumerable<XAttribute>
which just means that it is a list of XAttributes
that we can iterate over with forEach
. Lets store it in a dictionary as the XAttribute properties of Name
and Value
are a great fit for the key and value in the dictionary. Take note that we called ToString() on the Name
property when storing it as the key to the dictionary. attrib.Name
returns an XName not a string as we learned in How do I get the attributes of the Root Node? section. Since our dictionary expects a <string, string> not a <XName, string> we have to convert it. Here is how the code looks so far
using System.Xml.Linq;
XDocument doc = XDocument.Load(@"..\..\..\map01.tmx");
XDeclaration dec = doc.Declaration;
string version = dec.Version;
XElement root = (XElement)doc.FirstNode;
XName rootName = root.Name;
XAttribute ortho = root.Attribute("orientation");
XName orthoName = ortho.Name;
string orthoValue = ortho.Value;
Dictionary<string, string> rootData = new();
foreach (XAttribute attrib in root.Attributes())
{
rootData.Add(attrib.Name.ToString(), attrib.Value);
}
How do I get the rest of the Elements?
We still need to get the <tileset>
, <layer>
, and <data>
tags shown below
<tileset firstgid="1" source="tileset.tsx" />
<layer id="1" name="Tile Layer 1" width="40" height="15">
<data encoding="csv">0,0,0
0,0,0
1,0,1
</data>
</layer>
Thankfully since we have our root node those values are just descendants of the root.
IEnumerable<XElement> rootDescendants = root.Descendants();
root.Descendants() gets an IEnumerable of XElements
. This is similar to how root.Attributes() returned an IEnumerable<XAttribute>. Since it’s an IEnumerable we know we can iterate through it with foreach. Since we know the IEnumerable contains XElements we know that each XElement will have a name like we discussed in How do we get the Root node?. Since the descendants are XElements that also means we can get the XAttributes of the XElements like we did in Getting the rest of the attributes and their values. Let’s put it all together.
foreach(XElement ele in rootDescendants)
{
Console.WriteLine($"My Name is: {ele.Name.ToString()}");
if (ele.Name.ToString() == "data")
{
Console.WriteLine($"My element Values are {ele.Value}");
}
Console.WriteLine($"These are my attributes...");
foreach (XAttribute attrib in ele.Attributes())
{
Console.WriteLine($"attribute name: {attrib.Name.ToString()}, attribute value:{attrib.Value}");
}
Console.WriteLine("");
}
Note: Prefixing a string with $ allows you to embed blocks of executable C# code in the string using {}. This is called string interpolation.
There is one small wrinkle which you might have noticed in the code, which slightly changed the output for the data XElement
.
if (ele.Name.ToString() == "data")
{
Console.WriteLine($"My element Values are {ele.Value}");
}
XElements
can have values. These are just the values that are stored between the <tag> and </tag> (opening tag and closing tag). In this case, data
has the value “0,0,0,0,0,0,1,0,1” shown below.
<data encoding="csv">0,0,0
0,0,0
1,0,1
</data>
To get at this data in particular XElements
have a Value
property, so we add an if
statement as a special case to handle our data XElement
.
And that is it! You should now be able to get the most commonly needed data inside of an XDocument/XElement/XAttribute. There’s plenty more to learn about parsing XML but that’s it for now! And if you are interested in learning Monogame I can recommend Challacade’s YouTube Channel and Udemy Course6. I found them very easy to follow.
Lastly I would encourage anyone who has finds the Tiled editor useful to donate to the project7! Thorbjørn Lindeijer is getting close to being able to work on it full time, which is the dream of many Devs. The source code for this project can be found at the very bottom of the page. Please use if for whatever you wish, and consider it under the Apache 2.0 license8. Lastly I've noticed that my articles have been shared across some of the forums I frequent. Thank you for the vote of confidence! It means the world to me.
Call to Action📣
If you made it this far thanks for reading! I like to writing about programming topics, and technologies that interest me. I hope they interest you too! If you liked this article please consider liking and subscribing. And if you haven’t why not check out another article of mine!
Source code
using System.Xml.Linq;
XDocument doc = XDocument.Load(@"..\..\..\map01.tmx");
XDeclaration dec = doc.Declaration;
string version = dec.Version;
XElement root = (XElement)doc.FirstNode;
XName rootName = root.Name;
XAttribute ortho = root.Attribute("orientation");
XName orthoName = ortho.Name;
string orthoValue = ortho.Value;
IEnumerable<XElement> rootDescendants = root.Descendants();
Dictionary<string, string> rootData = new();
foreach (XAttribute attrib in root.Attributes())
{
rootData.Add(attrib.Name.ToString(), attrib.Value);
}
foreach(XElement ele in rootDescendants)
{
Console.WriteLine($"My Name is: {ele.Name.ToString()}");
if (ele.Name.ToString() == "data")
{
Console.WriteLine($"My element Values are {ele.Value}");
}
Console.WriteLine($"These are my attributes...");
foreach (XAttribute attrib in ele.Attributes())
{
Console.WriteLine($"attribute name: {attrib.Name.ToString()}, attribute value:{attrib.Value}");
}
Console.WriteLine("");
}