Lesson 13.2: More keyboard actions (and fixes)

I’m back from taking time off for my birthday, so this lesson will be a simple one. We’ll add the ability to change the displayed tab to Inventory, Quests, or Recipes. Plus, we’ll fix a potential crash if the user clicks on the datagrid and tries to use the keyboard actions.



Lesson Steps


Step 1: Modify WPFUI\MainWindow.xaml

There was a problem with the last change.

If the user clicks on one of the rows in the Inventory, Quests, or Recipes datagrid, that row has “focus” – the program thinks that is what we want to work with. Then, if the user presses the “W” key (to move North), the program will think they want to enter data into the datagrid cell that has focus.

Since some of the bound properties are readonly (they don’t have setters), this causes the program to crash.


To fix this problem, we’ll update the XAML Bindings with “Mode=OneWay”. Then, the UI will know that is should only read values from the properties, and not try to change the property’s values from the UI.


The code for this change in MainWindow.xaml is shown in step 2, because we’re making some other changes to the same part of the code. Look at lines 175, 180, 182, 195, 198, and 211 of the new code, to see where we add the OneWay mode to the binding.



Step 2: Modify WPFUI\MainWindow.xaml.cs

I also had a request to show how to use keyboard input to change the focus of the Inventory, Quests, and Recipes tabs. I also added the ability to use a keypress to show the trade screen – if the player is at a location with a trader.


To set focus on a TabItem, we need a way to distinguish the three different TabItems in the TabControl and set the focus to (display) the desired TabItem.

The TabControl object holds a collection of its TabItems. We could reference them by using an index: Inventory is at index 0, Quests is at index 1, and Recipes is at index 2.

But, if we ever add a new tab, or change the existing tabs’ order, we would need to remember to change the code to use the new index values.

To prevent that future problem, we’ll give each TabItem a Name value and use that name to set the focus. This is done in MainWindow.xaml, on lines 169, 189, and 205.

We also needed to give a name to the TabControl, so we can reference it in the code-behind page. On line 167, add the new attribute: x:Name=”PlayerDataTabControl”.


In MainWindow.xaml.cs, on lines 94-96, I added three new keys to watch for “I” (for inventory), “Q” (for quests), and “R” (for recipes). When the user presses one of those keys, we call a new SetTabFocusTo() function, passing in the name of the TabItem we went to set the focus on.


Add the new SetTabFocusTo function to lines 108-121.

We loop through the PlayerDataTabControl’s Items property (its collection of TabItems) and checks if the TabItem’s Name matches the value of the passed-in parameter. If it matches, it sets that TabItem’s IsSelected property to “true”, making is the displayed TabItem, and stops looking at the other TabItems for one with a matching name.

Notice line 112 “if(item is TabItem tabItem)”. It might look a little strange. This assigns the current item to the “tabItem” variable, whose expected datatype is TabItem. If the item is a TabItem, the “if” statement will evaluate to “true”, and the program will run the code inside. The code is functionally the same as the code below, but combine the first two lines into one:

// Try to cast "item" to a TabItem.
// tabItem is null if it fails ("item" is not a TabItem object).
TabItem tabItem = item as TabItem;
if(tabItem != null)
    tabItem.IsSelected = true;


To display the trade screen, I added line 97, which adds a new value to _userInputActions that looks for a “T” keypress and calls the function that displays the trade screen – the same function we use for the “Trade” button.

Because the user can press “T” when they’re at a location without a trader, we need to add a “guard clause” around the code that displays the trade screen. This is the new “if” statement on lines 71-77.


MainWindow.xaml.cs (lines 163-226, the “Inventory, Quests, and Recipes” section)

        <!-- Inventory, Quests, and Recipes -->
        <Grid Grid.Row="2" Grid.Column="0"

            <TabControl x:Name="PlayerDataTabControl">
                <TabItem Header="Inventory"
                    <DataGrid ItemsSource="{Binding CurrentPlayer.GroupedInventory}"
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Item.Name, Mode=OneWay}"
                            <DataGridTextColumn Header="Qty"
                                                Binding="{Binding Quantity, Mode=OneWay}"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Item.Price, Mode=OneWay}"

                <TabItem Header="Quests"
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name, Mode=OneWay}"
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted, Mode=OneWay}"

                <TabItem Header="Recipes"
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Recipes}"
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding Name, Mode=OneWay}"
                            <DataGridTemplateColumn MinWidth="75">
                                        <Button Click="OnClick_Craft"



using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using Engine.EventArgs;
using Engine.Models;
using Engine.ViewModels;

