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

 

 

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

 

 

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

 

 

Step 4: Modify Engine\Models\ItemQuantity.cs

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

 

ItemQuantity.cs

 

 

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

 

 

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

 

 

World.cs

 

 

Step 7: Modify Engine\Models\Monster.cs

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

Monster.cs

 

 

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

 

 

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

 

 

Step 10: Modify Engine\Models\Quest.cs

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

 

Quest.cs

 

 

Step 11: Modify Engine\Models\Weapon.cs

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

 

Weapon.cs

 

 

Step 12: Modify \ViewModels\GameSession.cs

Remove “set;” from line 20.

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

 

GameSession.cs

 

 

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

9 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.

Leave a Reply

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