Sometimes when I want to get something done quickly, I can settle for the most apparent solution even though it's not the most elegant or possibly even the best solution.

A lot of the time it's usually because I'm not that interested enough in the problem at hand because the more exciting problem is where I'm really heading, and in order to get there I need to implement this less uninteresting solution first, and so I hurry through it in order to get it done. This is the case with my level editor's XML functionality.

I serialize the level to XML once I'm done designing it in the level editor, and I can also load saved levels back in and modify it:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Xml;
using GameEditor.Models;
using Microsoft.Win32;

namespace GameEditor.ViewModels
{
    public class LevelManager 
    {
        public event EventHandler<string> OnFileSaved;
        public event EventHandler<string> OnAboutToSaveFile;
        public event EventHandler OnLevelLoaded;

        public void SaveLevelFile(Level level)
        {
            // Collect all the rooms and serialize to XML
            var xmlSettings = new XmlWriterSettings()
            {
                Indent = true,
            };

            var saveFileDialog = new SaveFileDialog
            {
                Filter = "XML Files (*.xml)|*.xml;"
            };

            try
            {
                if (!(saveFileDialog.ShowDialog() is true)) return;

                OnAboutToSaveFile?.Invoke(this, \("Saving File '{saveFileDialog.FileName}'...");

                using (var writer = XmlWriter.Create(saveFileDialog.FileName, xmlSettings))
                {
                    writer.WriteStartDocument();
                    writer.WriteStartElement("level"); // <level ...
                    writer.WriteAttributeString("cols", level.NumCols.ToString());
                    writer.WriteAttributeString("rows", level.NumRows.ToString());
                    writer.WriteAttributeString("autoPopulatePickups", level.AutoPopulatePickups.ToString());
                    foreach(var roomViewModel in level.Rooms)
                    {
                        var topVisible = roomViewModel.TopWallVisibility == Visibility.Visible;
                        var rightVisible = roomViewModel.RightWallVisibility == Visibility.Visible;
                        var bottomVisible = roomViewModel.BottomWallVisibility == Visibility.Visible;
                        var leftVisible = roomViewModel.LeftWallVisibility == Visibility.Visible;

                        writer.WriteStartElement("room"); // <room ...
                        writer.WriteAttributeString("number", roomViewModel.RoomNumber.ToString());
                        writer.WriteAttributeString("top", topVisible.ToString() );
                        writer.WriteAttributeString("right", rightVisible.ToString() );
                        writer.WriteAttributeString("bottom", bottomVisible.ToString() );
                        writer.WriteAttributeString("left", leftVisible.ToString() );

                        if(roomViewModel.ResidentGameObjectType != null)
                        {
                            var gameObjectType = roomViewModel.ResidentGameObjectType;
                            writer.WriteStartElement("object"); //<object ...
                            writer.WriteAttributeString("name", gameObjectType.Name);
                            writer.WriteAttributeString("type", gameObjectType.Type);
                            writer.WriteAttributeString("resourceId", gameObjectType.ResourceId.ToString());
                            writer.WriteAttributeString("assetPath", gameObjectType.AssetPath);
                            foreach(var property in gameObjectType.Properties)
                            {
                                writer.WriteStartElement("property"); //<property ..
                                writer.WriteAttributeString("name", property.Key);
                                writer.WriteAttributeString("value", property.Value);
                                writer.WriteEndElement();
                            }
                            writer.WriteEndElement();
                        }

                        writer.WriteEndElement();
                    }
                    writer.WriteEndElement();
                    writer.WriteEndDocument();
                }

                OnFileSaved?.Invoke(this,  \)"Saved File '{saveFileDialog.FileName}'.");
            }
            catch(Exception ex)
            {
                throw new Exception (\("Error saving level file '{saveFileDialog.FileName}': {ex.Message}");
            }   
        }

        public Level LoadLevelFile()
        {
            var openFileDialog = new OpenFileDialog();
            var level = new Level();

            if(openFileDialog.ShowDialog() is true)
            {
                var settings = new XmlReaderSettings
                {
                    DtdProcessing = DtdProcessing.Ignore
                };
                var reader = XmlReader.Create(openFileDialog.FileName, settings);
                RoomViewModel roomViewModel = null;
                
                while (reader.Read())
                {                    
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        if(reader.Name.Equals("level"))
                        {
                            level.NumCols = int.Parse(reader.GetAttribute("cols") ?? throw new Exception(
                                "NumCols Not found"));
                            level.NumRows = int.Parse(reader.GetAttribute("rows") ?? throw new Exception(
                                "NumRows Not found"));
                            level.AutoPopulatePickups = bool.Parse(reader.GetAttribute("autoPopulatePickups") ?? bool.FalseString);
                        }

                        roomViewModel = new RoomViewModel();

                        if(reader.Name.Equals("room"))
                        {
                            roomViewModel.RoomNumber = int.Parse(reader.GetAttribute("number") ?? "0");
                            roomViewModel.TopWallVisibility = bool.Parse(reader.GetAttribute("top") ??
                                                                         throw new Exception(
                                                                             "Top wall visibility Not found"))
                                ? Visibility.Visible
                                : Visibility.Hidden;
                            roomViewModel.RightWallVisibility = bool.Parse(reader.GetAttribute("right") ??
                                                                           throw new Exception(
                                                                               "Right wall visibility Not found"))
                                    ? Visibility.Visible
                                    : Visibility.Hidden;
                            roomViewModel.BottomWallVisibility = bool.Parse(reader.GetAttribute("bottom") ??
                                                                            throw new Exception(
                                                                                "Bottom wall visibility Not found"))
                                    ? Visibility.Visible
                                    : Visibility.Hidden;
                            roomViewModel.LeftWallVisibility =
                                bool.Parse(reader.GetAttribute("left") ??
                                           throw new Exception("Left wall visibility Not found"))
                                    ? Visibility.Visible
                                    : Visibility.Hidden;
                        }
                        if (reader.Name.Equals("object"))
                        {
                            roomViewModel.ResidentGameObjectType = new GameObjectType
                            {
                                AssetPath = reader.GetAttribute("assetPath"),
                                Name = reader.GetAttribute("name"),
                                ResourceId = int.Parse(reader.GetAttribute("resourceId") ?? throw new Exception("Resource Id Not found")),
                                Type = reader.GetAttribute("type"),
                                Properties = new List<KeyValuePair<string, string>>()
                            };
                        }

                        if (reader.Name.Equals("property"))
                        {
                            for (var i = 0; i < reader.AttributeCount; i++)
                            {
                                reader.MoveToAttribute(i);
                                roomViewModel.ResidentGameObjectType.Properties.Add(new KeyValuePair<string, string>(reader.Name, reader.Value));
                            }
                        }
                    }

                    if ((reader.NodeType == XmlNodeType.EndElement || reader.IsEmptyElement) && reader.Name.Equals("room"))
                    {
                        level.Rooms.Add(roomViewModel);
                    }
                }
            }
            OnLevelLoaded?.Invoke(this, EventArgs.Empty);
            return level;            
        }
    }
}

