Press "Enter" to skip to content

Lesson 14.3: Read World (Location) data from an XML file

I originally wanted to convert all the factories to use XML files in this lesson. However, there is a lot of extra work to handle the location image files. So, this lesson will cover the locations.

The next lesson will cover the monsters. There are some extra changes we’ll need to make for the loot items. A third lesson will handle all the other factory classes – their changes are simple.

NOTE: Some people have seen this error message: FileNotFoundException: Could not find file ‘C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\ImageName’. If you see that, go to the top of Window.xaml, and change d:DataContext=”{d:DesignInstance viewModels:GameSession}” to d:DataContext=”{Binding GameSession}”, that should fix it. This will instantiate a GameSession object (which has values in the properties) and bind to that. The “DesignInstance” just know what properties exist – it does have an object available.

Step 1: Create Engine\GameData\Locations.xml

This is the XML file for the Location object data.

Notice there is an attribute “RootImagePath” at the top-level “Locations” node. Because all the images will be in this one directory, I put this attribute here. We’ll read it once and add it to all the image file names in the “Location” nodes.

Because the Description value will be so long, I made it a child node, instead of making it an attribute. This is only a personal preference, so the “Location” node would not be an extremely long line.

Because “Description” is a child node, I put the text in between its opening and closing tags, instead of putting the description in another attribute. The text is surrounded with “<![CDATA[” and “]]>”. This is in case the description text contains anything that might look like valid XML. We want the text to be treated as text, and not as XML.

Each “Location” node has the “Description” child node. It can have a “Trader” node, and “Monsters” or “Quests” children.

NOTE: Remember to right-click on Locations.xml and set its “Copy to Output Directory” property to “Copy Always”.

Locations.xml

<?xml version="1.0" encoding="utf-8" ?>
<Locations RootImagePath="/Images/Locations/">
  <Location X="-2" Y="-1" Name="Farmer's Field" ImageName="FarmFields.png">
    <Description><![CDATA[There are rows of corn here, with giant rats hiding between them.]]></Description>
    <Monsters>
      <Monster ID="2" Percent="100"/>
    </Monsters>
  </Location>
  <Location X="-1" Y="-1" Name="Farmer's House" ImageName="Farmhouse.png">
    <Description><![CDATA[This is the house of your neighbor, Farmer Ted.]]></Description>
    <Trader Name="Farmer Ted"/>
  </Location>
  <Location X="0" Y="-1" Name="Home" ImageName="Home.png">
    <Description><![CDATA[This is your home.]]></Description>
  </Location>
  <Location X="-1" Y="0" Name="Trading Shop" ImageName="Trader.png">
    <Description><![CDATA[The shop of Susan, the trader.]]></Description>
    <Trader Name="Susan"/>
  </Location>
  <Location X="0" Y="0" Name="Town Square" ImageName="TownSquare.png">
    <Description><![CDATA[You see a fountain here.]]></Description>
  </Location>
  <Location X="1" Y="0" Name="Town Gate" ImageName="TownGate.png">
    <Description><![CDATA[There is a gate here, protecting the town from giant spiders.]]></Description>
  </Location>
  <Location X="2" Y="0" Name="Spider Forest" ImageName="SpiderForest.png">
    <Description><![CDATA[The trees in this forest are covered with spider webs.]]></Description>
    <Monsters>
      <Monster ID="3" Percent="100"/>
    </Monsters>
  </Location>
  <Location X="0" Y="1" Name="Herbalist's Hut" ImageName="HerbalistsHut.png">
    <Description><![CDATA[You see a small hut, with plants drying from the roof.]]></Description>
    <Quests>
      <Quest ID="1"/>
    </Quests>
    <Trader Name="Pete the Herbalist"/>
  </Location>
  <Location X="0" Y="2" Name="Herbalist's Garden" ImageName="HerbalistsGarden.png">
    <Description><![CDATA[There are many plants here, with snakes hiding behind them.]]></Description>
    <Monsters>
      <Monster ID="1" Percent="100"/>
    </Monsters>
  </Location>
</Locations>

Step 2: Modify Engine\Models\World.cs