namespace WPFUI
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
        private readonly GameSession _gameSession = new GameSession();
        private readonly Dictionary<Key, Action> _userInputActions = 
            new Dictionary<Key, Action>();

        public MainWindow()


            _gameSession.OnMessageRaised += OnGameMessageRaised;

            DataContext = _gameSession;

        private void OnClick_MoveNorth(object sender, RoutedEventArgs e)

        private void OnClick_MoveWest(object sender, RoutedEventArgs e)

        private void OnClick_MoveEast(object sender, RoutedEventArgs e)

        private void OnClick_MoveSouth(object sender, RoutedEventArgs e)

        private void OnClick_AttackMonster(object sender, RoutedEventArgs e)

        private void OnClick_UseCurrentConsumable(object sender, RoutedEventArgs e)

        private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
            GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));

        private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e)
            if(_gameSession.CurrentTrader != null)
                TradeScreen tradeScreen = new TradeScreen();
                tradeScreen.Owner = this;
                tradeScreen.DataContext = _gameSession;

        private void OnClick_Craft(object sender, RoutedEventArgs e)
            Recipe recipe = ((FrameworkElement)sender).DataContext as Recipe;

        private void InitializeUserInputActions()
            _userInputActions.Add(Key.W, () => _gameSession.MoveNorth());
            _userInputActions.Add(Key.A, () => _gameSession.MoveWest());
            _userInputActions.Add(Key.S, () => _gameSession.MoveSouth());
            _userInputActions.Add(Key.D, () => _gameSession.MoveEast());
            _userInputActions.Add(Key.Z, () => _gameSession.AttackCurrentMonster());
            _userInputActions.Add(Key.C, () => _gameSession.UseCurrentConsumable());
            _userInputActions.Add(Key.I, () => SetTabFocusTo("InventoryTabItem"));
            _userInputActions.Add(Key.Q, () => SetTabFocusTo("QuestsTabItem"));
            _userInputActions.Add(Key.R, () => SetTabFocusTo("RecipesTabItem"));
            _userInputActions.Add(Key.T, () => OnClick_DisplayTradeScreen(this, new RoutedEventArgs()));

        private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)

        private void SetTabFocusTo(string tabName)
            foreach(object item in PlayerDataTabControl.Items)
                if (item is TabItem tabItem)
                    if (tabItem.Name == tabName)
                        tabItem.IsSelected = true;


Step 3: Test the game.

Be sure to click on the different datagrids (especially the text values in the rows) and press some keys.


NOTE: The next thing I’ll probably work on is moving the data in the factories to XML files, so you can modify those files to add more locations, monster, items, etc. to the game – without needing to recompile the program. But, if there are features you want to see done first, please leave a comment and let me know.


Return to main page

12 thoughts on “Lesson 13.2: More keyboard actions (and fixes)

  1. hello Scott and all

    if clicked on the game message window then used the shortcut keys it would enter that key char into the game message window.. it would still perform the action(move/fight)

    nothing major but thought ill point it

    i think can be fixed by making it  read only and un-selected AcceptsReturn  via the properties


    or via code if change line 84

    82. <RichTextBox x:Name=”GameMessages”
    83.  Background=”Beige”
    84. VerticalScrollBarVisibility=”Auto” IsReadOnly=”True” AcceptsReturn=”False”>

    using that cant type there but still can select and copy the game messages if for some reason need to


    thanks for the lessons

    1. Ah, yes. The RichTextBox is another control we’re using only to display a value, but can be used to input values. Thanks for catching that and sharing your change. I’ll update the post to include that code tomorrow (hopefully – I’ve been busy on a project lately).

  2. Hi Scott,
    im looking forward for the xml part but is there a possibility that we can display the requierments for crafting or the quest requierements as a tooltip or sth cause it is hard to keep track af all ur quests and recipes


  3. Scott,
    I’m really enjoying this series so far but I have a question. I replaced the health text with a progress bar that I set to red to simulate a health bar. It worked for a while but I got to lessen 11.? something and the program would no longer run as long as the progress bar was active in the xaml file. I commented it out and all was good. Is there a way we can add a progress bar for health, magic etc?

    1. Hi Drew,

      I’m not sure what would cause the problem. But, I added a note to show how to connect progress bars in the UI. After I finish the current factory/XML lessons, I will do several UI changes/fixes that have been requested, and will include this as part of that.

  4. Hi Scott, another good lesson.

    I added the arrow keys as secondary movement keys, as I’m left handed. If I have selected a tab or list object in the combo box the arrow keys stop working for movement and now move up and down in the Tab box instead. E.G up and down the inventory list.

    Are the arrow keys hardcoded somewhere in WPF? Any way of blocking this behaviour?

    It’s quite common as a left hander to see gamnes where the arrows or numpad cant be bound correctly.

    Thanks in advance

    1. Hi Mark,

      You could probably use something like the code here: https://www.codeproject.com/Questions/1121828/How-do-I-stop-auto-selection-with-down-arrow-key-i. By having the handler function set “e.Handled = true;” It should stop the natural processing of the arrow keys that would occur in controls that normally allow navigation (comboboxes, data grids, etc.). You’d have to check for the different control types – the sample code in the link only handles ComboBoxes.

  5. Hi,
    I also wanted to close the TraderScreen on second pressing of T so I added in TradeScreen.xaml.cs a function(TradeWindow_OnKeyDown) to check if T was pressed if(e.Key==Key.T) and close TraderScreen if true. It works so far.

  6. Hi,
    Loving the tutorial so far.
    One tiny thing I’ve noticed in this lesson is your guard clause for the trader uses
    if (_gameSession.CurrentTrader != null)
    but we also have a HasTrader property.
    It might be tidier (and more fluent?) to use
    if (_gameSession.HasTrader).
    Of course it’s doing exactly the same thing anyway.


    1. Thanks Julian. A added an issue to the bug tracker to make sure backing variables are only accessed by a property’s getter and setter – never from another property’s getter or setter, or from a function (unless there is a specific need to do that).

Leave a Reply

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