Lesson 10.6: Clean up property setters and PropertyChanged notifications

A reader noticed the IsCompleted column in the Quest datagrid is not refreshed when the player completes a quest. That’s because the IsCompleted property in the QuestStatus class was not set to raise a PropertyChanged notification.

This lesson will fix that.

We’ll also remove some setters we don’t need on properties (which will prevent us from unintentionally setting those values elsewhere in the program) and take advantage of the CallerMemberName attribute, to clean up the code a bit.

 

 

Lesson Steps

Step 1: Modify Engine\BaseNotificationClass.cs

The first thing we’ll do is simplify raising the PropertyChanged event. Currently, we pass the name of the property into the OnPropertyChanged function, using “nameof(MyProperty)”. That ensures the correct property name is used in the notification.

However, we can simplify that when we want to send a notification for the property whose setter is calling OnPropertyChanged.

Add a new “using” statement, to include System.Runtime.CompilerServices in the class. Then, change the parameter on line 10 to the one shown below. With this change, if we don’t pass a property name into the function, it will use the name of the property that called the function.

If you look at the CurrentLocation setter, in the GameSession class, you’ll see that we call OnPropertyChanged several times. We could change the call on line 50, with “nameof(CurrentLocation)”, to use the parameterless version, because we are calling OnPropertyChanged in the CurrentLocation setter. However, we still need to pass in the property names for the “HasLocation…” properties.

 