The original AddLocation function took several parameters, constructed a Location object, and added the Location object to _locations. Now, we’re going to completely construct the Location object in WorldFactory. So, I changed AddLocation to accept the completely-built Location object.

World.cs
using System.Collections.Generic;
namespace Engine.Models
{
    public class World
    {
        private readonly List<Location> _locations = new List<Location>();
        internal void AddLocation(Location location)
        {
            _locations.Add(location);
        }
        public Location LocationAt(int xCoordinate, int yCoordinate)
        {
            foreach(Location loc in _locations)
            {
                if(loc.XCoordinate == xCoordinate && loc.YCoordinate == yCoordinate)
                {
                    return loc;
                }
            }
            return null;
        }
    }
}

Step 3: Modify Engine\Factories\WorldFactory.cs

The changes here are like what we did to the ItemFactory class.

We read the XML file, get the RootImagePath value, and parse the Location nodes.

One thing to notice is the ImageName value in the call to the Location constructor (line 51). Before, the image files were included as resources in the compiled program. The value looked like this:

$”/Engine;component/Images/Locations/{imageName}”

Now, it uses a directory path, relative to where the game is running (because of the “.” at the beginning of the file name string. In the next step, we’ll change the project to read the images from external files, instead of program resources.

This will let you send out a new Locations.xml file, or new images, to expand the game world – without needing to rebuild the program and have the player completely re-install the game.

WorldFactory.cs
using System.IO;
using System.Xml;
using Engine.Models;
using Engine.Shared;
namespace Engine.Factories
{
    internal static class WorldFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\Locations.xml";
        internal static World CreateWorld()
        {
            World world = new World();
            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));
                string rootImagePath =
                    data.SelectSingleNode("/Locations")
                        .AttributeAsString("RootImagePath");
                LoadLocationsFromNodes(world, 
                                       rootImagePath, 
                                       data.SelectNodes("/Locations/Location"));
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }
            return world;
        }
        private static void LoadLocationsFromNodes(World world, string rootImagePath, XmlNodeList nodes)
        {
            if(nodes == null)
            {
                return;
            }
            foreach(XmlNode node in nodes)
            {
                Location location =
                    new Location(node.AttributeAsInt("X"),
                                 node.AttributeAsInt("Y"),
                                 node.AttributeAsString("Name"),
                                 node.SelectSingleNode("./Description")?.InnerText ?? "",
                                 $".{rootImagePath}{node.AttributeAsString("ImageName")}");
                AddMonsters(location, node.SelectNodes("./Monsters/Monster"));
                AddQuests(location, node.SelectNodes("./Quests/Quest"));
                AddTrader(location, node.SelectSingleNode("./Trader"));
                world.AddLocation(location);
            }
        }
        private static void AddMonsters(Location location, XmlNodeList monsters)
        {
            if(monsters == null)
            {
                return;
            }
            foreach(XmlNode monsterNode in monsters)
            {
                location.AddMonster(monsterNode.AttributeAsInt("ID"),
                                    monsterNode.AttributeAsInt("Percent"));
            }
        }
        private static void AddQuests(Location location, XmlNodeList quests)
        {
            if(quests == null)
            {
                return;
            }
            foreach(XmlNode questNode in quests)
            {
                location.QuestsAvailableHere
                        .Add(QuestFactory.GetQuestByID(questNode.AttributeAsInt("ID")));
            }
        }
        private static void AddTrader(Location location, XmlNode traderHere)
        {
            if(traderHere == null)
            {
                return;
            }
            location.TraderHere =
                TraderFactory.GetTraderByName(traderHere.AttributeAsString("Name"));
        }
    }
}

Step 4: Modify the .png files in Engine\Images\Locations

Right-click on these files, change “Build Action” to “None” and “Copy to Output Directory” to “Copy Always” – just like we do with the XML data files for the factories.

NOTE: You can shift-click to select all the image files and change all their “Build Action” and “Copy to Output Directory” values at the same time.

Step 5: Create a new folder WPFUI\CustomConverters and a new class WPFUI\CustomConverters\FileToBitmapConverter.cs

When the location images were project resources, it was simple to display them in an UI Image control. Now that they are external files, we need to do an extra step to get them into the Image control.