This is the kind of approach I'm talking about. I wanted to create the XML data so that I could switch over to the game and actually load the XML and generate the level and game objects from it. I was not too concerned with how I did it, but only that I did it. 

It was also error-prone.

So this weekend, I needed to change something in this code and it was too difficult to do without breaking it, i.e it was not easy to maintain. So I decided that it was too painful to keep and looked for an improvement. I came across a much more concise and functional approach using LINQ to XML. I'm very impressed with it. 

Notice how easily XML can be produced directly from a C# class, basically transforming the class directly into an XML version. It's really cool. Also, reading the XML and transforming it back into the C# class is as easy:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Xml;
using System.Xml.Linq;
using GameEditor.Models;
using GameEditor.Utils;
using Microsoft.Win32;

namespace GameEditor.ViewModels
{
    public class LevelManager 
    {
        public event EventHandler<string> OnFileSaved;
        public event EventHandler<string> OnAboutToSaveFile;
        public event EventHandler OnLevelLoaded;

        public void SaveLevelFile(Level level, List<GameObjectType> gameObjectTypes)
        {
            // Collect all the rooms and serialize to XML
            var xmlSettings = new XmlWriterSettings()
            {
                Indent = true,
            };

            var saveFileDialog = new SaveFileDialog
            {
                Filter = "XML Files (*.xml)|*.xml;"
            };

            try
            {
                if (!(saveFileDialog.ShowDialog() is true)) return;

                OnAboutToSaveFile?.Invoke(this, \)"Saving File '{saveFileDialog.FileName}'...");
                
                var levelNode = new XElement("level",
                    new XAttribute("cols", level.NumCols),
                    new XAttribute("rows", level.NumRows),
                    new XAttribute("autoPopulatePickups", level.AutoPopulatePickups.ToString()),
                    from room in level.Rooms
                        
                    let gameObject = room.ResidentGameObjectType
                    let roomEl = new XElement("room",
                        new XAttribute("number", room.RoomNumber),
                        new XAttribute("top", room.TopWallVisibility.ToBoolString()),
                        new XAttribute("right", room.RightWallVisibility.ToBoolString()),
                        new XAttribute("bottom", room.BottomWallVisibility.ToBoolString()),
                        new XAttribute("left", room.LeftWallVisibility.ToBoolString()),
                        // <object>
                        gameObject != null 
                            ? new XElement("object",
                            new XAttribute("name", gameObject.Name),
                            new XAttribute("type", gameObject.Type),
                            new XAttribute("resourceId", gameObject.ResourceId),
                            new XAttribute("assetPath", gameObject.AssetPath), 
                            // <property>
                            from property in gameObjectTypes.Single(x=>x.Type == gameObject.Type).Properties // save any new props
                            let key = new XAttribute("name", property.Key)
                            let value = new XAttribute("value", property.Value)
                            select new XElement("property", key, value)) 
                            : null) 
                    select roomEl);

                using (var writer = XmlWriter.Create(saveFileDialog.FileName, xmlSettings))
                {
                    levelNode.WriteTo(writer);
                }

                OnFileSaved?.Invoke(this,  \("Saved File '{saveFileDialog.FileName}'.");
            }
            catch(Exception ex)
            {
                throw new Exception (\)"Error saving level file '{saveFileDialog.FileName}': {ex.Message}");
            }   
        }

