Since we now have traders in the world, we need to add a trade screen.
Step 1: Create a new Window in the WPFUI project named TradeScreen
Add the ViewModels namespace and set DataContext. When we instantiate a TradeScreen window, we’ll set its DataContext to the GameSession object from the main game window (on lines 6 and 7).
When we define the datagrids to show the inventories (the player’s and the trader’s), we set “AutoGenerateColumns” to “false” (lines 41 and 72). This is because we want to add a button to buy or sell the listed item.
The button definitions are on lines 56-64 and 87-95. This is how we can define the template for the cells however we want – in this case to display a button. Each button calls a function in the code-behind page to manage the purchase or sale of the selected item.
In the code-behind page (TradeScreen.xaml.cs), we have a private property named Session. It is the DataContext object, cast as a GameSession object. This is how we’ll reference the GameSession object, when we change the player’s gold, the player’s inventory, and the trader’s inventory.
We don’t really need a property for this – we could just cast the DataContext as a GameSession object whenever we need to access it. But, this eliminates doing that multiple times for the buy and sell functions.
In the buy and sell functions, the first thing we do is get the item that sent the click event (the row in the datagrid where the user clicked the buy or sell button) and cast it as a GameItem object. This is the object we will add/remove from the inventories, and use to determine the price.
In both functions, we check that the cast “item” variable is not null – in case the case to a GameItem didn’t work. It always should work, but it’s good to have a check.
In the “sell” function, if we have a GameItem object, we give the player gold, add the item to the trader’s inventory, and remove the item from the player’s inventory.
In the “buy” function, we do an additional check to see if the player has enough gold to buy the item. If they don’t we display a message and do nothing else. If the player has enough gold, we subtract the price of the item from their gold, remove the item from the trader’s inventory, and add the item to the player’s inventory.
We don’t need to do anything else. Because the properties all raise propertychanged events, the UI is automatically updated.
TradeScreen.xaml
<Window x:Class="WPFUI.TradeScreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine"
d:DataContext="{d:DesignInstance viewModels:GameSession}"
mc:Ignorable="d"
WindowStartupLocation="CenterOwner"
FontSize="11pt"
Title="Trade Screen" Height="480" Width="640">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
Content="{Binding CurrentTrader.Name}"/>
<Label Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Center"
Content="Your Inventory"/>
<Label Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Center"
Content="Trader's Inventory"/>
<DataGrid Grid.Row="2" Grid.Column="0"
Margin="10"
ItemsSource="{Binding CurrentPlayer.Inventory}"
AutoGenerateColumns="False"
HeadersVisibility="Column"
CanUserAddRows="False"
CanUserDeleteRows="False"
VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="Description"
IsReadOnly="True"
Width="*"
Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="Price"
IsReadOnly="True"
Width="Auto"
Binding="{Binding Path=Price}"/>
<DataGridTemplateColumn MinWidth="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Click="OnClick_Sell"
Width="55"
Content="Sell"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid Grid.Row="2" Grid.Column="1"
Margin="10"
ItemsSource="{Binding CurrentTrader.Inventory}"
AutoGenerateColumns="False"
HeadersVisibility="Column"
CanUserAddRows="False"
CanUserDeleteRows="False"
VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="Description"
IsReadOnly="True"
Width="*"
Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="Price"
IsReadOnly="True"
Width="Auto"
Binding="{Binding Path=Price}"/>
<DataGridTemplateColumn MinWidth="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Click="OnClick_Buy"
Width="55"
Content="Buy"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="3" Grid.Column="1"
HorizontalAlignment="Right"
Width="75"
Content="Close"
Click="OnClick_Close"/>
</Grid>
</Window>
TradeScreen.xaml.cs
using System.Windows;
using Engine.Models;
using Engine.ViewModels;
namespace WPFUI
{
/// <summary>
/// Interaction logic for TradeScreen.xaml
/// </summary>
public partial class TradeScreen : Window
{
public GameSession Session => DataContext as GameSession;
public TradeScreen()
{
InitializeComponent();
}
private void OnClick_Sell(object sender, RoutedEventArgs e)
{
GameItem item = ((FrameworkElement)sender).DataContext as GameItem;
if(item != null)
{
Session.CurrentPlayer.Gold += item.Price;
Session.CurrentTrader.AddItemToInventory(item);
Session.CurrentPlayer.RemoveItemFromInventory(item);
}
}
private void OnClick_Buy(object sender, RoutedEventArgs e)
{
GameItem item = ((FrameworkElement)sender).DataContext as GameItem;
if(item != null)
{
if(Session.CurrentPlayer.Gold >= item.Price)
{
Session.CurrentPlayer.Gold -= item.Price;
Session.CurrentTrader.RemoveItemFromInventory(item);
Session.CurrentPlayer.AddItemToInventory(item);
}
else
{
MessageBox.Show("You do not have enough gold");
}
}
}
private void OnClick_Close(object sender, RoutedEventArgs e)
{
Close();
}
}
}
Step 2: Modify MainWindow.xaml and MainWindow.xaml.cs
On line 266 of MainWindow.xaml, add the Click event handler “OnClick_DisplayTradeScreen” to the Trade button.
In MainWindow.xml.cs, add the new ” OnClick_DisplayTradeScreen ” function. This function instantiates a new Trade window, set its DataContext with the GameSession, and display the Trade window.
Instances of classes we define (like the GameSession object) are passed to other classes “by reference”, instead of “by value”.
So, there is only one GameSession object, which will be used by the main game screen and the trade screen. Any changes to the GameSession’s properties in the TradeScreen window will be reflected in MainWindow, because they both reference the same object.
We use ShowDialog(), instead of Show(), to prevent the player from clicking buttons on the game screen, until they close the trade screen. ShowDialog() is “modal”, because it prevents clicking on the other screen. Show() would be “non-modal”, because it would let you click on the other screen.
We want to prevent clicking on the other screen, because it would let the player move to a different location, which may not have a trader, or may have a different trader. That could be confusing to the player.
MainWindow.xaml
<Window x:Class="WPFUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine"
d:DataContext="{d:DesignInstance viewModels:GameSession}"
mc:Ignorable="d"
FontSize="11pt"
Title="Scott's Awesome Game" Height="768" Width="1024">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="225"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Menu -->
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="Menu" Background="AliceBlue"/>
<!-- Player stats -->
<Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
<Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/>
<Label Grid.Row="1" Grid.Column="0" Content="Class:"/>
<Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CharacterClass}"/>
<Label Grid.Row="2" Grid.Column="0" Content="Hit points:"/>
<Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.HitPoints}"/>
<Label Grid.Row="3" Grid.Column="0" Content="Gold:"/>
<Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
<Label Grid.Row="4" Grid.Column="0" Content="XP:"/>
<Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
<Label Grid.Row="5" Grid.Column="0" Content="Level:"/>
<Label Grid.Row="5" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
</Grid>
<!-- Gameplay -->
<Grid Grid.Row="1" Grid.Column="1"
Background="Beige">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- Game Messages -->
<Border Grid.Row="0" Grid.Column="0"
Grid.RowSpan="2"
BorderBrush="Gainsboro"
BorderThickness="1">
<RichTextBox x:Name="GameMessages"
Background="Beige"
VerticalScrollBarVisibility="Auto">
<RichTextBox.Resources>
<Style TargetType="{x:Type Paragraph}">
<Setter Property="Margin" Value="0"/>
</Style>
</RichTextBox.Resources>
</RichTextBox>
</Border>
<!-- Location information -->
<Border Grid.Row="0" Grid.Column="1"
BorderBrush="Gainsboro"
BorderThickness="1">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
HorizontalAlignment="Center"
Text="{Binding CurrentLocation.Name}"/>
<Image Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="125"
Width="125"
Source="{Binding CurrentLocation.ImageName}"/>
<TextBlock Grid.Row="2"
HorizontalAlignment="Center"
Text="{Binding CurrentLocation.Description}"
TextWrapping="Wrap"/>
</Grid>
</Border>
<!-- Monster information -->
<Border Grid.Row="1" Grid.Column="1"
BorderBrush="Gainsboro"
BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
HorizontalAlignment="Center"
Height="Auto"
Text="{Binding CurrentMonster.Name}" />
<Image Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="125"
Width="125"
Source="{Binding CurrentMonster.ImageName}" />
<StackPanel Grid.Row="2"
Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
HorizontalAlignment="Center"
Orientation="Horizontal">
<TextBlock>Current Hit Points:</TextBlock>
<TextBlock Text="{Binding CurrentMonster.HitPoints}" />
</StackPanel>
</Grid>
</Border>
</Grid>
<!-- Inventory and Quests -->
<Grid Grid.Row="2" Grid.Column="0"
Background="BurlyWood">
<TabControl>
<TabItem Header="Inventory">
<DataGrid ItemsSource="{Binding CurrentPlayer.Inventory}"
AutoGenerateColumns="False"
HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="Description"
Binding="{Binding Name}"
Width="*"/>
<DataGridTextColumn Header="Price"
Binding="{Binding Price}"
Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Quests">
<DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
AutoGenerateColumns="False"
HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding PlayerQuest.Name}"
Width="*"/>
<DataGridTextColumn Header="Done?"
Binding="{Binding IsCompleted}"
Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl>
</Grid>
<!-- Action controls -->
<Grid Grid.Row="2" Grid.Column="1"
Background="Lavender">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="255" />
</Grid.ColumnDefinitions>
<!-- Combat Controls -->
<Grid Grid.Row="0" Grid.Column="0"
Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<ComboBox Grid.Row="0" Grid.Column="0"
ItemsSource="{Binding CurrentPlayer.Weapons}"
SelectedItem="{Binding CurrentWeapon}"
DisplayMemberPath="Name"
SelectedValuePath="ID"/>
<Button Grid.Row="0" Grid.Column="2"
Content="Use"
Click="OnClick_AttackMonster"/>
</Grid>
<!-- Movement Controls -->
<Grid Grid.Row="0" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="1"
Height="25" Width="65" Margin="10"
Click="OnClick_MoveNorth"
Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
Content="North"/>
<Button Grid.Row="1" Grid.Column="0"
Height="25" Width="65" Margin="10"
Click="OnClick_MoveWest"
Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
Content="West"/>
<Button Grid.Row="1" Grid.Column="1"
Height="25" Width="65" Margin="10"
Click="OnClick_DisplayTradeScreen"
Visibility="{Binding HasTrader, Converter={StaticResource BooleanToVisibility}}"
Content="Trade"/>
<Button Grid.Row="1" Grid.Column="2"
Height="25" Width="65" Margin="10"
Click="OnClick_MoveEast"
Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
Content="East"/>
<Button Grid.Row="2" Grid.Column="1"
Height="25" Width="65" Margin="10"
Click="OnClick_MoveSouth"
Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
Content="South"/>
</Grid>
</Grid>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Documents;
using Engine.EventArgs;
using Engine.ViewModels;
namespace WPFUI
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly GameSession _gameSession = new GameSession();
public MainWindow()
{
InitializeComponent();
_gameSession.OnMessageRaised += OnGameMessageRaised;
DataContext = _gameSession;
}
private void OnClick_MoveNorth(object sender, RoutedEventArgs e)
{
_gameSession.MoveNorth();
}
private void OnClick_MoveWest(object sender, RoutedEventArgs e)
{
_gameSession.MoveWest();
}
private void OnClick_MoveEast(object sender, RoutedEventArgs e)
{
_gameSession.MoveEast();
}
private void OnClick_MoveSouth(object sender, RoutedEventArgs e)
{
_gameSession.MoveSouth();
}
private void OnClick_AttackMonster(object sender, RoutedEventArgs e)
{
_gameSession.AttackCurrentMonster();
}
private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
{
GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));
GameMessages.ScrollToEnd();
}
private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e)
{
TradeScreen tradeScreen = new TradeScreen();
tradeScreen.Owner = this;
tradeScreen.DataContext = _gameSession;
tradeScreen.ShowDialog();
}
}
}
Step 3: Test the game
Move to a location that has a trader, click the “Trade” button, and try to buy or sell items. You should notice the player’s gold change, along with their inventory in the main game screen. This is because these properties all have propertychanged events. So, the UI is automatically notified of changes.
NEXT LESSON: Lesson 10.1: Refactoring Base Class for Player, Monster, and Trader
PREVIOUS LESSON: Lesson 09.1: Creating Traders