With WPF, it is common to create custom converter classes when you want to use data differently from how it is stored. A common converter is converting a Boolean property to a Visibility value (Visible, Hidden, or Collapsed). This way, you can hide or show different parts of the UI, based on a property value.

Another common one is to convert a bound Boolean property to display a text value of “Yes” or “No”.

This converter class will let us bind the Image control to a file path (the location image file), read the file from disk, and convert it to a BitmapImage object – which can be displayed in a WPF Image control.

Custom converter class must implement the “IValueConverter” interface (line 9), which requires the class to have Convert and ConvertBack functions.

We are only going to convert from a file path and name to a BitmapImage, so we can have the ConvertBack function (lines 31-34) just return null.

In the Convert function, we try to cast the “value” parameter to a string and store it in the “filename” variable (line 16). The “value” parameter is the value of the bound property in the UI’s Image control, for this program.

If the value is a string, we’ll see if we have a DIctionary entry whose key is the filename.

Notice the “_locations” dictionary (lines 11-12) is static. That means it will be shared by everything that calls this FileToBitmapConverter class. This is a way to cache the images, so we don’t need to re-read the file from disk each time the player returns to a location.

If the location (image file path and name) is not already in the Dictionary, we create a new BitmapImage object from the file (lines 24-25) and add the location file name and BitmapImage to “_locations”, so it will be available the next time the player moves to this location.

Finally, on line 28, we get the BitmapImage object from the “_locations” cache, to populate the Image control’s Source property.