BaseNotificationClass.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Engine
{
    public class BaseNotificationClass : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

 

Step 2: Modify Engine\Models\QuestStatus.cs

Change line 3, so this class inherits from BaseNotificationClass. Change the IsCompleted property to use a backing variable, and have it call OnProperyChanged on line 14 – without “nameof(IsCompleted)”, since we are in IsCompleted’s setter.

The PlayerQuest property should never change, once the object is instantiated. So, you can remove the “set;” on line 7. This is so the property can only be set in the constructor. We don’t need to make this change, but it will make our code a bit safer. Now, we can’t make a mistake elsewhere in the program and try to set that property to a different value.

It often helps to write our code so we can’t make a mistake.

 

QuestStatus.cs

namespace Engine.Models
{
    public class QuestStatus : BaseNotificationClass
    {
        private bool _isCompleted;

        public Quest PlayerQuest { get; }
        public bool IsCompleted
        {
            get { return _isCompleted;}
            set
            {
                _isCompleted = value;
                OnPropertyChanged();
            }
        }

        public QuestStatus(Quest quest)
        {
            PlayerQuest = quest;
            IsCompleted = false;
        }
    }
}

 

Step 3: Modify Engine\Models\GameItem.cs

The properties in these objects should only be set when the object is instantiated. So, we can remove “set;” on lines 5 to 8.

 

GameItem.cs

namespace Engine.Models
{
    public class GameItem
    {
        public int ItemTypeID { get; }
        public string Name { get; }
        public int Price { get; }
        public bool IsUnique { get; }

        public GameItem(int itemTypeID, string name, int price, bool isUnique = false)
        {
            ItemTypeID = itemTypeID;
            Name = name;
            Price = price;
            IsUnique = isUnique;
        }

        public GameItem Clone()
        {
            return new GameItem(ItemTypeID, Name, Price, IsUnique);
        }
    }
}

 

Step 4: Modify Engine\Models\ItemQuantity.cs

Remove “set;” from the properties on lines 5 and 6.

 

ItemQuantity.cs

namespace Engine.Models
{
    public class ItemQuantity
    {
        public int ItemID { get; }
        public int Quantity { get; }

        public ItemQuantity(int itemID, int quantity)
        {
            ItemID = itemID;
            Quantity = quantity;
        }
    }
}

 

Step 5: Modify Engine\Models\LivingEntity.cs

We can change lines 24, 34, 44, 54, and 64, to use the new parameterless version of OnPropertyChanged.

We’ll also remove the “set;” for the Inventory property (line 68) and the GroupedInventory property (line 70).

 

LivingEntity.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Engine.Models
{
    public abstract class LivingEntity : BaseNotificationClass
    {
        #region Properties

        private string _name;
        private int _currentHitPoints;
        private int _maximumHitPoints;
        private int _gold;
        private int _level;

        public string Name
        {
            get { return _name; }
            private set
            {
                _name = value;
                OnPropertyChanged();
            }
        }

        public int CurrentHitPoints
        {
            get { return _currentHitPoints; }
            private set
            {
                _currentHitPoints = value;
                OnPropertyChanged();
            }
        }

        public int MaximumHitPoints
        {
            get { return _maximumHitPoints; }
            protected set
            {
                _maximumHitPoints = value;
                OnPropertyChanged();
            }
        }

        public int Gold
        {
            get { return _gold; }
            private set
            {
                _gold = value;
                OnPropertyChanged();
            }
        }

        public int Level
        {
            get { return _level; }
            protected set
            {
                _level = value;
                OnPropertyChanged();
            }
        }

        public ObservableCollection<GameItem> Inventory { get; }

        public ObservableCollection<GroupedInventoryItem> GroupedInventory { get; }

        public List<GameItem> Weapons =>
            Inventory.Where(i => i is Weapon).ToList();

        public bool IsDead => CurrentHitPoints <= 0;

        #endregion

        public event EventHandler OnKilled;

        protected LivingEntity(string name, int maximumHitPoints, int currentHitPoints, 
                               int gold, int level = 1)
        {
            Name = name;
            MaximumHitPoints = maximumHitPoints;
            CurrentHitPoints = currentHitPoints;
            Gold = gold;
            Level = level;

            Inventory = new ObservableCollection<GameItem>();
            GroupedInventory = new ObservableCollection<GroupedInventoryItem>();
        }

        public void TakeDamage(int hitPointsOfDamage)
        {
            CurrentHitPoints -= hitPointsOfDamage;

            if(IsDead)
            {
                CurrentHitPoints = 0;
                RaiseOnKilledEvent();
            }
        }

        public void Heal(int hitPointsToHeal)
        {
            CurrentHitPoints += hitPointsToHeal;

            if(CurrentHitPoints > MaximumHitPoints)
            {
                CurrentHitPoints = MaximumHitPoints;
            }
        }

        public void CompletelyHeal()
        {
            CurrentHitPoints = MaximumHitPoints;
        }

        public void ReceiveGold(int amountOfGold)
        {
            Gold += amountOfGold;
        }

        public void SpendGold(int amountOfGold)
        {
            if(amountOfGold > Gold)
            {
                throw new ArgumentOutOfRangeException($"{Name} only has {Gold} gold, and cannot spend {amountOfGold} gold");
            }

            Gold -= amountOfGold;
        }

        public void AddItemToInventory(GameItem item)
        {
            Inventory.Add(item);

            if(item.IsUnique)
            {
                GroupedInventory.Add(new GroupedInventoryItem(item, 1));
            }
            else
            {
                if(!GroupedInventory.Any(gi => gi.Item.ItemTypeID == item.ItemTypeID))
                {
                    GroupedInventory.Add(new GroupedInventoryItem(item, 0));
                }

                GroupedInventory.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
            }

            OnPropertyChanged(nameof(Weapons));
        }

        public void RemoveItemFromInventory(GameItem item)
        {
            Inventory.Remove(item);

            GroupedInventoryItem groupedInventoryItemToRemove = item.IsUnique ? 
                GroupedInventory.FirstOrDefault(gi => gi.Item == item) : 
                GroupedInventory.FirstOrDefault(gi => gi.Item.ItemTypeID == item.ItemTypeID);

            if(groupedInventoryItemToRemove != null)
            {
                if(groupedInventoryItemToRemove.Quantity == 1)
                {
                    GroupedInventory.Remove(groupedInventoryItemToRemove);
                }
                else
                {
                    groupedInventoryItemToRemove.Quantity--;
                }
            }

            OnPropertyChanged(nameof(Weapons));
        }

        #region Private functions

        private void RaiseOnKilledEvent()
        {
            OnKilled?.Invoke(this, new System.EventArgs());
        }

        #endregion
    }
}

 

Step 6: Modify Engine\Models\Location.cs and World.cs

The properties of the Location objects should never change, once they are instantiated. To make Location like our other classes, we need to add a constructor, so we can get the values to use for the properties. Then, we can remove the “set;” for its properties.

We won’t do this for the TraderHere property, since that value is set after the object is instantiated.

We also need to change the World.AddLocation function, to call the new constructor.

 

Location.cs

using System.Collections.Generic;
using System.Linq;
using Engine.Factories;

namespace Engine.Models
{
    public class Location
    {
        public int XCoordinate { get; }
        public int YCoordinate { get; }
        public string Name { get; }
        public string Description { get; }
        public string ImageName { get; }

        public List<Quest> QuestsAvailableHere { get; } = new List<Quest>();

        public List<MonsterEncounter> MonstersHere { get; } =
            new List<MonsterEncounter>();

        public Trader TraderHere { get; set; }

        public Location(int xCoordinate, int yCoordinate, string name, string description, string imageName)
        {
            XCoordinate = xCoordinate;
            YCoordinate = yCoordinate;
            Name = name;
            Description = description;
            ImageName = imageName;
        }

        public void AddMonster(int monsterID, int chanceOfEncountering)
        {
            if(MonstersHere.Exists(m => m.MonsterID == monsterID))
            {
                // This monster has already been added to this location.
                // So, overwrite the ChanceOfEncountering with the new number.
                MonstersHere.First(m => m.MonsterID == monsterID)
                            .ChanceOfEncountering = chanceOfEncountering;
            }
            else
            {
                // This monster is not already at this location, so add it.
                MonstersHere.Add(new MonsterEncounter(monsterID, chanceOfEncountering));
            }
        }

        public Monster GetMonster()
        {
            if(!MonstersHere.Any())
            {
                return null;
            }

            // Total the percentages of all monsters at this location.
            int totalChances = MonstersHere.Sum(m => m.ChanceOfEncountering);

            // Select a random number between 1 and the total (in case the total chances is not 100).
            int randomNumber = RandomNumberGenerator.NumberBetween(1, totalChances);

            // Loop through the monster list, 
            // adding the monster's percentage chance of appearing to the runningTotal variable.
            // When the random number is lower than the runningTotal,
            // that is the monster to return.
            int runningTotal = 0;

            foreach(MonsterEncounter monsterEncounter in MonstersHere)
            {
                runningTotal += monsterEncounter.ChanceOfEncountering;

                if(randomNumber <= runningTotal)
                {
                    return MonsterFactory.GetMonster(monsterEncounter.MonsterID);
                }
            }

            // If there was a problem, return the last monster in the list.
            return MonsterFactory.GetMonster(MonstersHere.Last().MonsterID);
        }
    }
}

 

World.cs

using System.Collections.Generic;

namespace Engine.Models
{
    public class World
    {
        private readonly List<Location> _locations = new List<Location>();

        internal void AddLocation(int xCoordinate, int yCoordinate, 
                                  string name, string description, string imageName)
        {
            _locations.Add(new Location(xCoordinate, yCoordinate, name, description,
                                        $"/Engine;component/Images/Locations/{imageName}"));
        }

        public Location LocationAt(int xCoordinate, int yCoordinate)
        {
            foreach(Location loc in _locations)
            {
                if(loc.XCoordinate == xCoordinate && loc.YCoordinate == yCoordinate)
                {
                    return loc;
                }
            }

            return null;
        }
    }
}

 

Step 7: Modify Engine\Models\Monster.cs

Remove the “set;” and “private set;” from lines 5 to 7 and line 9.

Monster.cs

namespace Engine.Models
{
    public class Monster : LivingEntity
    {
        public string ImageName { get; }
        public int MinimumDamage { get; }
        public int MaximumDamage { get; }

        public int RewardExperiencePoints { get; }

        public Monster(string name, string imageName,
                       int maximumHitPoints, int currentHitPoints,
                       int minimumDamage, int maxmumDamage,
                       int rewardExperiencePoints, int gold) : 
            base(name, maximumHitPoints, currentHitPoints, gold)
        {
            ImageName = $"/Engine;component/Images/Monsters/{imageName}";
            MinimumDamage = minimumDamage;
            MaximumDamage = maxmumDamage;
            RewardExperiencePoints = rewardExperiencePoints;
        }
    }
}

 

Step 8: Modify Engine\Models\MonsterEncounter.cs

Remove the “set;” from the MonsterID property. We need to leave it for ChanceOfEncountering, because we may need to reset that property’s value in the Location.AddMonster function. However, we do not need to make this property raise a PropertyChanged notification. Its value is always referenced, each time it’s used.

 

MonsterEncounter.cs

namespace Engine.Models
{
    public class MonsterEncounter
    {
        public int MonsterID { get; }
        public int ChanceOfEncountering { get; set; }

        public MonsterEncounter(int monsterID, int chanceOfEncountering)
        {
            MonsterID = monsterID;
            ChanceOfEncountering = chanceOfEncountering;
        }
    }
}

 

Step 9: Modify Engine\Models\Player.cs

Change lines 21 and 32 to use the parameterless OnPropertyChanged.

Remove the “set;” from line 38.

 

Player.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Engine.Models
{
    public class Player : LivingEntity
    {
        #region Properties

        private string _characterClass;
        private int _experiencePoints;

        public string CharacterClass
        {
            get { return _characterClass; }
            set
            {
                _characterClass = value;
                OnPropertyChanged();
            }
        }

        public int ExperiencePoints
        {
            get { return _experiencePoints; }
            private set
            {
                _experiencePoints = value;

                OnPropertyChanged();

                SetLevelAndMaximumHitPoints();
            }
        }

        public ObservableCollection<QuestStatus> Quests { get; }

        #endregion

        public event EventHandler OnLeveledUp;

        public Player(string name, string characterClass, int experiencePoints,
                      int maximumHitPoints, int currentHitPoints, int gold) : 
            base(name, maximumHitPoints, currentHitPoints, gold)
        {
            CharacterClass = characterClass;
            ExperiencePoints = experiencePoints;

            Quests = new ObservableCollection<QuestStatus>();
        }

        public bool HasAllTheseItems(List<ItemQuantity> items)
        {
            foreach (ItemQuantity item in items)
            {
                if (Inventory.Count(i => i.ItemTypeID == item.ItemID) < item.Quantity)
                {
                    return false;
                }
            }

            return true;
        }

        public void AddExperience(int experiencePoints)
        {
            ExperiencePoints += experiencePoints;
        }

        private void SetLevelAndMaximumHitPoints()
        {
            int originalLevel = Level;

            Level = (ExperiencePoints / 100) + 1;

            if (Level != originalLevel)
            {
                MaximumHitPoints = Level * 10;

                OnLeveledUp?.Invoke(this, System.EventArgs.Empty);
            }
        }
    }
}

 

Step 10: Modify Engine\Models\Quest.cs

Remove “set;” from all the properties from line 7 through 15.

 

Quest.cs

using System.Collections.Generic;

namespace Engine.Models
{
    public class Quest
    {
        public int ID { get; }
        public string Name { get; }
        public string Description { get; }

        public List<ItemQuantity> ItemsToComplete { get; }

        public int RewardExperiencePoints { get; }
        public int RewardGold { get; }
        public List<ItemQuantity> RewardItems { get; }

        public Quest(int id, string name, string description, List<ItemQuantity> itemsToComplete,
                     int rewardExperiencePoints, int rewardGold, List<ItemQuantity> rewardItems)
        {
            ID = id;
            Name = name;
            Description = description;
            ItemsToComplete = itemsToComplete;
            RewardExperiencePoints = rewardExperiencePoints;
            RewardGold = rewardGold;
            RewardItems = rewardItems;
        }
    }
}

 

Step 11: Modify Engine\Models\Weapon.cs

Remove “set;” from the properties on lines 5 and 6.

 

Weapon.cs

namespace Engine.Models
{
    public class Weapon : GameItem
    {
        public int MinimumDamage { get; }
        public int MaximumDamage { get; }

        public Weapon(int itemTypeID, string name, int price, int minDamage, int maxDamage)
            : base(itemTypeID, name, price, true)
        {
            MinimumDamage = minDamage;
            MaximumDamage = maxDamage;
        }

        public new Weapon Clone()
        {
            return new Weapon(ItemTypeID, Name, Price, MinimumDamage, MaximumDamage);
        }
    }
}

 

Step 12: Modify \ViewModels\GameSession.cs

Remove “set;” from line 20.

Use the parameterless OnPropertyChanged on lines 50, 84, and 96.

 

GameSession.cs

using System;
using System.Linq;
using Engine.EventArgs;
using Engine.Factories;
using Engine.Models;

namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        public event EventHandler<GameMessageEventArgs> OnMessageRaised;

        #region Properties

        private Player _currentPlayer;
        private Location _currentLocation;
        private Monster _currentMonster;
        private Trader _currentTrader;

        public World CurrentWorld { get; }

        public Player CurrentPlayer
        {
            get { return _currentPlayer; }
            set
            {
                if(_currentPlayer != null)
                {
                    _currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled -= OnCurrentPlayerKilled;
                }

                _currentPlayer = value;

                if (_currentPlayer != null)
                {
                    _currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled += OnCurrentPlayerKilled;
                }
            }
        }

        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;

                OnPropertyChanged();
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));

                CompleteQuestsAtLocation();
                GivePlayerQuestsAtLocation();
                GetMonsterAtLocation();

                CurrentTrader = CurrentLocation.TraderHere;
            }
        }

        public Monster CurrentMonster
        {
            get { return _currentMonster; }
            set
            {
                if(_currentMonster != null)
                {
                    _currentMonster.OnKilled -= OnCurrentMonsterKilled;
                }
                
                _currentMonster = value;

                if(_currentMonster != null)
                {
                    _currentMonster.OnKilled += OnCurrentMonsterKilled;

                    RaiseMessage("");
                    RaiseMessage($"You see a {CurrentMonster.Name} here!");
                }

                OnPropertyChanged();
                OnPropertyChanged(nameof(HasMonster));
            }
        }

        public Trader CurrentTrader
        {
            get { return _currentTrader; }
            set
            {
                _currentTrader = value; 
                
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasTrader));
            }
        }

        public Weapon CurrentWeapon { get; set; }

        public bool HasLocationToNorth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;

        public bool HasLocationToEast => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;

        public bool HasLocationToSouth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;

        public bool HasLocationToWest => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;

        public bool HasMonster => CurrentMonster != null;

        public bool HasTrader => CurrentTrader != null;

        #endregion

        public GameSession()
        {
            CurrentPlayer = new Player("Scott", "Fighter", 0, 10, 10, 1000000);

            if (!CurrentPlayer.Weapons.Any())
            {
                CurrentPlayer.AddItemToInventory(ItemFactory.CreateGameItem(1001));
            }

            CurrentWorld = WorldFactory.CreateWorld();

            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }

        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }

        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }

        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }

        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }

        private void CompleteQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                QuestStatus questToComplete =
                    CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
                                                             !q.IsCompleted);

                if(questToComplete != null)
                {
                    if(CurrentPlayer.HasAllTheseItems(quest.ItemsToComplete))
                    {
                        // Remove the quest completion items from the player's inventory
                        foreach (ItemQuantity itemQuantity in quest.ItemsToComplete)
                        {
                            for(int i = 0; i < itemQuantity.Quantity; i++)
                            {
                                CurrentPlayer.RemoveItemFromInventory(CurrentPlayer.Inventory.First(item => item.ItemTypeID == itemQuantity.ItemID));
                            }
                        }

                        RaiseMessage("");
                        RaiseMessage($"You completed the '{quest.Name}' quest");

                        // Give the player the quest rewards
                        RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
                        CurrentPlayer.AddExperience(quest.RewardExperiencePoints);

                        RaiseMessage($"You receive {quest.RewardGold} gold");
                        CurrentPlayer.ReceiveGold(quest.RewardGold);

                        foreach(ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);

                            RaiseMessage($"You receive a {rewardItem.Name}");
                            CurrentPlayer.AddItemToInventory(rewardItem);
                        }

                        // Mark the Quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }

        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));

                    RaiseMessage("");
                    RaiseMessage($"You receive the '{quest.Name}' quest");
                    RaiseMessage(quest.Description);

                    RaiseMessage("Return with:");
                    foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }

                    RaiseMessage("And you will receive:");
                    RaiseMessage($"   {quest.RewardExperiencePoints} experience points");
                    RaiseMessage($"   {quest.RewardGold} gold");
                    foreach(ItemQuantity itemQuantity in quest.RewardItems)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                }
            }
        }

        private void GetMonsterAtLocation()
        {
            CurrentMonster = CurrentLocation.GetMonster();
        }

        public void AttackCurrentMonster()
        {
            if (CurrentWeapon == null)
            {
                RaiseMessage("You must select a weapon, to attack.");
                return;
            }

            // Determine damage to monster
            int damageToMonster = RandomNumberGenerator.NumberBetween(CurrentWeapon.MinimumDamage, CurrentWeapon.MaximumDamage);

            if (damageToMonster == 0)
            {
                RaiseMessage($"You missed the {CurrentMonster.Name}.");
            }
            else
            {
                RaiseMessage($"You hit the {CurrentMonster.Name} for {damageToMonster} points.");
                CurrentMonster.TakeDamage(damageToMonster);
            }

            if(CurrentMonster.IsDead)
            {
                // Get another monster to fight
                GetMonsterAtLocation();
            }
            else
            {
                // Let the monster attack
                int damageToPlayer = RandomNumberGenerator.NumberBetween(CurrentMonster.MinimumDamage, CurrentMonster.MaximumDamage);

                if (damageToPlayer == 0)
                {
                    RaiseMessage($"The {CurrentMonster.Name} attacks, but misses you.");
                }
                else
                {
                    RaiseMessage($"The {CurrentMonster.Name} hit you for {damageToPlayer} points.");
                    CurrentPlayer.TakeDamage(damageToPlayer);
                }
            }
        }

        private void OnCurrentPlayerKilled(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage("");
            RaiseMessage($"The {CurrentMonster.Name} killed you.");

            CurrentLocation = CurrentWorld.LocationAt(0, -1);
            CurrentPlayer.CompletelyHeal();
        }

        private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage("");
            RaiseMessage($"You defeated the {CurrentMonster.Name}!");

            RaiseMessage($"You receive {CurrentMonster.RewardExperiencePoints} experience points.");
            CurrentPlayer.AddExperience(CurrentMonster.RewardExperiencePoints);

            RaiseMessage($"You receive {CurrentMonster.Gold} gold.");
            CurrentPlayer.ReceiveGold(CurrentMonster.Gold);

            foreach(GameItem gameItem in CurrentMonster.Inventory)
            {
                RaiseMessage($"You receive one {gameItem.Name}.");
                CurrentPlayer.AddItemToInventory(gameItem);
            }
        }

        private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage($"You are now level {CurrentPlayer.Level}!");
        }

        private void RaiseMessage(string message)
        {
            OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
        }
    }
}

 