        public Level LoadLevelFile()
        {
            var openFileDialog = new OpenFileDialog();

            Level level = null;
            if(openFileDialog.ShowDialog() is true)
            {
                level = (from levels in XElement.Load(openFileDialog.FileName).AncestorsAndSelf()
                        select new Level
                        {
                            NumCols = GetAsNumber(levels, "cols"),
                            NumRows = GetAsNumber(levels, "rows"),
                            AutoPopulatePickups = GetAsBool(levels, "autoPopulatePickups"),
                            Rooms = levels.Descendants("room").Select(r => new RoomViewModel
                            {
                                RoomNumber = GetAsNumber(r, "number"),
                                TopWallVisibility = GetAsVisibilityFromTruthString(r,"top"),
                                LeftWallVisibility = GetAsVisibilityFromTruthString(r,"left"),
                                RightWallVisibility = GetAsVisibilityFromTruthString(r,"right"),
                                BottomWallVisibility = GetAsVisibilityFromTruthString(r, "bottom"),
                                ResidentGameObjectType = r.Descendants("object").Select(o => new GameObjectType()
                                {
                                    Name = GetAsString(o, "name"),
                                    Type = GetAsString(o, "type"),
                                    ResourceId = GetAsNumber(o, "resourceId"),
                                    AssetPath = GetAsString(o, "assetPath"),
                                    Properties = o.Descendants("property")
                                        .Select(p => new KeyValuePair<string, string>(key: GetAsString(p, "name"), 
                                            value: GetAsString(p, "value"))).ToList()
                                }).SingleOrDefault()
                            }).ToList(),
                        }).SingleOrDefault();

            }
            OnLevelLoaded?.Invoke(this, EventArgs.Empty);
            return level;            
        }

        private static bool GetAsBool(XElement o, string attributeName)
            => GetAsString(o, attributeName).ToBool();

        private static string GetAsString(XElement o, string attributeName) 
            => (o.Attribute(attributeName) ?? throw new NullReferenceException(attributeName)).Value;

        private static Visibility GetAsVisibility(XElement r, string attributeName) 
            => GetAsString(r, attributeName).ToVisibility();

        private static Visibility GetAsVisibilityFromTruthString(XElement r, string attributeName) 
            => GetAsString(r, attributeName).VisibilityFromTruthString();

        private static int GetAsNumber(XElement r, string attributeName) 
            => GetAsString(r, attributeName).ToNumber();
    }
}

This is very easy to read and it hasn't got any loops in it, ie. it's functional instead of imperative, so no need to remember which loop you're in or wonder if you're forgetting about an end tag or something silly like that.

It did require some getting used to I admit as like most functional programming, it's ultimately one big expression that does it all. 

Both code versions produce and read the same XML however, I much prefer the second version due to its conciseness and ease of use. Also, its really easy to add new nodes or attributes to the XML tree, and then being able to easily transform the XML directly into the C# class right there is also pretty great. 

Another win for functional programming!