You’ve been writing event handler methods since Chapter 1, but this is the first time you’re firing an event. You’ll learn all about how this works and what’s going on in Chapter 15. For now, all you need to know is that an interface can include an event, and that your
OnPropertyChanged() method is following a standard C# pattern for raising events to other objects.
This is a standard
.NET pattern for
raising events.
Don’t forget to implement
INotifyPropertyChanged.
Data binding works only when the controls implement that interface.
If you leave : INotifyPropertyChanged
out of the class declaration, your bound controls won’t get updated—even if the data object fires PropertyChanged events.
528 Appendix ii
go fish goes xaml
Finish porting the Go Fish! game to a WPF application. You’ll need to modify the XAML from earlier in this chapter to add data binding, copy all the classes and enums from the Go Fish! game in Chapter 8 (or download them from our website), and update the Player and Game classes.
Add the existing class files and change their namespace to match your app. Add these files to your project from the Chapter 8 Go Fish! code: Values.cs, Suits.cs, Card.cs, Deck.cs,
CardComparer_bySuit.cs, CardComparer_byValue.cs, Game.cs, and Player.cs. You can use the Add Existing
Item option in the Solution Explorer, but you’ll need to change the namespace in each of them to match your new projects (just like you did with multipart projects earlier in the book).
Try building your project. You should get errors in Game.cs and Player.cs that look like this: 1
Remove all references to WinForms classes and objects; add using lines to Game.
You’re not in the WinForms world anymore, so delete using System.Windows.Forms; from
the top of Game.cs and Player.cs. You’ll also need to remove all mentions of TextBox. You’ll need to modify the Game class to use INotifyPropertyChanged and ObservableCollection<T>, so add these using lines to the top of Game.cs:
using System.ComponentModel;
using System.Collections.ObjectModel; 2
Add an instance of Game as a static resource and set up the data context.
Modify your XAML to add an instance of Game as a static resource and use it as the data context for the grid that contains the Go Fish! page you built earlier in the chapter. Here’s the XAML for the static resource: <local:Game x:Key="game"/> — and you’re going to need a new constructor because you can include only resources that have parameterless constructors:
public Game() {
PlayerName = "Ed";
Hand = new ObservableCollection<string>(); ResetGame();
} 3
Add public properties to the Game class for data binding.
Here are the properties you’ll be binding to properties of the controls in the page: public bool GameInProgress { get; private set; }
public bool GameNotStarted { get { return !GameInProgress; } } public string PlayerName { get; set; }
public ObservableCollection<string> Hand { get; private set; } public string Books { get { return DescribeBooks(); } }
public string GameProgress { get; private set; } 4
Make sure you add the
<Window.Resources>
section to the top of your XAML, and you’ll also need
to add the xmlns:local tag, exactly like you did on
you are here 4 529 windows presentation foundation
public void StartGame() { ClearProgress(); GameInProgress = true;
OnPropertyChanged("GameInProgress"); OnPropertyChanged("GameNotStarted"); Random random = new Random();
players = new List<Player>();
players.Add(new Player(PlayerName, random, this)); players.Add(new Player("Bob", random, this)); players.Add(new Player("Joe", random, this)); Deal();
players[0].SortHand(); Hand.Clear();
foreach (String cardName in GetPlayerCardNames()) Hand.Add(cardName);
if (!GameInProgress)
AddProgress(DescribePlayerHands()); OnPropertyChanged("Books");
} public void GameInProgress = false;ResetGame() {
OnPropertyChanged("GameInProgress"); OnPropertyChanged("GameNotStarted"); books = new Dictionary<Values, Player>(); stock = new Deck();
Hand.Clear(); }
public void AddProgress(string progress) { GameProgress = progress + Environment.NewLine + GameProgress; OnPropertyChanged("GameProgress"); }
IsEnabled="{Binding GameInProgress}" IsEnabled="{Binding GameNotStarted}" Use binding to enable or disable the TextBox, ListBox, and Buttons.
You want the “Your Name” TextBox and the “Start the game!” Button to be enabled only when the game is not started, and you want the “Your hand” ListBox and “Ask for a card” Button to be enabled only when the game is in progress. You’ll add code to the Game class to set the
GameInProgress property. Have a look at the GameNotStarted property. Figure out how it works, and then add the following property bindings to the TextBox, ListBox, and two Buttons: 5 IsEnabled="{Binding GameNotStarted}" IsEnabled="{Binding GameInProgress}"
You’ll need
two of each
of these.
Modify the Player class so it tells the Game to display the game’s progress. The WinForms version of the Player class takes a TextBox as a parameter for its constructor. Change that to take a reference to the Game class and store it in a private field. (Look at the
StartGame() method below to see how this new constructor is used when adding players.) Find the lines that use the TextBox reference and replace them with calls to the Game object’s
AddProgress() method. 6
Modify the Game class.
Change the PlayOneRound() method so that it’s void instead of returning a Boolean, and have it use the AddProgress() method instead of the TextBox to display progress. If a player won, display that progress, reset the game, and return. Otherwise, refresh the Hand collection and describe the hands. You’ll also need to add/update these four methods and figure out what they do and how they work. 7
public void ClearProgress() { GameProgress = String.Empty; OnPropertyChanged("GameProgress"); }
You’ll also need to implement the
INotifyPropertyChanged
interface and add the same
OnPropertyChanged() method that you used in the MenuMaker class. The updated methods use it, and your
modified PullOutBooks() method
530 Appendix ii
exercise solution
class Player {
private string name;
public string Name { get { return name; } } private Random random;
private Deck cards; private Game game;
public Player(String name, Random random, Game game) { this.name = name;
this.random = random; this.game = game;
this.cards = new Deck(new Card[] { });
game.AddProgress(name + " has just joined the game"); }
public Deck DoYouHaveAny(Values value) {
Deck cardsIHave = cards.PullOutValues(value);
game.AddProgress(Name + " has " + cardsIHave.Count + " " + Card.Plural(value)); return cardsIHave;
}
public void AskForACard(List<Player> players, int myIndex, Deck stock, Values value) { game.AddProgress(Name + " asks if anyone has a " + value);
int totalCardsGiven = 0;
for (int i = 0; i < players.Count; i++) { if (i != myIndex) {
Player player = players[i];
Deck CardsGiven = player.DoYouHaveAny(value); totalCardsGiven += CardsGiven.Count; while (CardsGiven.Count > 0) cards.Add(CardsGiven.Deal()); } } if (totalCardsGiven == 0) {
game.AddProgress(Name + " must draw from the stock."); cards.Add(stock.Deal());
} }
// ... the rest of the Player class is the same ...
A
Game game;
public MainWindow() { InitializeComponent();
game = this.FindResource("game") as Game; }
private void startButton_Click(object sender, RoutedEventArgs e) { game.StartGame();
}
private void askForACard_Click(object sender, RoutedEventArgs e) { if (cards.SelectedIndex >= 0)
game.PlayOneRound(cards.SelectedIndex); }
private void cards_MouseDoubleClick(object sender, MouseButtonEventArgs e) { if (cards.SelectedIndex >= 0)
game.PlayOneRound(cards.SelectedIndex); }
These are the changes needed for the Player class:
you are here 4 531 windows presentation foundation
<Grid Margin="10" DataContext="{StaticResource ResourceKey=game}"> <TextBlock Text="Your Name" />
<StackPanel Orientation="Horizontal" Grid.Row="1">
<TextBox x:Name="playerName" FontSize="12" Width="150" Text="{Binding PlayerName, Mode=TwoWay}" IsEnabled="{Binding GameNotStarted}" />
<Button x:Name="startButton" Margin="5,0" IsEnabled="{Binding GameNotStarted}" Content="Start the game!" Click="startButton_Click"/>
</StackPanel>
<TextBlock Text="Game progress" Grid.Row="2" Margin="0,10,0,0"/>
<ScrollViewer Grid.Row="3" FontSize="12" Background="White" Foreground="Black" Content="{Binding GameProgress}" />
<TextBlock Text="Books" Margin="0,10,0,0" Grid.Row="4"/>
<ScrollViewer FontSize="12" Background="White" Foreground="Black" Grid.Row="5" Grid.RowSpan="2"
Content="{Binding Books}" />
<TextBlock Text="Your hand" Grid.Row="0" Grid.Column="2" /> <ListBox x:Name="cards" Background="White" FontSize="12" Height="Auto" Margin="0,0,0,10"
Grid.Row="1" Grid.RowSpan="5" Grid.Column="2"
ItemsSource="{Binding Hand}" IsEnabled="{Binding GameInProgress}" MouseDoubleClick="cards_MouseDoubleClick" />
<Button x:Name="askForACard" Content="Ask for a card"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="6" Grid.Column="2"
Click="askForACard_Click" IsEnabled="{Binding GameInProgress}" /> <Grid.ColumnDefinitions> <ColumnDefinition Width="5*"/> <ColumnDefinition Width="40"/> <ColumnDefinition Width="2*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MinHeight="150" /> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid>
A These are the changes needed for the XAML: