Contents
- Introduction
- Background
- Step-by-Step
- Solitaire and Augmented Reality
- Final Thoughts
Introduction
In this article, I will show you how to create the classic Solitaire and Spider Solitaire games using WPF. I have based these games closely around the versions found
in Windows Vista onwards. First, some screenshots:
Klondike Solitaire
Spider Solitaire
The Casino (Home Page)
Background
I've been meaning to write this up for a while but it has turned into one of those 'never quite done' projects. There are a couple more things I would have loved
to get in but I think the time has come to draw a line under the project - I'm leaving a list of nice-to-have features at the end of the article so if anyone fancies
contributing, then go right ahead!
Step-By-Step
I'm going to go through the whole project step-by-step, so I'm building a brand new project from scratch and taking you through it. However, some bits that might
be repetitive or are generic will be brushed over - if anyone feels anything is missing, then please comment and I'll elaborate.
Step 1: Build the Projects
Create a new WPF application named Solitaire (I've targeted .NET 4, I recommend you do the same so the code I've written will work for you).
Immediately add a new WPF User Control Library to the solution named 'SolitaireGames'. This is where we'll stick the solitaire game code and the control that hosts it,
we'll keep it in a separate library in case we ever want to add it to another project. In the project 'Solitaire', add a reference to the project 'SolitaireGames'.
In SolitaireGames, delete 'UserControl1.xaml', we don't need it.
We'll be using the MVVM design pattern in this project, I am using my own lightweight library Apex.
I have included the distributable Apex.dll at the top of the article, both of these projects will need to have it as a dependency. And now we're good to go.
Step 2: Create Core Classes
Well, we're going to need classes and enumerations to represent playing cards. Let's add them one-by-one to the SolitaireGames project. First, create a file called CardColor.cs:
namespace SolitaireGames
{
public enum CardColour
{
Black,
Red
}
}
Not even a using
directive, nice and easy.
Now create CardSuit.cs:
namespace SolitaireGames
{
public enum CardSuit
{
Hearts,
Diamonds,
Clubs,
Spades
}
}
So far so good, finally we need the most important enumeration - the CardType
enum. Add CardType.cs:
namespace SolitaireGames
{
public enum CardType
{
HA,
H2,
H3,
H4,
H5,
H6,
H7,
H8,
H9,
H10,
HJ,
HQ,
HK,
DA,
D2,
D3,
D4,
D5,
D6,
D7,
D8,
D9,
D10,
DJ,
DQ,
DK,
CA,
C2,
C3,
C4,
C5,
C6,
C7,
C8,
C9,
C10,
CJ,
CQ,
CK,
SA,
S2,
S3,
S4,
S5,
S6,
S7,
S8,
S9,
S10,
SJ,
SQ,
SK
}
}
OK, with this one, I skipped the 'comment every enum member' rule, we're really going wild now.
The next file we're going to add is PlayingCard.cs, and with this one, we'll take it bit by bit.
using Apex.MVVM;
namespace SolitaireGames
{
public class PlayingCard : ViewModel
{
public CardSuit Suit
{
get
{
int enumVal = (int)CardType;
if (enumVal < 13)
return CardSuit.Hearts;
if (enumVal < 26)
return CardSuit.Diamonds;
if(enumVal < 39)
return CardSuit.Clubs;
return CardSuit.Spades;
}
}
The PlayingCard
class is a ViewModel, as described in the Apex article. All that this does is give us access
to the NotifyingProperty
construct, which handles all the INotifyPropertyChanged
stuff we get in a ViewModel class; we'll see more of this in a bit.
What is a playing card? Well, in this context, a playing card is more than just the face value, it is a card that is played, i.e., we know not just its value but
also whether it is face down and so on. The first property is just a little helper property that gets the suit - based on the numeric value
of the card type (which is a property defined later!).
public int Value
{
get
{
return ((int)CardType) % 13;
}
}
public CardColour Colour
{
get
{
return ((int)CardType) < 26 ?
CardColour.Red : CardColour.Black;
}
}
The card value is another helper property, useful when we want to see if one card is higher than another and so on. We also have the card colour property - again useful when
comparing cards. So far these are all read only properties - they're just helpers and all based on CardType
, which comes next.
private NotifyingProperty CardTypeProperty =
new NotifyingProperty("CardType", typeof(CardType), CardType.SA);
public CardType CardType
{
get { return (CardType)GetValue(CardTypeProperty); }
set { SetValue(CardTypeProperty, value); }
}
OK, this might look a bit unfamiliar. With Apex, we define NotifyingProperty
s - they are written to look very similar
to dependency properties but automatically handle calling NotifyPropertyChanged
- so all the important members of this class will be notifying properties, and when their values
change, anything bound to them will know.
The next four properties are IsFaceDown
(is the card face down in the game), IsPlayable
(can this card be moved around by the user),
and FaceDownOffset
/FaceUpOffset
(these will be used occasionally when laying out cards). Here they are and this ends the PlayingCard.cs file:
private NotifyingProperty IsFaceDownProperty =
new NotifyingProperty("IsFaceDown", typeof(bool), false);
public bool IsFaceDown
{
get { return (bool)GetValue(IsFaceDownProperty); }
set { SetValue(IsFaceDownProperty, value); }
}
private NotifyingProperty IsPlayableProperty =
new NotifyingProperty("IsPlayable", typeof(bool), false);
public bool IsPlayable
{
get { return (bool)GetValue(IsPlayableProperty); }
set { SetValue(IsPlayableProperty, value); }
}
private NotifyingProperty FaceDownOffsetProperty =
new NotifyingProperty("FaceDownOffset", typeof(double), default(double));
public double FaceDownOffset
{
get { return (double)GetValue(FaceDownOffsetProperty); }
set { SetValue(FaceDownOffsetProperty, value); }
}
private NotifyingProperty FaceUpOffsetProperty =
new NotifyingProperty("FaceUpOffset",
typeof(double), default(double));
public double FaceUpOffset
{
get { return (double)GetValue(FaceUpOffsetProperty); }
set { SetValue(FaceUpOffsetProperty, value); }
}
A little bit of forward thinking here. If you are familiar with the pivot control for Windows Phone 7, this is roughly how we're going to present this app.
There'll be a pivot control with four items - Klondike Solitaire (which is just standard solitaire in Windows), the 'Casino' (where we see the statistics and can go to any other game),
Spider Solitaire, and the settings.
As we've got more than one card game, we'll create a common base class for the ViewModel for a card game. Create a file called CardGameViewModel.cs:
using System;
using System.Collections.Generic;
using Apex.MVVM;
using System.Windows.Threading;
namespace SolitaireGames
{
public class CardGameViewModel : ViewModel
{
I don't normally put properties and members first, but doing it this way will make it easier to go through. For a game, we need:
- A timer to time how long we've been playing
- A score
- An elapsed time
- A 'moves' counter (number of distinct moves made)
- A flag to indicate the game is won
- An event that is fired when the game is won
- A few Commands - Go to casino, card clicked, and deal a new game
Commands are handled in Apex via the ViewModelCommand
class, we'll see more later. Anywhere, here are the properties and members we'll need for a card game:
private DispatcherTimer timer = new DispatcherTimer();
private DateTime lastTick;
private NotifyingProperty scoreProperty =
new NotifyingProperty("Score", typeof(int), 0);
public int Score
{
get { return (int)GetValue(scoreProperty); }
set { SetValue(scoreProperty, value); }
}
private readonly NotifyingProperty elapsedTimeProperty =
new NotifyingProperty("ElapsedTime",
typeof(double), default(double));
public TimeSpan ElapsedTime
{
get { return TimeSpan.FromSeconds(
(double)GetValue(elapsedTimeProperty)); }
set { SetValue(elapsedTimeProperty, value.TotalSeconds); }
}
private readonly NotifyingProperty movesProperty =
new NotifyingProperty("Moves", typeof(int), 0);
public int Moves
{
get { return (int)GetValue(movesProperty); }
set { SetValue(movesProperty, value); }
}
private NotifyingProperty isGameWonProperty =
new NotifyingProperty("IsGameWon", typeof(bool), false);
public bool IsGameWon
{
get { return (bool)GetValue(isGameWonProperty); }
set { SetValue(isGameWonProperty, value); }
}
private ViewModelCommand leftClickCardCommand;
public ViewModelCommand LeftClickCardCommand
{
get { return leftClickCardCommand; }
}
private ViewModelCommand rightClickCardCommand;
public ViewModelCommand RightClickCardCommand
{
get { return rightClickCardCommand; }
}
private ViewModelCommand goToCasinoCommand;
public ViewModelCommand GoToCasinoCommand
{
get { return goToCasinoCommand; }
}
private ViewModelCommand dealNewGameCommand;
public ViewModelCommand DealNewGameCommand
{
get { return dealNewGameCommand; }
}
public event Action GameWon;
By now you should be familiar with NotifyingProperty
s, and ViewModelCommand
is a very standard MVVM command object,
more info is on the Apex article.
Right the bulk of this class is what we have above, let's finish it off.
public CardGameViewModel()
{
timer.Interval = TimeSpan.FromMilliseconds(500);
timer.Tick += new EventHandler(timer_Tick);
leftClickCardCommand = new ViewModelCommand(DoLeftClickCard, true);
rightClickCardCommand = new ViewModelCommand(DoRightClickCard, true);
dealNewGameCommand = new ViewModelCommand(DoDealNewGame, true);
goToCasinoCommand = new ViewModelCommand(DoGoToCasino, true);
}
private void DoGoToCasino()
{
}
protected virtual void DoLeftClickCard(object parameter)
{
}
protected virtual void DoRightClickCard(object parameter)
{
}
protected virtual void DoDealNewGame(object parameter)
{
StopTimer();
ElapsedTime = TimeSpan.FromSeconds(0);
Moves = 0;
Score = 0;
IsGameWon = false;
}
The constructor sets up the timer and creates the ViewModelCommand
s. Note that three of the commands do nothing but they must
exist - they're protected
, and also derived classes may need to do special things with them. The DoDealNewGame
is going to get fun later on, but for now,
it just resets everything. This class is nearly done - we just offer a way to start and stop the timer and a simple way to fire the GameWon
event - remember events cannot
be directly invoked from derived classes!
public void StartTimer()
{
lastTick = DateTime.Now;
timer.Start();
}
public void StopTimer()
{
timer.Stop();
}
private void timer_Tick(object sender, EventArgs e)
{
DateTime timeNow = DateTime.Now;
ElapsedTime += timeNow - lastTick;
lastTick = timeNow;
}
protected void FireGameWonEvent()
{
Action wonEvent = GameWon;
if (wonEvent != null)
wonEvent();
}
OK, we've spent quite a bit of time with the core classes, it's time to actually start making the game.
Step 3: Klondike Solitaire - The Logic
Klondike Solitaire is our familiar friend 'Solitaire' in Windows. We'll name it KlondikeSolitaire in this project to distinguish it from Spider Solitaire or any others we have.
Add a folder called KlondikeSolitaire to the SolitaireGames project.
Create a class called KlondikeSolitaireViewModel
, this is going to hold all the logic for the game.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Apex.MVVM;
using Apex.Extensions;
using System.Collections.ObjectModel;
namespace SolitaireGames.KlondikeSolitaire
{
public enum DrawMode
{
DrawOne = 0,
DrawThree = 1
}
public class KlondikeSolitaireViewModel : CardGameViewModel
{
One useful little enum at the top of the file is DrawMode
- it defines whether we draw one or three cards. Again, I prefer to have member variables and properties
at the end, but I'll write them out in an order here that makes it easier to describe:
List<observablecollection<playingcard>> foundations =
new List<observablecollection<playingcard>>();
List<observablecollection<playingcard>> tableaus =
new List<observablecollection<playingcard>>();
private ObservableCollection<playingcard> foundation1 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> foundation2 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> foundation3 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> foundation4 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau1 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau2 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau3 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau4 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau5 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau6 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> tableau7 =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> stock =
new ObservableCollection<playingcard>();
private ObservableCollection<playingcard> waste =
new ObservableCollection<playingcard>();
Some nomenclature: A Foundation is where we build a run of suited cards, the top four piles in Solitaire. We've got four of them and they're observable collections.
A Tableau is a set of cards, some face up or face down where we do most of the work, we move cards between tableau according to the rules of the game. Klondike has seven tableaus,
with one to seven cards in each to begin with. The Stock is the set of face down cards we can 'turn' into the Waste - the Waste is the small pile of cards we can draw from.
We'll also want a list that holds each Tableau and Foundation, this'll come in handy later. Here are the last properties:
private NotifyingProperty DrawModeProperty =
new NotifyingProperty("DrawMode", typeof(DrawMode),
DrawMode.DrawThree);
public DrawMode DrawMode
{
get { return (DrawMode)GetValue(DrawModeProperty); }
set { SetValue(DrawModeProperty, value); }
}
public ObservableCollection<playingcard> Foundation1 { get { return foundation1; } }
public ObservableCollection<playingcard> Foundation2 { get { return foundation2; } }
public ObservableCollection<playingcard> Foundation3 { get { return foundation3; } }
public ObservableCollection<playingcard> Foundation4 { get { return foundation4; } }
public ObservableCollection<playingcard> Tableau1 { get { return tableau1; } }
public ObservableCollection<playingcard> Tableau2 { get { return tableau2; } }
public ObservableCollection<playingcard> Tableau3 { get { return tableau3; } }
public ObservableCollection<playingcard> Tableau4 { get { return tableau4; } }
public ObservableCollection<playingcard> Tableau5 { get { return tableau5; } }
public ObservableCollection<playingcard> Tableau6 { get { return tableau6; } }
public ObservableCollection<playingcard> Tableau7 { get { return tableau7; } }
public ObservableCollection<playingcard> Stock { get { return stock; } }
public ObservableCollection<playingcard> Waste { get { return waste; } }
private ViewModelCommand turnStockCommand;
public ViewModelCommand TurnStockCommand
{
get { return turnStockCommand; }
}
Now we have the familiar notifying property for the draw mode, a set of accessors for the various card stacks, and finally a command - 'Turn Stock',
which will move cards from the Stock to the Waste.
Now we can add the functionality.
public KlondikeSolitaireViewModel()
{
foundations.Add(foundation1);
foundations.Add(foundation2);
foundations.Add(foundation3);
foundations.Add(foundation4);
tableaus.Add(tableau1);
tableaus.Add(tableau2);
tableaus.Add(tableau3);
tableaus.Add(tableau4);
tableaus.Add(tableau5);
tableaus.Add(tableau6);
tableaus.Add(tableau7);
turnStockCommand = new ViewModelCommand(DoTurnStock, true);
if (Apex.Design.DesignTime.IsDesignTime)
DoDealNewGame(null);
}
public IList<playingcard> GetCardCollection(PlayingCard card)
{
if (stock.Contains(card)) return stock;
if (waste.Contains(card)) return waste;
foreach (var foundation in foundations)
if (foundation.Contains(card)) return foundation;
foreach (var tableau in tableaus)
if (tableau.Contains(card)) return tableau;
return null;
}
The constructor adds the Foundations and Tableaus to the master list, wires up the Turn Stock command, and (if we're in the designer) actually deals
a game (this'll be useful later, in the design view, we'll have a freshly dealt game to see). We also have a helper function to get the parent collection
of any card (the View will need this later).
Now we have the first chunk of serious logic:
protected override void DoDealNewGame(object parameter)
{
base.DoDealNewGame(parameter);
stock.Clear();
waste.Clear();
foreach (var tableau in tableaus)
tableau.Clear();
foreach (var foundation in foundations)
foundation.Clear();
List<cardtype> eachCardType = new List<cardtype>();
foreach (CardType cardType in Enum.GetValues(typeof(CardType)))
eachCardType.Add(cardType);
List<playingcard> playingCards = new List<playingcard>();
foreach (var cardType in eachCardType)
playingCards.Add(new PlayingCard()
{ CardType = cardType, IsFaceDown = true });
playingCards.Shuffle();
for (int i = 0; i < 7; i++)
{
for (int j = 0; j < i; j++)
{
PlayingCard faceDownCard = playingCards.First();
playingCards.Remove(faceDownCard);
faceDownCard.IsFaceDown = true;
tableaus[i].Add(faceDownCard);
}
PlayingCard faceUpCard = playingCards.First();
playingCards.Remove(faceUpCard);
faceUpCard.IsFaceDown = false;
faceUpCard.IsPlayable = true;
tableaus[i].Add(faceUpCard);
}
foreach (var playingCard in playingCards)
{
playingCard.IsFaceDown = true;
playingCard.IsPlayable = false;
stock.Add(playingCard);
}
playingCards.Clear();
StartTimer();
}
I hope I've commented it well enough for you to follow it. It basically sets up the newly dealt game, arranging the cards. Note that we set IsPlayable
when appropriate - this'll be used later to make sure that we can only drag cards that we should be able to drag.
The main 'command' of the View Model logic-wise is the Turn Stock command, which will turn one or three cards from the Stock to the Waste or clear the Waste:
private void DoTurnStock()
{
if (stock.Count == 0)
{
foreach (var card in waste)
{
card.IsFaceDown = true;
card.IsPlayable = false;
stock.Insert(0, card);
}
waste.Clear();
}
else
{
foreach (var wasteCard in waste)
wasteCard.FaceUpOffset = 0;
int cardsToDraw = 0;
switch (DrawMode)
{
case DrawMode.DrawOne:
cardsToDraw = 1;
break;
case DrawMode.DrawThree:
cardsToDraw = 3;
break;
default:
cardsToDraw = 1;
break;
}
for (int i = 0; i < cardsToDraw; i++)
{
if (stock.Count > 0)
{
PlayingCard card = stock.Last();
stock.Remove(card);
card.IsFaceDown = false;
card.IsPlayable = false;
card.FaceUpOffset = 30;
waste.Add(card);
}
}
}
foreach (var wasteCard in waste)
wasteCard.IsPlayable = wasteCard == waste.Last();
}
In Klondike, we can automatically move a card to the appropriate foundation; we'll need a function to handle this (this happens when we right click on a card).
public bool TryMoveCardToAppropriateFoundation(PlayingCard card)
{
if (waste.LastOrDefault() == card)
foreach (var foundation in foundations)
if (MoveCard(waste, foundation, card, false))
return true;
bool inTableau = false;
int i = 0;
for (; i < tableaus.Count && inTableau == false; i++)
inTableau = tableaus[i].Contains(card);
if (inTableau == false)
return false;
foreach (var foundation in foundations)
if (MoveCard(tableaus[i - 1], foundation, card, false))
return true;
return false;
}
If we right click in some blank space, it'll try and move every card to its appropriate Foundation, so let's put a function together for this:
public void TryMoveAllCardsToAppropriateFoundations()
{
bool keepTrying = true;
while (keepTrying)
{
bool movedACard = false;
if (waste.Count > 0)
if (TryMoveCardToAppropriateFoundation(waste.Last()))
movedACard = true;
foreach (var tableau in tableaus)
{
if (tableau.Count > 0)
if (TryMoveCardToAppropriateFoundation(tableau.Last()))
movedACard = true;
}
keepTrying = movedACard;
}
}
Perfect. Now we have a function inherited from the base class CardGameViewModel
that is called when a card is right clicked,
we can use this to call the 'TryMoveCard...
' function:
protected override void DoRightClickCard(object parameter)
{
base.DoRightClickCard(parameter);
PlayingCard card = parameter as PlayingCard;
if (card == null)
return;
TryMoveCardToAppropriateFoundation(card);
}
Another thing we'll need to know is whether we've won - so let's add a function that can check to see if we've won, and if we have set the IsGameWon
flag:
public void CheckForVictory()
{
foreach (var foundation in foundations)
if (foundation.Count < 13)
return;
IsGameWon = true;
StopTimer();
FireGameWonEvent();
}
Later on we're going to wire up the View so that we can drag cards from one stack to another. When this happens, we need to know if the move is valid and if it
is actually performing the move and update the score. This is the most complicated function of the class, so look carefully over the comments:
public bool MoveCard(ObservableCollection<playingcard> from,
ObservableCollection<playingcard> to,
PlayingCard card, bool checkOnly)
{
if (from == to)
return false;
int scoreModifier = 0;
if (from == Waste)
{
if (foundations.Contains(to))
{
if ((to.Count == 0 && card.Value == 0) ||
(to.Count > 0 && to.Last().Suit ==
card.Suit && (to.Last()).Value == (card.Value - 1)))
{
scoreModifier = 10;
}
else
return false;
}
else if (tableaus.Contains(to))
{
if ((to.Count == 0 && card.Value == 12) ||
(to.Count > 0 && to.Last().Colour != card.Colour
&& (to.Last()).Value == (card.Value + 1)))
{
scoreModifier = 5;
}
else
return false;
}
else
return false;
}
else if (tableaus.Contains(from))
{
if (foundations.Contains(to))
{
if ((to.Count == 0 && card.Value == 0) ||
(to.Count > 0 && to.Last().Suit == card.Suit
&& (to.Last()).Value == (card.Value - 1)))
{
scoreModifier = 10;
}
else
return false;
}
else if (tableaus.Contains(to))
{
if ((to.Count == 0 && card.Value == 12) ||
(to.Count > 0 && to.Last().Colour != card.Colour
&& (to.Last()).Value == (card.Value + 1)))
{
scoreModifier = 0;
}
else
return false;
}
else
return false;
}
else if (foundations.Contains(from))
{
if (tableaus.Contains(to))
{
if ((to.Count == 0 && card.Value == 12) ||
(to.Count > 0 && to.Last().Colour != card.Colour
&& (to.Last()).Value == (card.Value + 1)))
{
scoreModifier = -15;
}
else
return false;
}
else if (foundations.Contains(to))
{
if (from.Count == 1 && to.Count == 0)
{
scoreModifier = 0;
}
else
return false;
}
else
return false;
}
else
return false;
if (checkOnly)
return true;
DoMoveCard(from, to, card);
Score += scoreModifier;
Moves++;
if (from == Waste && Waste.Count > 0)
Waste.Last().IsPlayable = true;
CheckForVictory();
return true;
}
You may have noticed that there is a DoMoveCard
function called near the end; this actually moves the card from one place to another and is a bit more straightforward
than the last function:
private void DoMoveCard(ObservableCollection<playingcard> from,
ObservableCollection<playingcard> to,
PlayingCard card)
{
List<playingcard> run = new List<playingcard>();
for (int i = from.IndexOf(card); i < from.Count; i++)
run.Add(from[i]);
foreach(var runCard in run)
from.Remove(runCard);
foreach(var runCard in run)
to.Add(runCard);
if (from.Count > 0)
{
PlayingCard topCard = from.Last();
topCard.IsFaceDown = false;
topCard.IsPlayable = true;
}
}
That's it - the class is done. Let's move onto the visuals.
Step 3: Klondike Solitaire - The View
We've got a solid ViewModel, Notifying Properties, and Observable Collections as well as Commands, so creating the View should be a breeze.
Add a new UserControl called KlondikeSolitaireView
to the KlondikeSolitaire folder. Let's put the XAML together.
<UserControl x:Class="SolitaireGames.KlondikeSolitaire.KlondikeSolitaireView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SolitaireGames.KlondikeSolitaire"
xmlns:solitaireGames="clr-namespace:SolitaireGames"
xmlns:apexControls="clr-namespace:Apex.Controls;assembly=Apex"
xmlns:apexCommands="clr-namespace:Apex.Commands;assembly=Apex"
xmlns:apexDragAndDrop="clr-namespace:Apex.DragAndDrop;assembly=Apex"
mc:Ignorable="d"
x:Name="klondikeSolitaireView"
d:DesignHeight="300" d:DesignWidth="300">
-->
<UserControl.Resources>
<ResourceDictionary
Source="/SolitaireGames;component/Resources/
SolitaireGamesResourceDictionary.xaml" />
</UserControl.Resources>
The user control is going to use a few different namespaces which we defined early on. We point to a single resource dictionary for the assembly - but we'll go through this afterwards.
<!---->
<apexControls:ApexGrid
Rows="*,Auto"
DataContext="{Binding ViewModel, ElementName=klondikeSolitaireView}">
<!---->
<Viewbox Grid.Row="0" Margin="10">
<!---->
<apexDragAndDrop:DragAndDropHost
x:Name="dragAndDropHost"
MouseRightButtonDown="dragAndDropHost_MouseRightButtonDown"
MinimumHorizontalDragDistance="0.0"
MinimumVerticalDragDistance="0.0">
The whole control is wrapped in a grid (actually an ApexGrid; see this article
for details: apexgrid.aspx) with the game at the top and a small row of buttons and info at the bottom. The grid has its data context set
to the ViewModel property we'll see later.
The game is in a ViewBox
which is great because it'll mean it looks consistent at different sizes. The DragAndDrop
host is a class provided by Apex
that provides efficient and easy drag and drop functionality for cases where we're trying to move elements, not necessarily items from a tree to a list or whatever.
The DragAndDrop
host is a monster to explain in detail, so it is detailed in Appendix 2. Anything within DragAndDropHost
will be eligible
for our simplified drag and drop functionality.
OK, time to lay out the Tableaus, Foundations, and so on, binding each one to the appropriate View Model member.
<!---->
<apexControls:ApexGrid Width="1200" Height="840" Columns="*,*,*,*,*,*,*" Rows="240,600">
<!---->
<solitaireGames:CardStackControl
x:Name="dragStack" Grid.Row="0"
Grid.Column="0" Margin="0,-2000,0,0"
Orientation="Vertical"
FaceDownOffset="10" FaceUpOffset="30"
apexDragAndDrop:DragAndDrop.IsDragSource="False"/>
<!---->
<Border
Grid.Row="0" Grid.Column="0"
Style="{StaticResource StackMarker}" />
<solitaireGames:CardStackControl
Grid.Row="0" Grid.Column="0"
ItemsSource="{Binding Stock}" Cursor="Hand"
Orientation="Horizontal" FaceDownOffset="0"
FaceUpOffset="0"
MouseLeftButtonUp="CardStackControl_MouseLeftButtonUp" />
<!---->
<Border
Grid.Row="0" Grid.Column="1"
Style="{StaticResource StackMarker}" />
<solitaireGames:CardStackControl
x:Name="wasteStack"
Grid.Row="0" Grid.Column="1"
Grid.ColumnSpan="2" ItemsSource="{Binding Waste}"
Orientation="Horizontal" OffsetMode="UseCardValues" />
<!---->
<Border
Grid.Row="0" Grid.Column="3"
Style="{StaticResource StackMarker}" />
<solitaireGames:CardStackControl
Grid.Row="0" Grid.Column="3"
ItemsSource="{Binding Foundation1}"
Orientation="Horizontal" FaceDownOffset="0"
FaceUpOffset="0" />
<Border
Grid.Row="0" Grid.Column="4"
Style="{StaticResource StackMarker}" />
<solitaireGames:CardStackControl
Grid.Row="0" Grid.Column="4"
ItemsSource="{Binding Foundation2}"
Orientation="Horizontal"
FaceDownOffset="0" FaceUpOffset="0" />
<Border
Grid.Row="0" Grid.Column="5"
Style="{StaticResource StackMarker}" />
<solitaireGames:CardStackControl
Grid.Row="0" Grid.Column="5"
ItemsSource="{Binding Foundation3}"
Orientation="Horizontal"
FaceDownOffset="0" FaceUpOffset="0" />
<Border
Grid.Row="0" Grid.Column="6"
Style="{StaticResource StackMarker}" />
<solitaireGames:CardStackControl
Grid.Row="0" Grid.Column="6"
ItemsSource="{Binding Foundation4}"
Orientation="Horizontal" FaceDownOffset="0"
FaceUpOffset="0" />
<!---->
<Border
Grid.Row="1" Grid.Column="0"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="0"
ItemsSource="{Binding Tableau1}"
Orientation="Vertical"
FaceDownOffset="10" FaceUpOffset="30" />
<Border
Grid.Row="1" Grid.Column="1"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Tableau2}"
Orientation="Vertical"
FaceDownOffset="10" FaceUpOffset="30" />
<Border
Grid.Row="1" Grid.Column="2"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="2"
ItemsSource="{Binding Tableau3}"
Orientation="Vertical"
FaceDownOffset="10" FaceUpOffset="30" />
<Border
Grid.Row="1" Grid.Column="3"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="3"
ItemsSource="{Binding Tableau4}"
Orientation="Vertical"
FaceDownOffset="10" FaceUpOffset="30" />
<Border
Grid.Row="1" Grid.Column="4"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="4"
ItemsSource="{Binding Tableau5}"
Orientation="Vertical"
FaceDownOffset="10" FaceUpOffset="30" />
<Border
Grid.Row="1" Grid.Column="5"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="5"
ItemsSource="{Binding Tableau6}"
Orientation="Vertical" FaceDownOffset="10"
FaceUpOffset="30" />
<Border
Grid.Row="1" Grid.Column="6"
Style="{StaticResource RunMarker}" />
<solitaireGames:CardStackControl
Grid.Row="1" Grid.Column="6"
ItemsSource="{Binding Tableau7}"
Orientation="Vertical" FaceDownOffset="10"
FaceUpOffset="30" />
</apexControls:ApexGrid>
</apexDragAndDrop:DragAndDropHost>
</Viewbox>
We're referring to some things here that'll be detailed later, but in a nutshell, we have:
dragStack
: A 'hidden' stack that holds the stack of cards being dragged.
CardStackControl
: An items control that can lay its children out in stacks, with various different ways of offsetting each item. See Appendix 1.
StackMarker
: A resource used to draw a faint white border for a stack.
RunMarker
: A resource used to draw a fading white set of lines for a run.
Ignoring the resources here, what we're really just doing is putting together a few ItemsControl
s and binding them to our ViewModel.
Below the actual game, we put some commands and some info:
<!---->
<apexControls:PaddedGrid
Grid.Row="1" Padding="4"
Columns="Auto,*,Auto,Auto,Auto,*,Auto">
<Button
Style="{StaticResource CasinoButtonStyle}"
Grid.Column="0" Width="120" Content="Deal New Game"
Command="{Binding DealNewGameCommand}" />
<TextBlock Grid.Column="2"
Style="{StaticResource CasinoTextStyle}"
VerticalAlignment="Center">
<Run Text="Score:" />
<Run Text="{Binding Score}" />
</TextBlock>
<TextBlock Grid.Column="3"
Style="{StaticResource CasinoTextStyle}"
VerticalAlignment="Center">
<Run Text="Moves:" />
<Run Text="{Binding Moves}" />
</TextBlock>
<TextBlock Grid.Column="4"
Style="{StaticResource CasinoTextStyle}"
VerticalAlignment="Center">
<Run Text="Time:" />
<Run Text="{Binding ElapsedTime,
Converter={StaticResource
TimeSpanToShortStringConverter}}" />
</TextBlock>
<Button
Style="{StaticResource CasinoButtonStyle}"
Grid.Column="6" Width="120" Content="Go to Casino"
Command="{Binding GoToCasinoCommand}" />
</apexControls:PaddedGrid>
We use a padded grid to lay things out nicely (wpfpaddedgrid.aspx). This is straightforward - we have a deal new game button,
a go to casino button, the score, time, and moves. Again, the resources are described later.
When the game is won, we just overlay the XAML below - this is why we have the IsGameWon
property.
<!---->
<apexControls:ApexGrid
Rows="*,Auto,Auto,Auto,*" Grid.RowSpan="2" Background="#00FFFFFF"
Visibility="{Binding IsGameWon,
Converter={StaticResource
BooleanToVisibilityConverter}}">
<TextBlock
Grid.Row="1" FontSize="34"
FontWeight="SemiBold" Foreground="#99FFFFFF"
HorizontalAlignment="Center" Text="You Win!" />
<TextBlock
Grid.Row="2" FontSize="18" Foreground="#99FFFFFF"
HorizontalAlignment="Center" TextWrapping="Wrap">
<Run Text="You scored" />
<Run Text="{Binding Score}" />
<Run Text="in" />
<Run Text="{Binding Moves}" />
<Run Text="moves!" />
</TextBlock>
<StackPanel
Grid.Row="3" Orientation="Horizontal"
HorizontalAlignment="Center">
<Button
Style="{StaticResource CasinoButtonStyle}" Width="120"
Margin="4" Content="Play Again"
Command="{Binding DealNewGameCommand}" />
<Button
Style="{StaticResource CasinoButtonStyle}" Width="120"
Margin="4" Content="Back to Casino"
Command="{Binding GoToCasinoCommand}" />
</StackPanel>
</apexControls:ApexGrid>
</apexControls:ApexGrid>
</UserControl>
That's it - that's the View for Klondike! The eagle-eyed among you may have noticed we have a few code-behind functions, let's add them.
The constructor will wire in the drag and drop host:
public KlondikeSolitaireView()
{
InitializeComponent();
dragAndDropHost.DragAndDropStart +=
new DragAndDropDelegate(Instance_DragAndDropStart);
dragAndDropHost.DragAndDropContinue +=
new DragAndDropDelegate(Instance_DragAndDropContinue);
dragAndDropHost.DragAndDropEnd +=
new DragAndDropDelegate(Instance_DragAndDropEnd);
}
I'm not going to go into a huge amount of detail about drag and drop right now, but here's the skinny:
DragAndDropStart
: Is called when any UIElement
that has DragAndDrop.IsDraggable
set to true
is dragged.
DragAndDropContinue
: Is called when a dragged element is moved over an element with DragAndDrop.IsDropTarget
set to true
.
DragAndDropEnd
: Is called when a dragged element is released over a drop target.
Why write my own? Well, the plan is that it'll work in Silverlight as well - but this is a topic for another day.
When drag and drop starts, we check the card is OK, and put it and the cards below it into the invisible drag stack - this will be what we draw as the drag adorner.
void Instance_DragAndDropStart(object sender, DragAndDropEventArgs args)
{
PlayingCard card = args.DragData as PlayingCard;
if (card == null || card.IsPlayable == false)
{
args.Allow = false;
return;
}
args.Allow = true;
IList<playingcard> cards = ViewModel.GetCardCollection(card);
draggingCards = new List<playingcard>();
int start = cards.IndexOf(card);
for (int i = start; i < cards.Count; i++)
draggingCards.Add(cards[i]);
dragStack.ItemsSource = draggingCards;
dragStack.UpdateLayout();
args.DragAdorner = new Apex.Adorners.VisualAdorner(dragStack);
ItemsControl sourceStack = args.DragSource as ItemsControl;
foreach (var dragCard in draggingCards)
((ObservableCollection<playingcard>)
sourceStack.ItemsSource).Remove(dragCard);
}
Why the custom adorner? Silverlight is the short answer. When we drag a card, if it is draggable, we drag it and all the cards below it. We hide them in the source
stack by putting them in the drag stack, which is what's drawn by the adorner.
void Instance_DragAndDropContinue(object sender, DragAndDropEventArgs args)
{
args.Allow = true;
}
The continue function is trivial in this case, we're always going to let the operation continue; when we get to DragAndDropEnd
, that's when we'll check all is OK.
Drag and drop end moves cards from the temporary drag stack back to the source and then just lets the View Model take over.
void Instance_DragAndDropEnd(object sender, DragAndDropEventArgs args)
{
ItemsControl sourceStack = args.DragSource as ItemsControl;
foreach (var dragCard in draggingCards)
((ObservableCollection<playingcard>)
((ItemsControl)args.DragSource).ItemsSource).Add(dragCard);
if (args.DropTarget != null)
{
ViewModel.MoveCard(
(ObservableCollection<playingcard>)((ItemsControl)args.DragSource).ItemsSource,
(ObservableCollection<playingcard>)((ItemsControl)args.DropTarget).ItemsSource,
(PlayingCard)args.DragData, false);
}
}
We have a ViewModel dependency property as well, we'll see why this is important when we build the Casino.
private static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel",
typeof(KlondikeSolitaireViewModel), typeof(KlondikeSolitaireView),
new PropertyMetadata(new KlondikeSolitaireViewModel()));
public KlondikeSolitaireViewModel ViewModel
{
get { return (KlondikeSolitaireViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
The last thing in the View code-behind is the temporary storage for the dragged cards, we let a right click that's not on a card call the TryMoveAllCards
... function,
and a left click on the stock stack (i.e., only handled when the stack is empty) call the Turn Stock command (so we can click on the empty stock to turn cards from the waste back over).
private void dragAndDropHost_MouseRightButtonDown(object sender,
MouseButtonEventArgs e)
{
ViewModel.TryMoveAllCardsToAppropriateFoundations();
}
private List<playingcard> draggingCards;
private void CardStackControl_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
ViewModel.TurnStockCommand.DoExecute(null);
}
This is the View done.
Step 4: Resources
In the View created previously, we referenced a resource dictionary, let's now add that dictionary.
Create a folder called Resources in SolitaireGames, add a ResourceDictionary named SolitiareGamesResourceDictionary.xaml.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:apexMVVM="clr-namespace:Apex.MVVM;assembly=Apex"
xmlns:apexCommands="clr-namespace:Apex.Commands;assembly=Apex"
xmlns:apexDragAndDrop="clr-namespace:Apex.DragAndDrop;assembly=Apex"
xmlns:local="clr-namespace:SolitaireGames"
xmlns:apexConverters="clr-namespace:Apex.Converters;assembly=Apex"
xmlns:solitaireGames="clr-namespace:SolitaireGames"
xmlns:klondike="clr-namespace:SolitaireGames.KlondikeSolitaire"
xmlns:spider="clr-namespace:SolitaireGames.SpiderSolitaire">
-->
<apexConverters:BooleanToVisibilityConverter
x:Key="BooleanToVisibilityConverter" />
<solitaireGames:TimeSpanToShortStringConverter
x:Key="TimeSpanToShortStringConverter" />
First we have the usual raft of namespace declarations. Then a couple of converters. Apex.Converters.BooleanToVisibilityConverter
is just
like the standard one - except that you can pass Invert
as its parameter and it'll invert the result.
In the Klondike View, we show a time span, but we want it in a particular format, and for that, we have a converter. It's such a simple class, I'll just drop
it in below (it is called TimeSpanToShortStringConverter
).
using System;
using System.Windows.Data;
namespace SolitaireGames
{
class TimeSpanToShortStringConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
TimeSpan timeSpan = (TimeSpan)value;
if(timeSpan.Hours > 0)
return string.Format("{0:D2}:{1:D2}:{2:D2}",
timeSpan.Hours,
timeSpan.Minutes,
timeSpan.Seconds);
else
return string.Format("{0:D2}:{1:D2}",
timeSpan.Minutes,
timeSpan.Seconds);
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The next converter we have is a playing card to brush converter, this will take a PlayingCard
object and return the appropriate brush. If you look in the sample code,
you'll see that Resources contains a folder called Decks, this contains four deck folders, Classic, Hearts, Large Print, and Seasons.
Each of these folders contains an image with each card type as a name (e.g., S2.png is the 2 of Spades, HA is the ace of Hearts), and a Back.png as a card background image.
These resources were extracted from the Windows 7 Solitaire games and tidied, cropped, etc. in Photoshop (600 odd files, it took a while, thank you PhotoShop batch processing).
The PlayingCardToBrushConverter
will allow us to turn a PlayingCard
into the appropriate brush for it - with the brushes stored in a dictionary so we only
create them as needed. The additional decks functionality was added later so is a bit kludgy, but it works!
Again, it's a simple converter so I'm not going to go into the details:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Globalization;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SolitaireGames
{
public class PlayingCardToBrushConverter : IMultiValueConverter
{
public static void SetDeckFolder(string folderName)
{
brushes.Clear();
deckFolder = folderName;
}
static string deckFolder = "Classic";
static Dictionary<string,> brushes = new Dictionary<string,>();
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
if (values == null || values.Count() != 2)
return null;
CardType cardType = (CardType)values[0];
bool faceDown = (bool)values[1];
string imageSource = string.Empty;
if (faceDown)
imageSource = "Back";
else
imageSource = cardType.ToString();
imageSource =
"pack://application:,,,/SolitaireGames;component/Resources/Decks/" +
deckFolder + "/" + imageSource + ".png";
if (brushes.ContainsKey(imageSource) == false)
brushes.Add(imageSource, new ImageBrush(
new BitmapImage(new Uri(imageSource))));
return brushes[imageSource];
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
In essence, we write the enum value as a string and build a path from some hard coded values.
Back to the dictionary XAML, the most important data template comes next:
<!---->
<DataTemplate DataType="{x:Type solitaireGames:PlayingCard}">
<Border
Width="140" Height="190" Cursor="Hand"
BorderThickness="1" CornerRadius="6"
apexDragAndDrop:DragAndDrop.IsDraggable="True"
apexCommands:ExtendedCommands.RightClickCommand="{Binding RelativeSource={RelativeSource
FindAncestor, AncestorType={x:Type UserControl}}, Path=ViewModel.RightClickCardCommand}"
apexCommands:ExtendedCommands.RightClickCommandParameter="{Binding }"
>
<apexCommands:EventBindings.EventBindings>
<apexCommands:EventBindingCollection>
<apexCommands:EventBinding
EventName="MouseLeftButtonUp"
Command="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}},
Path=ViewModel.LeftClickCardCommand}"
CommandParameter="{Binding}" />
</apexCommands:EventBindingCollection>
</apexCommands:EventBindings.EventBindings>
<Border.Background>
<MultiBinding Converter="{StaticResource PlayingCardToBrushConverter}">
<Binding Path="CardType" />
<Binding Path="IsFaceDown" />
</MultiBinding>
</Border.Background>
Essentially, a playing card is drawn as a border with a brush provided by the converter we just saw. The DragAndDrop.IsDraggable
rears its ugly head here,
making sure that cards can be dragged. The RightClickCommand
is going to assume that somewhere up the visual tree, we have a ViewModel that derives from
CardGameViewModel
. We next have an event binding for the left mouse click.
Event Bindings and Mouse Up
Why not just use apexCommands:ExtendedCommands.LeftClickCommand
rather than the more wordy Apex Event Binding? Well, in this case, we want left clicks
of cards to be registered only when the mouse is released, otherwise it is going to play merry hell with the events used in drag and drop. EventBinding is funky,
it'll route any event to a command, handling datacontexts, etc.
Finally, for the card, we have a kludge - the face images are too white, we need a border, but the back images are fine - so we only draw the border if we're face up.
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFaceDown}" Value="True">
<Setter Property="BorderBrush" Value="#00ffffff" />
</DataTrigger>
<DataTrigger Binding="{Binding IsFaceDown}" Value="False">
<Setter Property="BorderBrush" Value="#ff666666" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</DataTemplate>
The next part of the resource dictionary defines how a CardStackControl
is styled. This is a bit odd as it is essentially passing properties from itself to its layout panel,
which is a CardStackPanel
. CardStack
s are a bit fruity and as they're not critical in understanding how the app works,
they are detailed in Appendix 1. However, here's the XAML:
<!---->
<Style TargetType="{x:Type solitaireGames:CardStackControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type solitaireGames:CardStackControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ItemsControl ItemsSource="{TemplateBinding ItemsSource}"
apexDragAndDrop:DragAndDrop.IsDragSource="True"
apexDragAndDrop:DragAndDrop.IsDropTarget="True"
Background="Transparent">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<solitaireGames:CardStackPanel
FaceDownOffset="{Binding FaceDownOffset, RelativeSource=
{RelativeSource AncestorType={x:Type solitaireGames:CardStackControl}}}"
FaceUpOffset="{Binding FaceUpOffset, RelativeSource=
{RelativeSource AncestorType={x:Type solitaireGames:CardStackControl}}}"
OffsetMode="{Binding OffsetMode, RelativeSource=
{RelativeSource AncestorType={x:Type solitaireGames:CardStackControl}}}"
NValue="{Binding NValue, RelativeSource=
{RelativeSource AncestorType={x:Type solitaireGames:CardStackControl}}}"
Orientation="{Binding Orientation, RelativeSource=
{RelativeSource AncestorType={x:Type solitaireGames:CardStackControl}}}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The next four styles are for consistent looking text, curved borders for stacks and runs, and rounded buttons that look nice on a green baize background.
<!---->
<Style x:Key="CasinoTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="#99FFFFFF" />
<Setter Property="FontSize" Value="16" />
</Style>
<!---->
<Style x:Key="StackMarker" TargetType="Border">
<Setter Property="Padding" Value="10" />
<Setter Property="BorderThickness" Value="6" />
<Setter Property="CornerRadius" Value="15" />
<Setter Property="BorderBrush" Value="#33FFFFFF" />
<Setter Property="Margin" Value="8,10,40,60" />
</Style>
<!---->
<Style x:Key="RunMarker" TargetType="Border">
<Setter Property="Padding" Value="10" />
<Setter Property="BorderThickness" Value="6" />
<Setter Property="CornerRadius" Value="15" />
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#33FFFFFF" Offset="0" />
<GradientStop Color="#00FFFFFF" Offset="0.8" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Margin" Value="8,10,40,40" />
</Style>
<!---->
<Style x:Key="CasinoButtonStyle" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
Padding="4" BorderThickness="2"
CornerRadius="15" BorderBrush="#66FFFFFF"
Background="#11FFFFFF"
Cursor="Hand">
<ContentPresenter
TextBlock.Foreground="#99FFFFFF"
TextBlock.FontWeight="SemiBold"
HorizontalAlignment="Center"
Content="{TemplateBinding Content}"
/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
We only data bind two enums in the whole app - the draw mode of Klondike and the Difficulty of Spider, so we finish the resource dictionary off with data providers for them:
<!---->
<ObjectDataProvider
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}" x:Key="DrawModeValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="klondike:DrawMode" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<!---->
<ObjectDataProvider
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}"
x:Key="DifficultyValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="spider:Difficulty" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
Step 5: Spider Solitaire
If I detail Spider Solitaire here, the article will be too long. The code is in the sample and is strikingly similar to the Klondike code - we have a View Model and
a Ciew, a set of Tableaus, etc. If you can follow the Klondike code, the Spider code is fine. If you are building the project step-by-step with the article, you can
add the SpiderSolitiare folder now and drag in the files from the download at the top of the page.
Step 6: The Casino
The Casino is going to be our home page or hub for all of the solitaire action. It'll hold the View Models for the two games and also some statistics. In fact,
statistics are the next thing we'll work on. Let's create a View Model for some statistics. Add a file called GameStatistics.cs to SolitaireGames:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Apex.MVVM;
namespace SolitaireGames
{
public class GameStatistics : ViewModel
{
private NotifyingProperty GameNameProperty =
new NotifyingProperty("GameName",
typeof(string), default(string));
public string GameName
{
get { return (string)GetValue(GameNameProperty); }
set { SetValue(GameNameProperty, value); }
}
private NotifyingProperty GamesPlayedProperty =
new NotifyingProperty("GamesPlayed",
typeof(int), default(int));
public int GamesPlayed
{
get { return (int)GetValue(GamesPlayedProperty); }
set { SetValue(GamesPlayedProperty, value); }
}
private NotifyingProperty GamesWonProperty =
new NotifyingProperty("GamesWon",
typeof(int), default(int));
public int GamesWon
{
get { return (int)GetValue(GamesWonProperty); }
set { SetValue(GamesWonProperty, value); }
}
private NotifyingProperty GamesLostProperty =
new NotifyingProperty("GamesLost",
typeof(int), default(int));
public int GamesLost
{
get { return (int)GetValue(GamesLostProperty); }
set { SetValue(GamesLostProperty, value); }
}
private NotifyingProperty HighestWinningStreakProperty =
new NotifyingProperty("HighestWinningStreak",
typeof(int), default(int));
public int HighestWinningStreak
{
get { return (int)GetValue(HighestWinningStreakProperty); }
set { SetValue(HighestWinningStreakProperty, value); }
}
private NotifyingProperty HighestLosingStreakProperty =
new NotifyingProperty("HighestLosingStreak",
typeof(int), default(int));
public int HighestLosingStreak
{
get { return (int)GetValue(HighestLosingStreakProperty); }
set { SetValue(HighestLosingStreakProperty, value); }
}
private NotifyingProperty CurrentStreakProperty =
new NotifyingProperty("CurrentStreak",
typeof(int), default(int));
public int CurrentStreak
{
get { return (int)GetValue(CurrentStreakProperty); }
set { SetValue(CurrentStreakProperty, value); }
}
private NotifyingProperty CumulativeScoreProperty =
new NotifyingProperty("CumulativeScore",
typeof(int), default(int));
public int CumulativeScore
{
get { return (int)GetValue(CumulativeScoreProperty); }
set { SetValue(CumulativeScoreProperty, value); }
}
private NotifyingProperty HighestScoreProperty =
new NotifyingProperty("HighestScore",
typeof(int), default(int));
public int HighestScore
{
get { return (int)GetValue(HighestScoreProperty); }
set { SetValue(HighestScoreProperty, value); }
}
private NotifyingProperty AverageScoreProperty =
new NotifyingProperty("AverageScore",
typeof(double), default(double));
public double AverageScore
{
get { return (double)GetValue(AverageScoreProperty); }
set { SetValue(AverageScoreProperty, value); }
}
private NotifyingProperty CumulativeGameTimeProperty =
new NotifyingProperty("CumulativeGameTime",
typeof(double), default(double));
public TimeSpan CumulativeGameTime
{
get { return TimeSpan.FromSeconds(
(double)GetValue(CumulativeGameTimeProperty)); }
set { SetValue(CumulativeGameTimeProperty, value.TotalSeconds); }
}
private NotifyingProperty AverageGameTimeProperty =
new NotifyingProperty("AverageGameTime",
typeof(double), default(double));
public TimeSpan AverageGameTime
{
get { return TimeSpan.FromSeconds(
(double)GetValue(AverageGameTimeProperty)); }
set { SetValue(AverageGameTimeProperty, value.TotalSeconds); }
}
private ViewModelCommand resetCommand;
public ViewModelCommand ResetCommand
{
get { return resetCommand; }
}
The statistics View Model is mostly just a set of properties - but we also need a constructor to wire in the View Model command and the reset command.
Serializing TimeSpans
You may have noticed that although the notifying properties AverageGameTime
and CumulativeGameTime
are exposed as TimeSpan
s, they're stored
and defined as double
s. This is because we will want to serialize this whole object, and TimeSpan
objects do not serialize with XmlSerializer
!
Below is the constructor and reset command:
public GameStatistics()
{
resetCommand = new ViewModelCommand(DoReset, true);
}
private void DoReset()
{
GamesPlayed = 0;
GamesWon = 0;
GamesLost = 0;
HighestWinningStreak = 0;
HighestLosingStreak = 0;
CurrentStreak = 0;
CumulativeScore = 0;
HighestScore = 0;
AverageScore = 0;
CumulativeGameTime = TimeSpan.FromSeconds(0);
AverageGameTime = TimeSpan.FromSeconds(0);
}
The final and most important function is the UpdateStatistics
function - this will update the statistics from a CardGameViewModel
object:
public void UpdateStatistics(CardGameViewModel cardGame)
{
GamesPlayed++;
if (cardGame.IsGameWon)
GamesWon++;
else
GamesLost++;
if (cardGame.IsGameWon)
CurrentStreak = CurrentStreak < 0 ? 1 : CurrentStreak + 1;
else
CurrentStreak = CurrentStreak > 0 ? -1 : CurrentStreak - 1;
if (CurrentStreak > HighestWinningStreak)
HighestWinningStreak = CurrentStreak;
else if (Math.Abs(CurrentStreak) > HighestLosingStreak)
HighestLosingStreak = Math.Abs(CurrentStreak);
if (cardGame.Score > HighestScore)
HighestScore = cardGame.Score;
if (cardGame.IsGameWon)
{
CumulativeScore += cardGame.Score;
AverageScore = CumulativeScore / GamesWon;
}
CumulativeGameTime += cardGame.ElapsedTime;
AverageGameTime =
TimeSpan.FromTicks(CumulativeGameTime.Ticks / (GamesWon + GamesLost));
}
That's the GameStatistics
done. Now let's build the Casino View Model - this is going to be the big one that holds everything else. Add a folder to SolitaireGames
called Casino. Now add a class called CasinoViewModel
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Apex.MVVM;
using SolitaireGames.KlondikeSolitaire;
using System.IO.IsolatedStorage;
using System.IO;
using System.Xml.Serialization;
using SolitaireGames.SpiderSolitaire;
namespace SolitaireGames.Casino
{
public class CasinoViewModel : ViewModel
{
public CasinoViewModel()
{
goToCasinoCommand = new ViewModelCommand(DoGoToCasino, true);
goToKlondikeSolitaireCommand =
new ViewModelCommand(DoGoToKlondikeSolitaire, true);
goToSpiderSolitaireCommand =
new ViewModelCommand(DoGoToSpiderSolitaire, true);
settingsCommand = new ViewModelCommand(DoSettingsCommand, true);
}
private ViewModelCommand goToCasinoCommand;
private ViewModelCommand goToKlondikeSolitaireCommand;
private ViewModelCommand goToSpiderSolitaireCommand;
private ViewModelCommand settingsCommand;
public ViewModelCommand GoToCasinoCommand
{
get { return goToCasinoCommand; }
}
public ViewModelCommand GoToKlondikeSolitaireCommand
{
get { return goToKlondikeSolitaireCommand; }
}
public ViewModelCommand GoToSpiderSolitaireCommand
{
get { return goToSpiderSolitaireCommand; }
}
public ViewModelCommand SettingsCommand
{
get { return settingsCommand; }
}
private void DoGoToCasino()
{
KlondikeSolitaireViewModel.StopTimer();
SpiderSolitaireViewModel.StopTimer();
}
private void DoGoToSpiderSolitaire()
{
if(SpiderSolitaireViewModel.Moves > 0)
SpiderSolitaireViewModel.StartTimer();
}
private void DoGoToKlondikeSolitaire()
{
if(KlondikeSolitaireViewModel.Moves > 0)
KlondikeSolitaireViewModel.StartTimer();
}
private void DoSettingsCommand()
{
}
(Again, I'm describing this class in an order that makes it easier to break down, not in the order it is defined in the actual file). The first thing we have
is four commands - one for each of the places we can navigate to.
When we move from place to place, we start or stop the appropriate game timers, but how do we actually move? Well, as the current screen is a View concern, we let the
View listen out for the commands and handle them as it determines is appropriate - the ViewModel just does the data and logic side of things. So for example,
the DoSettingsCommand
actually does nothing - however, later on, we'll have a View listen for this command and change the current screen when it fires.
Next we have the Klondike and Spider View Models and statistics:
private NotifyingProperty KlondikeSolitaireStatisticsProperty =
new NotifyingProperty("KlondikeSolitaireStatistics", typeof(GameStatistics),
new GameStatistics() { GameName = "Klondike Solitaire" });
public GameStatistics KlondikeSolitaireStatistics
{
get { return (GameStatistics)GetValue(KlondikeSolitaireStatisticsProperty); }
set { SetValue(KlondikeSolitaireStatisticsProperty, value); }
}
private NotifyingProperty SpiderSolitaireStatisticsProperty =
new NotifyingProperty("SpiderSolitaireStatistics", typeof(GameStatistics),
new GameStatistics() { GameName = "Spider Solitaire" });
public GameStatistics SpiderSolitaireStatistics
{
get { return (GameStatistics)GetValue(SpiderSolitaireStatisticsProperty); }
set { SetValue(SpiderSolitaireStatisticsProperty, value); }
}
private NotifyingProperty KlondikeSolitaireViewModelProperty =
new NotifyingProperty("KlondikeSolitaireViewModel",
typeof(KlondikeSolitaireViewModel),
new KlondikeSolitaireViewModel());
public KlondikeSolitaireViewModel KlondikeSolitaireViewModel
{
get { return (KlondikeSolitaireViewModel)GetValue(
KlondikeSolitaireViewModelProperty); }
set { SetValue(KlondikeSolitaireViewModelProperty, value); }
}
private NotifyingProperty SpiderSolitaireViewModelProperty =
new NotifyingProperty("SpiderSolitaireViewModel",
typeof(SpiderSolitaireViewModel),
new SpiderSolitaireViewModel());
public SpiderSolitaireViewModel SpiderSolitaireViewModel
{
get { return (SpiderSolitaireViewModel)GetValue(
SpiderSolitaireViewModelProperty); }
set { SetValue(SpiderSolitaireViewModelProperty, value); }
}
As we can see, CasinoViewModel
holds the View Models for the games.
Nested ViewModels
In terms of Design Patterns, there are various opinions on how things like nested View Models should work. In an extensible design, we might have a list of games
in the casino and use the Managed Extensible Framework to allow us to add games arbitrarily. However, in this case, we're just having the child View Models
as properties - it's getting the job done in this case, but it might be a practice to look over carefully before using it in other projects.
We're going to allow the deck to be chosen via a combo box. You may recall that decks were added at the last minute, so this isn't too clean, but again, it works.
I could have tidied it up, but I wanted to finally get this published:
private NotifyingProperty DeckFolderProperty =
new NotifyingProperty("DeckFolder", typeof(string), "Classic");
public string DeckFolder
{
get { return (string)GetValue(DeckFolderProperty); }
set
{
SetValue(DeckFolderProperty, value);
PlayingCardToBrushConverter.SetDeckFolder(value);
}
}
private List<string> deckFolders =
new List<string>() { "Classic", "Hearts", "Seasons", "Large Print" };
[XmlIgnore]
public List<string> DeckFolders
{
get { return deckFolders; }
}
Now for MVVM experts, you may have not been too comfortable with nested View Models - the next section is even more cheeky:
public void Save()
{
IsolatedStorageFile isoStore =
IsolatedStorageFile.GetStore(IsolatedStorageScope.User |
IsolatedStorageScope.Domain | IsolatedStorageScope.Assembly,
null, null);
using (IsolatedStorageFileStream isoStream =
new IsolatedStorageFileStream("Casino.xml",
FileMode.Create, isoStore))
{
XmlSerializer casinoSerializer =
new XmlSerializer(typeof(CasinoViewModel));
casinoSerializer.Serialize(isoStream, this);
}
}
public static CasinoViewModel Load()
{
IsolatedStorageFile isoStore =
IsolatedStorageFile.GetStore(IsolatedStorageScope.User |
IsolatedStorageScope.Domain | IsolatedStorageScope.Assembly,
null, null);
try
{
using (IsolatedStorageFileStream isoStream =
new IsolatedStorageFileStream("Casino.xml",
FileMode.Open, isoStore))
{
XmlSerializer casinoSerializer =
new XmlSerializer(typeof(CasinoViewModel));
return (CasinoViewModel)casinoSerializer.Deserialize(isoStream);
}
}
catch
{
}
return new CasinoViewModel();
}
Two functions - Save
and Load
. They allow us to persist the whole Casino.
Serialzing ViewModels
MVVM-wise, this shouldn't be done. A View Model is presentation logic - it is the bridge between the View and the Model - it is the Model that should be used to persist data.
However, this gets the job done - it is a small application and we can adapt the pattern to our needs.
Understanding the rules of patterns such as MVVM is very important if you are going to break them like this - you must understand what limitations you are building in. In this case,
the application isn't going to be built on over years with a high cost for customer change requests, etc., so we can use the pattern in the way it works for us - it doesn't need
to be extendible with regards to its data storage mechanism.
As in the note before, you should think very carefully about doing something like this in a 'serious' application because it will cause problems
if the structure of the View Model changes.
Once a Casino has been loaded or created, we need to wire in some events and so on, so let's give it an Initialise
function to do one-off initialization:
public void Initialise()
{
KlondikeSolitaireViewModel.DealNewGameCommand.Executed +=
new CommandEventHandler(KlondikeDealNewGameCommand_Executed);
KlondikeSolitaireViewModel.GameWon +=
new Action(KlondikeSolitaireViewModel_GameWon);
KlondikeSolitaireViewModel.GoToCasinoCommand.Executed +=
new CommandEventHandler(GoToCasinoCommand_Executed);
SpiderSolitaireViewModel.DealNewGameCommand.Executed +=
new CommandEventHandler(SpiderDealNewGameCommand_Executed);
SpiderSolitaireViewModel.GameWon +=
new Action(SpiderSolitaireViewModel_GameWon);
SpiderSolitaireViewModel.GoToCasinoCommand.Executed +=
new CommandEventHandler(GoToCasinoCommand_Executed);
PlayingCardToBrushConverter.SetDeckFolder(DeckFolder);
}
What's going on here? Well, we're listening out for certain commands that are fired by the child View Models.
Apex Commands
An Apex Command fires two events - Executing
before the command is executed, which allows the command to be cancelled, and Executed
after the command is executed.
We can use these events in Views or other View Models to know when ViewModelCommand
s have been fired.
What do we want to listen to these commands for? Well, really just to save (in case the game ends unexpectedly) and update stats:
void KlondikeSolitaireViewModel_GameWon()
{
KlondikeSolitaireStatistics.UpdateStatistics(KlondikeSolitaireViewModel);
Save();
}
void SpiderSolitaireViewModel_GameWon()
{
SpiderSolitaireStatistics.UpdateStatistics(KlondikeSolitaireViewModel);
Save();
}
void KlondikeDealNewGameCommand_Executed(object sender, CommandEventArgs args)
{
if (KlondikeSolitaireViewModel.Moves > 0)
KlondikeSolitaireStatistics.UpdateStatistics(KlondikeSolitaireViewModel);
Save();
}
void SpiderDealNewGameCommand_Executed(object sender, CommandEventArgs args)
{
if (SpiderSolitaireViewModel.Moves > 0)
SpiderSolitaireStatistics.UpdateStatistics(SpiderSolitaireViewModel);
Save();
}
void GoToCasinoCommand_Executed(object sender, CommandEventArgs args)
{
GoToCasinoCommand.DoExecute(null);
}
That's it - the Casino View Model is done. We can now do the casino View. Add a UserControl called CasinoView
to SolitaireGames:
<UserControl x:Class="SolitaireGames.Casino.CasinoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:solitaireGames="clr-namespace:SolitaireGames"
xmlns:local="clr-namespace:SolitaireGames.Casino"
xmlns:apexControls="clr-namespace:Apex.Controls;assembly=Apex"
xmlns:klondikeSolitaire="clr-namespace:SolitaireGames.KlondikeSolitaire"
xmlns:spiderSolitaire="clr-namespace:SolitaireGames.SpiderSolitaire"
xmlns:apexCommands="clr-namespace:Apex.Commands;assembly=Apex"
mc:Ignorable="d"
x:Name="casinoViewModel"
d:DesignHeight="300" d:DesignWidth="300"
DataContext="{Binding CasinoViewModel, ElementName=casinoViewModel}">
-->
<UserControl.Resources>
<ResourceDictionary
Source="/SolitaireGames;component/Resources/
SolitaireGamesResourceDictionary.xaml" />
</UserControl.Resources>
So far so good, just the namespaces, datacontext, and dictionary.
<!---->
<Grid>
<!---->
<Image Source="/SolitaireGames;component/Resources/Backgrounds/Background.jpg"
Stretch="Fill" />
<!---->
<apexControls:PivotControl x:Name="mainPivot" ShowPivotHeadings="False">
Now we have a grid to hold everything, a baize image (which you can get from the sample project), and a pivot control which will hold the games, casino, and settings.
The Pivot Control
Apex contains a control called PivotControl
which can hold a set of PivotItem
s. This is very similar to the pivot control seen in WP7 applications.
If ShowPivotHeadings
is set to true
, we see the headings at the top of the control allowing us to move between items; however, in this app, we use
buttons in other places to move around.
If anyone thinks the PivotControl
is useful, then I will write it up in detail in a separate article.
Now for the first pivot item:
<!---->
<apexControls:PivotItem Title="Klondike">
<klondikeSolitaire:KlondikeSolitaireView
x:Name="klondikeSolitaireView"
ViewModel="{Binding KlondikeSolitaireViewModel}" />
</apexControls:PivotItem>
The first pivot item is a KlondikeSolitaireView
bound to the casino's Klondike Solitaire ViewModel.
<!---->
<apexControls:PivotItem Title="Casino" IsInitialPage="True" >
<!---->
<apexControls:ApexGrid Rows="Auto,Auto,*,Auto">
<!---->
<TextBlock
Grid.Row="0" FontSize="34" HorizontalAlignment="Center"
Foreground="#99FFFFFF" Text="Solitaire" />
<!---->
<apexControls:ApexGrid Grid.Row="1"
Rows="Auto" Columns="*,Auto,Auto,*">
<!---->
<local:StatisticsView
Width="300"
Grid.Column="1" Grid.Row="0"
GameStatistics="{Binding KlondikeSolitaireStatistics}"
apexCommands:ExtendedCommands.LeftClickCommand=
"{Binding GoToKlondikeSolitaireCommand}"
Cursor="Hand" />
<local:StatisticsView
Width="300"
Grid.Column="2" Grid.Row="0"
GameStatistics="{Binding SpiderSolitaireStatistics}"
apexCommands:ExtendedCommands.LeftClickCommand=
"{Binding GoToSpiderSolitaireCommand}"
Cursor="Hand" />
</apexControls:ApexGrid>
<!---->
<apexControls:ApexGrid
Grid.Row="4" Columns="*,Auto,Auto,Auto,*">
<Button
Grid.Column="1"
Style="{StaticResource CasinoButtonStyle}" Width="120"
Margin="4" Content="Play Klondike"
Command="{Binding GoToKlondikeSolitaireCommand}" />
<Button
Grid.Column="2" Style="{StaticResource CasinoButtonStyle}"
Width="120" Margin="4" Content="Settings"
Command="{Binding SettingsCommand}" />
<Button
Grid.Column="3" Style="{StaticResource CasinoButtonStyle}"
Width="120" Margin="4" Content="Play Spider"
Command="{Binding GoToSpiderSolitaireCommand}" />
</apexControls:ApexGrid>
</apexControls:ApexGrid>
</apexControls:PivotItem>
The next pivot item is the casino itself. It shows a title, two StatisticsView
s (which we'll see later), and some commands to go from one page to another.
Now we have Spider Solitaire:
<!---->
<apexControls:PivotItem Title="Spider">
<spiderSolitaire:SpiderSolitaireView
x:Name="spiderSolitaireView"
ViewModel="{Binding SpiderSolitaireViewModel}" />
</apexControls:PivotItem>
Again fairly straightforward. The last pivot item is a settings page, where we bind to things like the Klondike draw mode or casino deck style:
<!---->
<apexControls:PivotItem Title="Settings">
<!---->
<apexControls:ApexGrid Rows="Auto,Auto,*,Auto">
<!---->
<TextBlock
Grid.Row="0" FontSize="34" HorizontalAlignment="Center"
Foreground="#99FFFFFF" Text="Settings" />
<!---->
<apexControls:PaddedGrid Grid.Row="1" Columns="*,*"
Width="500" Padding="4"
Rows="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Text="Solitaire" FontWeight="Bold"
HorizontalAlignment="Center"
Style="{StaticResource CasinoTextStyle}" />
<TextBlock
Grid.Row="1" Grid.Column="0" Text="Deck Style"
HorizontalAlignment="Right"
Style="{StaticResource CasinoTextStyle}" />
<ComboBox
Grid.Row="1" Grid.Column="1"
SelectedItem="{Binding DeckFolder}"
ItemsSource="{Binding DeckFolders}" />
<TextBlock
Grid.Row="2" Grid.Column="1"
Style="{StaticResource CasinoTextStyle}"
FontSize="12" Text="Requires Restart" />
<TextBlock
Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Text="Klondike Solitaire" FontWeight="Bold"
HorizontalAlignment="Center"
Style="{StaticResource CasinoTextStyle}" />
<TextBlock
Grid.Row="4" Grid.Column="0" Text="Draw Mode"
HorizontalAlignment="Right"
Style="{StaticResource CasinoTextStyle}" />
<ComboBox
Grid.Row="4" Grid.Column="1"
SelectedItem="{Binding KlondikeSolitaireViewModel.DrawMode}"
ItemsSource="{Binding Source={StaticResource DrawModeValues}}" />
<TextBlock
Grid.Row="5" Grid.Column="0" Text="Statistics"
HorizontalAlignment="Right"
Style="{StaticResource CasinoTextStyle}" />
<Button
Grid.Row="5" Grid.Column="1"
Content="Reset" HorizontalAlignment="Left"
Width="80" Style="{StaticResource CasinoButtonStyle}"
Command="{Binding KlondikeSolitaireStatistics.ResetCommand}" />
<TextBlock
Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2"
Text="Spider Solitaire" FontWeight="Bold"
HorizontalAlignment="Center"
Style="{StaticResource CasinoTextStyle}" />
<TextBlock
Grid.Row="7" Grid.Column="0" Text="Difficulty"
HorizontalAlignment="Right"
Style="{StaticResource CasinoTextStyle}" />
<ComboBox
Grid.Row="7" Grid.Column="1"
SelectedItem="{Binding SpiderSolitaireViewModel.Difficulty}"
ItemsSource="{Binding Source={StaticResource DifficultyValues}}" />
<TextBlock
Grid.Row="8" Grid.Column="0" Text="Statistics"
HorizontalAlignment="Right"
Style="{StaticResource CasinoTextStyle}" />
<Button
Grid.Row="8" Grid.Column="1"
Content="Reset" HorizontalAlignment="Left"
Width="80" Style="{StaticResource CasinoButtonStyle}"
Command="{Binding SpiderSolitaireStatistics.ResetCommand}" />
</apexControls:PaddedGrid>
<Button
Grid.Row="3" Style="{StaticResource CasinoButtonStyle}"
Width="120" Margin="4" Content="Casino"
Command="{Binding GoToCasinoCommand}" />
</apexControls:ApexGrid>
</apexControls:PivotItem>
</apexControls:PivotControl>
</Grid>
</UserControl>
And that's it for the View. Now for the code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SolitaireGames.Casino
{
public partial class CasinoView : UserControl
{
public CasinoView()
{
InitializeComponent();
}
void GoToKlondikeSolitaireCommand_Executed(object sender,
Apex.MVVM.CommandEventArgs args)
{
mainPivot.SelectedPivotItem = mainPivot.PivotItems[0];
}
void GoToSpiderSolitaireCommsand_Executed(object sender,
Apex.MVVM.CommandEventArgs args)
{
mainPivot.SelectedPivotItem = mainPivot.PivotItems[2];
}
void GoToCasinoCommand_Executed(object sender,
Apex.MVVM.CommandEventArgs args)
{
mainPivot.SelectedPivotItem = mainPivot.PivotItems[1];
}
private void SettingsCommand_Executed(object sender,
Apex.MVVM.CommandEventArgs args)
{
mainPivot.SelectedPivotItem = mainPivot.PivotItems[3];
}
private static readonly DependencyProperty CasinoViewModelProperty =
DependencyProperty.Register("CasinoViewModel",
typeof(CasinoViewModel), typeof(CasinoView),
new PropertyMetadata(null,
new PropertyChangedCallback(OnCasinoViewModelChanged)));
public CasinoViewModel CasinoViewModel
{
get { return (CasinoViewModel)GetValue(CasinoViewModelProperty); }
set { SetValue(CasinoViewModelProperty, value); }
}
private static void OnCasinoViewModelChanged(DependencyObject o,
DependencyPropertyChangedEventArgs args)
{
CasinoView me = o as CasinoView;
me.CasinoViewModel.GoToCasinoCommand.Executed +=
new Apex.MVVM.CommandEventHandler(me.GoToCasinoCommand_Executed);
me.CasinoViewModel.GoToSpiderSolitaireCommand.Executed +=
new Apex.MVVM.CommandEventHandler(
me.GoToSpiderSolitaireCommsand_Executed);
me.CasinoViewModel.GoToKlondikeSolitaireCommand.Executed +=
new Apex.MVVM.CommandEventHandler(
me.GoToKlondikeSolitaireCommand_Executed);
me.CasinoViewModel.SettingsCommand.Executed +=
new Apex.MVVM.CommandEventHandler(me.SettingsCommand_Executed);
}
}
}
There's very little to this - the CasinoViewModel
is a dependency property. When it is set, we listen for the 'GoTo...' commands, and when they're fired, just move the pivot
control selection to the correct item.
The last thing for the casino is the StatisticsView
, which is little usercontrol that shows the details of a StatisticsViewModel
. Add a UserControl called
StatisticsView
to the Casino folder, here's the code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SolitaireGames.Casino
{
public partial class StatisticsView : UserControl
{
public StatisticsView()
{
InitializeComponent();
}
private static readonly DependencyProperty GameStatisticsProperty =
DependencyProperty.Register("GameStatistics",
typeof(GameStatistics), typeof(StatisticsView),
new PropertyMetadata(null));
public GameStatistics GameStatistics
{
get { return (GameStatistics)GetValue(GameStatisticsProperty); }
set { SetValue(GameStatisticsProperty, value); }
}
}
}
Just one dependency property - the statistics View Model (which allows us to bind the ViewModel to the CasinoViewModel
child items). And finally the XAML:
<UserControl x:Class="SolitaireGames.Casino.StatisticsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:apexControls="clr-namespace:Apex.Controls;assembly=Apex"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
x:Name="statisticsView">
-->
<UserControl.Resources>
<ResourceDictionary
Source="/SolitaireGames;component/Resources/
SolitaireGamesResourceDictionary.xaml" />
</UserControl.Resources>
<Border Padding="10" Margin="10"
BorderBrush="#99FFFFFF" BorderThickness="6" CornerRadius="15"
DataContext="{Binding GameStatistics, ElementName=statisticsView}">
<apexControls:PaddedGrid
Rows="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
Columns="3*,*"
TextBlock.Foreground="#99FFFFFF"
TextBlock.FontSize="13">
-->
<TextBlock
Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="2" HorizontalAlignment="Center"
Text="{Binding GameName}" FontSize="24" />
-->
<TextBlock Grid.Row="1" Grid.Column="0" Text="Games Played" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding GamesPlayed}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Games Won" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding GamesWon}" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="Games Lost" />
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding GamesLost}" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="Current Streak" />
<TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding CurrentStreak}" />
<TextBlock Grid.Row="5" Grid.Column="0" Text="Highest Winning Streak" />
<TextBlock Grid.Row="5" Grid.Column="1" Text="{Binding HighestWinningStreak}" />
<TextBlock Grid.Row="6" Grid.Column="0" Text="Highest Losing Streak" />
<TextBlock Grid.Row="6" Grid.Column="1" Text="{Binding HighestLosingStreak}" />
<TextBlock Grid.Row="7" Grid.Column="0" Text="Highest Score" />
<TextBlock Grid.Row="7" Grid.Column="1" Text="{Binding HighestScore}" />
<TextBlock Grid.Row="8" Grid.Column="0" Text="Cumulative Score" />
<TextBlock Grid.Row="8" Grid.Column="1" Text="{Binding CumulativeScore}" />
<TextBlock Grid.Row="9" Grid.Column="0" Text="Average Score" />
<TextBlock Grid.Row="9" Grid.Column="1" Text="{Binding AverageScore}" />
<TextBlock Grid.Row="10" Grid.Column="0" Text="Cumulative Time" />
<TextBlock Grid.Row="10" Grid.Column="1"
Text="{Binding CumulativeGameTime,
Converter={StaticResource TimeSpanToShortStringConverter}}" />
<TextBlock Grid.Row="11" Grid.Column="0" Text="Average Time" />
<TextBlock Grid.Row="11" Grid.Column="1"
Text="{Binding AverageGameTime, Converter={StaticResource
TimeSpanToShortStringConverter}}" />
</apexControls:PaddedGrid>
</Border>
</UserControl>
As we can see, this control just shows game stats in a rounded border.
Step 7: The Solitaire Application
Now go to the Solitaire project and open MainWindow.xaml, make sure you have a reference to Apex and SolitaireGames, and add a CasinoView
as below:
<Window x:Class="Solitaire.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:casino="clr-namespace:SolitaireGames.Casino;assembly=SolitaireGames"
Title="Solitaire" Height="400" Width="650"
Icon="/Solitaire;component/Solitaire.ico">
-->
<casino:CasinoView x:Name="casinoView" />
</Window>
And the code-behind just loads the ViewModel (and saves it when we close):
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
casinoView.CasinoViewModel = CasinoViewModel.Load();
casinoView.CasinoViewModel.Initialise();
Closing +=
new System.ComponentModel.CancelEventHandler(MainWindow_Closing);
}
void MainWindow_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
casinoView.CasinoViewModel.Save();
}
}
The final thing we've added is an icon called Solitaire.ico, which is provided in the download.
Hit F5 and enjoy!
Appendix 1: The Card Stack Control
The card stack control is a rather complicated beast so I've included it as an appendix. It is an items control that has a set of properties that are passed
to a card stack panel for layout. Here is the card stack panel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
namespace SolitaireGames
{
public enum OffsetMode
{
EveryCard,
EveryNthCard,
TopNCards,
BottomNCards,
UseCardValues
}
public class CardStackPanel : StackPanel
{
private readonly Size infiniteSpace =
new Size(Double.MaxValue, Double.MaxValue);
protected override Size MeasureOverride(Size constraint)
{
Size resultSize = new Size(0, 0);
List<size> offsets = CalculateOffsets();
double totalX = (from o in offsets select o.Width).Sum();
double totalY = (from o in offsets select o.Height).Sum();
foreach(UIElement child in Children)
{
child.Measure(infiniteSpace);
}
if (LastChild != null)
{
totalX += LastChild.DesiredSize.Width;
totalY += LastChild.DesiredSize.Height;
}
return new Size(totalX, totalY);
}
protected override Size ArrangeOverride(Size finalSize)
{
double x = 0, y = 0;
int n = 0;
int total = Children.Count;
List<size> offsets = CalculateOffsets();
if ((ActualWidth > 0 && finalSize.Width > ActualWidth) ||
(ActualHeight > 0 && finalSize.Height > ActualHeight))
{
double overrunX = finalSize.Width - ActualWidth;
double overrunY = finalSize.Height - ActualHeight;
double dx = overrunX / offsets.Count;
double dy = overrunY / offsets.Count;
for (int i = 0; i < offsets.Count; i++)
{
offsets[i] = new Size(Math.Max(0, offsets[i].Width - dx),
Math.Max(0, offsets[i].Height - dy));
}
finalSize.Width -= overrunX;
finalSize.Height -= overrunY;
}
foreach (UIElement child in Children)
{
PlayingCard card = ((FrameworkElement)child).DataContext as PlayingCard;
if (card == null)
continue;
child.Arrange(new Rect(x, y, child.DesiredSize.Width,
child.DesiredSize.Height));
x += offsets[n].Width;
y += offsets[n].Height;
n++;
}
return finalSize;
}
private List<size> CalculateOffsets()
{
List<size> offsets = new List<size>();
int n = 0;
int total = Children.Count;
foreach (UIElement child in Children)
{
PlayingCard card = ((FrameworkElement)child).DataContext as PlayingCard;
if (card == null)
continue;
double faceDownOffset = 0;
double faceUpOffset = 0;
switch (OffsetMode)
{
case OffsetMode.EveryCard:
faceDownOffset = FaceDownOffset;
faceUpOffset = FaceUpOffset;
break;
case OffsetMode.EveryNthCard:
if (((n + 1) % NValue) == 0)
{
faceDownOffset = FaceDownOffset;
faceUpOffset = FaceUpOffset;
}
break;
case OffsetMode.TopNCards:
if ((total - NValue) <= n && n < total)
{
faceDownOffset = FaceDownOffset;
faceUpOffset = FaceUpOffset;
}
break;
case OffsetMode.BottomNCards:
if (n < NValue)
{
faceDownOffset = FaceDownOffset;
faceUpOffset = FaceUpOffset;
}
break;
case SolitaireGames.OffsetMode.UseCardValues:
faceDownOffset = card.FaceDownOffset;
faceUpOffset = card.FaceUpOffset;
break;
default:
break;
}
n++;
Size offset = new Size(0, 0);
switch (Orientation)
{
case Orientation.Horizontal:
offset.Width = card.IsFaceDown ? faceDownOffset : faceUpOffset;
break;
case Orientation.Vertical:
offset.Height = card.IsFaceDown ? faceDownOffset : faceUpOffset;
break;
default:
break;
}
offsets.Add(offset);
}
return offsets;
}
private UIElement LastChild
{
get { return Children.Count > 0 ? Children[Children.Count - 1] : null; }
}
private static readonly DependencyProperty FaceDownOffsetProperty =
DependencyProperty.Register("FaceDownOffset",
typeof(double), typeof(CardStackPanel),
new FrameworkPropertyMetadata(5.0,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public double FaceDownOffset
{
get { return (double)GetValue(FaceDownOffsetProperty); }
set { SetValue(FaceDownOffsetProperty, value); }
}
private static readonly DependencyProperty FaceUpOffsetProperty =
DependencyProperty.Register("FaceUpOffset",
typeof(double), typeof(CardStackPanel),
new FrameworkPropertyMetadata(5.0,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public double FaceUpOffset
{
get { return (double)GetValue(FaceUpOffsetProperty); }
set { SetValue(FaceUpOffsetProperty, value); }
}
private static readonly DependencyProperty OffsetModeProperty =
DependencyProperty.Register("OffsetMode",
typeof(OffsetMode), typeof(CardStackPanel),
new PropertyMetadata(OffsetMode.EveryCard));
public OffsetMode OffsetMode
{
get { return (OffsetMode)GetValue(OffsetModeProperty); }
set { SetValue(OffsetModeProperty, value); }
}
private static readonly DependencyProperty NValueProperty =
DependencyProperty.Register("NValue",
typeof(int), typeof(CardStackPanel),
new PropertyMetadata(1));
public int NValue
{
get { return (int)GetValue(NValueProperty); }
set { SetValue(NValueProperty, value); }
}
}
}
The CardStackControl
simply duplicates the key properties and passes them to its child CardStackPanel
via the template
in the SolitaireGamesResourcesDictionary.xaml file. The idea behind the card stack panel is that it can do just about anything we need for laying out cards.
It also will make sure stacks are 'squeezed' if needs be into the available space.
Appendix 2: Drag and Drop
Drag and drop in Apex is a full topic on its own; if anyone feels the approach used would be useful in other applications, then please let me know and I'll write it up as a full article.
Solitaire and Augmented Reality
I got a very interesting message from Rupam Das (http://www.codeproject.com/script/Membership/View.aspx?mid=8114613), who has made an augmented reality version of the project! In his application, you use your webcam to play the game physically by picking up cards with gestures. Other gestures, like thumbs up and thumbs down are bound to commands in the game – here’s a screenshot:
The project is called GesCard. Check out the YouTube video with the link here https://www.youtube.com/watch?v=wCOjuPdBooI. Thanks to rupam for getting in touch and sharing this very cool code!
Final Thoughts
There are many things that could be done to improve this application:
- Animation
- Sounds
- Different scoring models
These are just a few, but it came to the point where it was getting too big to write up as a sensible article so I upload it now; feel free to play with it,
change it, and suggest improvements.
There may well be bugs as I've transcribed the whole project from one solution to another in the order I've written the article to make sure everything
is detailed properly, please let me know if you find any!
I've written this entire article with a broken ulna, so apologies for spelling mistakes or other oddities!
Apex
Apex in its most basic form is written up at apex.aspx but the latest version is available
at: http://apex.codeplex.com/.