FileToBitmapConverter.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace WPFUI.CustomConverters
{
    public class FileToBitmapConverter : IValueConverter
    {
        private static readonly Dictionary<string, BitmapImage> _locations =
            new Dictionary<string, BitmapImage>();
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(!(value is string filename))
            {
                return null;
            }
            if(!_locations.ContainsKey(filename))
            {
                _locations.Add(filename,
                               new BitmapImage(new Uri($"{AppDomain.CurrentDomain.BaseDirectory}{filename}",
                                                       UriKind.Absolute)));
            }
            return _locations[filename];
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

Step 6: Modify WPFUI\App.xaml

To use the FileToBitmapConverter in a WPF window, we’ll add it as an Application Resource (lines 7-9). Now, we can use this converter in any WPF control in this project.

On line 4, we create “converters” as the XML namespace for our customer converters. Then, on line 8, we say, “In the ‘converters’ namespace, we have the FileToBitmapConverter. Create a key (name) we can use when we want to use this converter in a WPF window/control.”

App.xaml
<Application x:Class="WPFUI.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:converters="clr-namespace:WPFUI.CustomConverters"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         <converters:FileToBitmapConverter x:Key="FileToBitmapConverter"/>
    </Application.Resources>
</Application>

Step 7: Modify WPFUI\MainWindow.xaml

Finally, add the converter to the Image control (lines 115-116). Now, when the program tries to bind the CurrentLocation.ImageName value to the Image control, it will first convert it with FileToBitmapConverter.Convert().

This is how we go from a file on disk to a graphic displayed in the UI.

MainWindow.xaml (lines 110-116)

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>

Step 8: Test the game

NEXT LESSON: Lesson 14.4: Read Monster data from an XML file

PREVIOUS LESSON: Lesson 14.2: Creating extension methods

17 Comments

  1. Steven
    Steven 2021-11-14

    I get this error ‘C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\ImageName’. I make the change to this d:DataContext=”{Binding GameSession}” when I do this becomes greyed out xmlns:viewModels=”clr-namespace:Engine.ViewModels;assembly=Engine” I can run game still but when I go to a location that has a monster the game freezes. sorry if I double posted for some reason it seems my comments are disappearing

    • SOSCSRPG
      SOSCSRPG 2021-11-14

      Hi Steven,

      That error is only in Visual Studio because Visual Studio is trying to bind to the WPF Image control to the file in the ImageName property. However, since that value is null during development, it shows the error. When you run the game, the ImageName property has a value, so the game can display an image. You can ignore that error during development.

      In case you ever post another comment, they are hidden until I approve them. I get 10-20 spam comments per day, so have to hide unapproved comments. I wish I didn’t have to do that, but that’s how some people are. 🙁

  2. Peter
    Peter 2022-01-29

    Hi.
    im trying to modify App.xaml, and every time i try it. it will wrote error that filetobitmapconverter doesnt exist in wpfui…

    • SOSCSRPG
      SOSCSRPG 2022-01-29

      Hi Peter,

      Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub, Dropbox, or some other file-sharing location so I can look at it?

      • Peter
        Peter 2022-01-29

        i zipped it here.
        LINK REMOVED FOR PRIVACY

        • SOSCSRPG
          SOSCSRPG 2022-01-29

          Hello Peter,

          I downloaded the files and will let you know what I find. It may take a little while, because I have to add the language pack for your alphabet.

        • SOSCSRPG
          SOSCSRPG 2022-01-29

          Hello Peter,

          Can you double-check step 5 from this lesson?

          I do not see the CustomConverters folder in the WPF project, or the FileToBitmapConverter.cs class that goes in that folder.

          Please let me know if that does help you fix the error.

          • Peter
            Peter 2022-01-29

            I uploaded bad version of game here is right version with error : LINK REMOVED FOR PRIVACY
            i tried to repair it but every time it says same error (filetobitmapconverter doesnt exist in namespace)…

          • SOSCSRPG
            SOSCSRPG 2022-01-29

            Hello Peter,

            I think I found the solution.

            In FileToBitmapConverter.cs
            Line 9, change to “public class FileToBitmapConverter : IValueConverter”

            This class needs to show that it implements the IValueConverter interface.

            Also, there are a few changes I think need to be done for loading data from the XML file, to get the XPath, node, and attribute names to match.

            In Stavba Sveta.cs
            Line 23, change: “/Lokace” to “/MnožstvíLokací”
            Line 28, change: “/MnoštvíLokací” to “/MnožstvíLokací”

            In Lokace.xml
            Line 61, change: “Procentra” to “Procenta”
            Line 92, change: “Procentra” to “Procenta”
            Line 109, change: “Procentra” to “Procenta”

            Please tell me if that does not fix the error.

  3. Peter
    Peter 2022-01-29

    Thanks it helped. it works fine.
    i love your work.

  4. Lance
    Lance 2023-04-16

    Only one of the image files shows properties. I can’t get any others to show. This was an issue back when the images were first added, but I saw that this lesson had a different way to display them so I just moved on, but now it’s still not working.

    Thanks for any help. Google is useless for this issue.

    • SOSCSRPG
      SOSCSRPG 2023-04-19

      Hi Lance,

      The newer version of Visual Studio (and the newer versions of .NET) might have things set up a different way. Can you zip the top directory (the one with the solution, and the project directories under it) and upload the zip file some place I can investigate (Dropbox, Google Drive, a direct message to me in the Discord channel https://discord.com/invite/qTma89NFFS)?

  5. Wicked Cat
    Wicked Cat 2023-05-14

    Did i miss a step somewhere? I checked the whole video, ended up copy/pasting the code changes so I could be sure I didn’t mess it up and I’m getting 3 errors.

    ‘Location’ does not contain a constructor that takes 5 arguments in WorldFactory.cs

    The name “FileToBitmapConverter” does not exist in the namespace “clr-namespaace:WPFUI.CustomConverter” in App.xaml (I couldn’t figure out the answer you gave to Peter, sorry)

    and The resource “FileToBitmapConverter” has an incompatible type in MainWindow.xaml

    sorry for the long comment but (i think) i copied everything you did so i’m not sure why it’s broken. Thanks for all the help even years later 🙂

    • SOSCSRPG
      SOSCSRPG 2023-05-15

      It sounds like there are two problems.

      For the Location constructor issue, check that the WorldFactory.cs file has the 4 “using” lines at the top. If those are there, I’m not sure why that error would appear.

      For the FileToBitmapConverter issue, it sounds like you have the file, but the program isn’t recognizing it for some reason.

      I probably need to look at the solution to track these down. Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub, Dropbox, or some other file-sharing location so I can look at it?

      • Wicked Cat
        Wicked Cat 2023-05-16

        Alright, I believe I sent you (SOSCSRPG) a request to be a collaborator on github so you should be able to see it.

Leave a Reply

Your email address will not be published. Required fields are marked *