Step 13: Compile the program, and make sure it runs.

There were a lot of changes, mostly to make the code cleaner. This is often when errors are created.

 

Return to main page

28 thoughts on “Lesson 10.6: Clean up property setters and PropertyChanged notifications

  1. Hey Scott, another great lesson!  I’ve got a couple of questions:

    First,  we remove the setters for the auto-implemented properties in this lesson, but I don’t understand how we are able to assign values to those properties if they don’t have setters. How does the backing field get set without a setter method?  Is this just for constructors?

     

    Also,  in my output window of Visual Studio, I’ve noticed some strange errors involving the combo box. Everything is working correctly it seems.  Are these errors indicating some problem in the XAML or the C#?   Below are the errors:

     

    System.Windows.Data Error: 40 : BindingExpression path error: ‘ID’ property not found on ‘object’ ”Weapon’ (HashCode=33489078)’. BindingExpression:Path=ID; DataItem=’Weapon’ (HashCode=33489078); target element is ‘ComboBox’ (Name=”); target property is ‘NoTarget’ (type ‘Object’)

     

    System.Windows.Data Error: 4 : Cannot find source for binding with reference ‘RelativeSource FindAncestor, AncestorType=’System.Windows.Controls.ItemsControl’, AncestorLevel=’1”. BindingExpression:Path=HorizontalContentAlignment; DataItem=null; target element is ‘ComboBoxItem’ (Name=”); target property is ‘HorizontalContentAlignment’ (type ‘HorizontalAlignment’)

    System.Windows.Data Error: 4 : Cannot find source for binding with reference ‘RelativeSource FindAncestor, AncestorType=’System.Windows.Controls.ItemsControl’, AncestorLevel=’1”. BindingExpression:Path=VerticalContentAlignment; DataItem=null; target element is ‘ComboBoxItem’ (Name=”); target property is ‘VerticalContentAlignment’ (type ‘VerticalAlignment’)

    1. Thanks Gary!

      Yes, removing the “set;” makes it so the hidden backing variable that the compiler auto-creates can only be set by constructors. If we were to write out the code differently, it would look like this. The “readonly” in this version is what makes it so the variable can only be set by a constructor. Removing “set;” from an auto-property does the same thing:
      public class MyClass
      {
      private readonly int _backingVariableForID;

      public ID
      {
      get { return _backingVariableForID; }
      }

      public MyClass(int id)
      {
      _backingVariableForID = id;
      }
      }

      This is similar to creating immutable (unchangeable) objects in many of the functional languages – such as F#.

      For the error messages you see, can you upload your version of the solution to Dropbox or GitHub, so I can check it out?

  2. Thanks Scott,  that makes sense.  I see how it works now.

    Also, I put my solution and all my files up on a GitHub repository, but I’m not sure how to point you to it.  This is the link , I hope that works.  I think it should.

    LINK REMOVED FOR PRIVACY

    1. I found the source of the error message. It took a while, because it’s a runtime error (happens while running the game).

      The weapon ComboBox in MainWindow.xaml is trying to use “ID” for the SelectedValuePath. However, the property on Weapon is actually “ItemTypeID”. You can change it to use ItemTypeID, or just ignore it (and remove “SelectedValuePath=”ID””), since we don’t use SelectedValue for anything. We use CurrentWeapon, which is bound to SelectedItem.

  3. Thanks Scott for your lessons, again! I followed your video step by step but I have a weird issue now when I run the program, a red X appears in this line inside the WorldFactory class.

    newWorld.AddLocation(-2, -1, “Farmer’s Field”, “There are rows of corn growing here, with giant rats hiding between them”, “FarmFields.png”);

    newWorld.LocationAt(-2, -1).AddMonster(2, 100); (here is when the X appears)

    And I get this message.

    System.Reflection.TargetInvocationException: ‘Exception has been thrown by the target of an invocation.’
    NullReferenceException: Object reference not set to an instance of an object.

    Could this happen ‘cos we removed the setters from the Location class? I wrote the constructor following the steps in your video, so I am puzzled..

    EDIT: I removed that line and I get the same error in a similar line, but related to Traders.

    newWorld.LocationAt(-1, -1).TraderHere = TraderFactory.GetTraderByName(“Farmer Ted”);

    EDIT 2: Fixed. The issue was with my Location.cs code, I copied yours from this page and now it works. This was my code

    CODE REMOVED, SINCE YOU FOUND THE ERROR

  4. Finally, found the issue with my code. XCoordinate = XCoordinate; in the constructor instead of XCoordinate=xCoordinate . I feel silly now.

    That being said, I am so thankful for your lessons. You’re an excellent teacher, Scott.

    best wishes

  5. If you double click an item in either the inventory or quests tabs besides the headers an exception is thrown. The error is:
    “System.InvalidOperationException was unhandled
    Message: An unhandled exception of type ‘System.InvalidOperationException’ occurred in PresentationFramework.dll
    Additional information: A TwoWay or OneWayToSource binding cannot work on the read-only property ‘Name’ of type ‘Engine.Models.Weapon’.”

    If I change the mode of the xaml binding to oneway it fixes the problem. I don’t know if that is the correct way to fix the problem though. The behavior doesn’t present itself in the traderscreen.

  6. Hi Scott, thank you for great lecture. I am learning so much every day.
    I have a question.
    in LivingEntity.cs

    public ObservableCollection Inventory { get; }

    public ObservableCollection GroupedInventory { get; }

    Wouldn’t it be beneficial to leave them as is {get; set;}?

    Won’t there be chance in the future that creature’s inventory may change for some reason?

    1. For the ObservableCollection properties, and other types of collection properties, you probably only want to initialize (set) them once – inside the class constructor.

      When we want to change the items in the collection, we will call the Add() or Remove() functions on the collection property, instead of making a new list and completely resetting the value of the property.

      Is that clear?

  7. Hi Scott.
    In the MainWindow.xaml.cs file I have using Engine.EventArgs;
    I think at some stage I made a mistake and changed this from EventsArg to EventArgs.
    Now the program does not accept Engine.EventArgs and says EventArgs does not exsist in the namespace Engine.
    Under Engine in the Solutions window I have a folder called EventArgs and under the folder I have GameMessageEventArgs.cs

    Should I have a file called EventArgs.cs under the Engine?

    Brian

    1. Check inside your GameMessageEventArgs.cs class, and make sure the namespace around the class says “Engine.EventArgs”, and not “Engine.EventArg”. The namespace inside the class is the value you’ll need to have in the “using” statement in other classes.

      If that doesn’t work, can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it?

  8. Hello Scott, thank you for this amazing tutorial! I have one question about objects in the inventory. Currently, when I run the program, I am able to click on the item name in the inventory and take the text out/rename the item while playing the game. How would I go about changing it to where the user can’t edit the item’s name in game?

  9. I noticed what appeared to be a typo in line 13 on Monster.cs but in line 19 you also use the incorrect spelling for maxmumDamage parameter so not a defect. You also correctly spelled the property name. Resulting fine: -3 DKP. 😉

    1. I’ll add that to the list of things to fix in the upcoming “bug fix and cleanup” videos. You’d think I’d be a better typist after working with computers for almost forty years. 🙁

  10. Scott,
    I fixed the Error: 40 that Gary was talking about earlier but also getting this error and seems to be after turning in a quest?

    System.Windows.Data Error: 4 : Cannot find source for binding with reference ‘RelativeSource FindAncestor, AncestorType=’System.Windows.Controls.ItemsControl’, AncestorLevel=’1”. BindingExpression:Path=HorizontalContentAlignment; DataItem=null; target element is ‘ComboBoxItem’ (Name=”); target property is ‘HorizontalContentAlignment’ (type ‘HorizontalAlignment’)

    System.Windows.Data Error: 4 : Cannot find source for binding with reference ‘RelativeSource FindAncestor, AncestorType=’System.Windows.Controls.ItemsControl’, AncestorLevel=’1”. BindingExpression:Path=VerticalContentAlignment; DataItem=null; target element is ‘ComboBoxItem’ (Name=”); target property is ‘VerticalContentAlignment’ (type ‘VerticalAlignment’)

    Thanks,
    James

    1. Hi James,

      Are you able to run the program, without errors?

      This looks like one of those weird things that WPF reports as an error, even though it isn’t really an error. If you really want to get rid of the warnings, you can check out https://stackoverflow.com/questions/47391020/cannot-find-source-for-binding-with-reference-relativesource-findancestor or https://stackoverflow.com/questions/15070861/comboboxitem-continues-to-throw-binding-error-despite-style.

      Personally, I just ignore them. That’s probably not the best thing to do, since I might end up ignoring a real problem.

      Let me know if the program has a visible problem (like the quest not being updated, or something). If so, I’d want to investigate that.

      Scott

      1. Scott,
        The program runs and quest updates and quest says completed, but just one of those things I noticed in the output window. I will look at the links you posted and let you know if I was able to find a solution? Is here or Discord a better place for these type of questions?
        James

        1. James,

          Either place is good for questions.

          This might be better for questions about problems with the code in the lessons, so other people with the same problem can see the solution. Discord might be better if you’re modifying the program and asking questions about your specific changes. But, I usually check both places at the same time.

          Scott

  11. Scott,
    Okay throwing in the towel. I have no idea how to fix other than ignoring it for now.
    For someone who has mild case of OCD it bothers me 🙂
    Thanks,
    James

  12. Scott,
    Theory I have is that System.Windows.Data Error: 4 is somehow caused by the weapon ComboBox having a null default item and when completing the quest the weapon selection is updated. The Error goes back to when we introduced the selection of the combat item.

    I tried this and seems to have cleared the Error: 4
    In the MainWindow.xaml file I gave the ComboBox under the combat controls a name.

    <ComboBox
        x:Name="WeaponSelection"
        Grid.Row="0" Grid.Column="0"
        ItemsSource="{Binding CurrentPlayer.Weapons, Mode=OneWay}"
        SelectedItem="{Binding CurrentWeapon}"
        DisplayMemberPath="Name"
        SelectedValuePath="ItemTypeID"/>

    Then in the MainWindow.xaml.cs I added to lines to the MainWindow() function

    public MainWindow()
    {
        InitializeComponent();
        _gameSession.OnMessageRaised += OnGameMessageRaised;
        DataContext = _gameSession;
    
        WeaponSelection.ItemsSource = _gameSession.CurrentPlayer.Weapons;
        WeaponSelection.SelectedItem = _gameSession.CurrentPlayer.Weapons[0];
    }

    This is probably in no way how this should be done and I know that the player has the pointy stick for the first weapon at this point.

    Program ran and Error 4 is gone when I finish the quest.

    James

      1. Scott,
        The code I have above breaks how you select weapons works but just modified to show weapon combobox is causing this ERROR when turning in the quest.
        Something I have noticed with out my breaking modifications is if you sell the pointy stick at another vendor before going back to turn in the quest you don’t get the error.
        James

  13. Scott,
    Think this may have solved the ERROR 4 i was seeing.

    In GameSession.cs I added a little helper function.

    private void UpdateItemsSelectionButtons()
    {
      // keep weapon combo-box populated
      if(CurrentPlayer.CurrentWeapon == null && CurrentPlayer.Weapons.Count != 0) {
        var weapons = CurrentPlayer.Weapons;
        CurrentPlayer.CurrentWeapon = weapons.First();
      }
    
      // keep consumable combo-box populated
      if(CurrentPlayer.HasConsumable) {
        var consumable = CurrentPlayer.Consumables;
        CurrentPlayer.CurrentConsumable = consumable.First();
      }
    }

    Then I place function before leaving the if statement in the CraftItemUsing function,

    at the end of the UseCurrentConsumable function, and in the CurrentLocation setter.

    https://github.com/Jtaim/SOSCSRPG and see the DefaultWeapon branch.

Leave a Reply

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