Press "Enter" to skip to content

Lesson 14.2: Creating extension methods

Since we’ll need to use the XML parsing code for all the factories, I’m going to move the parsing functions to a common class.

I’m also going to change them to “extension methods”, which I think look a little nicer.

Step 1: Create the folder Engine\Shared

We are not required to put extension methods in a new folder. I just want to put the shared “utility” functions and classes in one place.

Step 2: Create the class Engine\Shared\ExtensionMethods.cs

When you create your own programs, you can name this class whatever you want – it is not required to be Extension Methods. This is just my preference.

Objects can have methods – like our Player objects have AddExperience and LearnRecipe. We call them by writing “player.AddExperience(5);”, and the function acts on the object stored in the “player” variable.

The standard .NET objects also have functions, like myString.ToUpper(), which returns an upper-case version of mystring.

Extension methods let you “extend” the available functions on objects. When you create them, you define what datatype (or interface) it works on, and the function will be visible in IntelliSense.

An extension method looks like a normal function, except it must follow these three additional rules:

  • The function must be in a static class.
  • The function must be static.
  • The first parameter must also have “this” – to signify the object/interface you can call the extension method from.

In the code below, the extension method AttributeAsString (lines 13-23) can be called on any XmlNode object (the first parameter, with “this”). The object it’s called from will be passed in as the “node” parameter, which is used on line 15.

This lets us get the attribute value in this format:

string nameValue = myXmlNode.AttributeAsString("Name");

For other datatypes, we’ll have an extension method that calls AttributeAsString (because all attributes are natively strings) and converts the result to the desired datatype. This is what AttributeAsInt (lines 8-11) does, to get our integer values from the XML nodes.

Extension methods don’t really add anything new – they’re still just a function. But, it can make your code look cleaner and easier to understand.

ExtensionMethods.cs
using System;
using System.Xml;
namespace Engine.Shared
{
    public static class ExtensionMethods
    {
        public static int AttributeAsInt(this XmlNode node, string attributeName)
        {
            return Convert.ToInt32(node.AttributeAsString(attributeName));
        }
        public static string AttributeAsString(this XmlNode node, string attributeName)
        {
            XmlAttribute attribute = node.Attributes?[attributeName];
            if(attribute == null)
            {
                throw new ArgumentException($"The attribute '{attributeName}' does not exist");
            }
            return attribute.Value;
        }
    }
}

Step 3: Modify Engine\Factories\ItemFactory.cs

Now, we can use the extension methods in the factory class.

On line 7, add “using Engine.Shared;”, so we have access to the extension methods.

Change lines 57-59, 66-67, and 73 to use the new extension methods.

Delete lines 93-113, where we had the original functions to get attribute values from the XmlNode.

ItemFactory.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Engine.Actions;
using Engine.Models;
using Engine.Shared;
namespace Engine.Factories
{
    public static class ItemFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\GameItems.xml";
        private static readonly List<GameItem> _standardGameItems = new List<GameItem>();
        static ItemFactory()
        {
            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));
                LoadItemsFromNodes(data.SelectNodes("/GameItems/Weapons/Weapon"));
                LoadItemsFromNodes(data.SelectNodes("/GameItems/HealingItems/HealingItem"));
                LoadItemsFromNodes(data.SelectNodes("/GameItems/MiscellaneousItems/MiscellaneousItem"));
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }
        }
        public static GameItem CreateGameItem(int itemTypeID)
        {
            return _standardGameItems.FirstOrDefault(item => item.ItemTypeID == itemTypeID)?.Clone();
        }
        public static string ItemName(int itemTypeID)
        {
            return _standardGameItems.FirstOrDefault(i => i.ItemTypeID == itemTypeID)?.Name ?? "";
        }
        private static void LoadItemsFromNodes(XmlNodeList nodes)
        {
            if(nodes == null)
            {
                return;
            }
            foreach(XmlNode node in nodes)
            {
                GameItem.ItemCategory itemCategory = DetermineItemCategory(node.Name);
                
                GameItem gameItem =
                    new GameItem(itemCategory,
                                 node.AttributeAsInt("ID"),
                                 node.AttributeAsString("Name"),
                                 node.AttributeAsInt("Price"),
                                 itemCategory == GameItem.ItemCategory.Weapon);
                if(itemCategory == GameItem.ItemCategory.Weapon)
                {
                    gameItem.Action =
                        new AttackWithWeapon(gameItem,
                                             node.AttributeAsInt("MinimumDamage"),
                                             node.AttributeAsInt("MaximumDamage"));
                }
                else if(itemCategory == GameItem.ItemCategory.Consumable)
                {
                    gameItem.Action =
                        new Heal(gameItem,
                                 node.AttributeAsInt("HitPointsToHeal"));
                }
                _standardGameItems.Add(gameItem);
            }
        }
        private static GameItem.ItemCategory DetermineItemCategory(string itemType)
        {
            switch(itemType)
            {
                case "Weapon":
                    return GameItem.ItemCategory.Weapon;
                case "HealingItem":
                    return GameItem.ItemCategory.Consumable;
                default:
                    return GameItem.ItemCategory.Miscellaneous;
            }
        }
    }
}

Step 4: Test the game

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

PREVIOUS LESSON: Lesson 14.1: Moving game data to external files

    Leave a Reply

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