Introduction
WPF is very powerful framework in terms of flexibility in defining visual characteristics of the UI. The visual designing aspect of WPF allows for developer - designer collaboration (and separation of concerns). In this tutorial, I will take a practical example of a card game and illustrate how to model a playing card in WPF, and define templates for defining the visual aspect of the card as per the sign and the value of the card.
Evaluating development options
I wanted to model a playing card so it is highly reusable and theme-able so other developers and designers can consume it easily. At a basic level, a card must contain its value and type and be selectable. There were several development options available in WPF I could opt for:
- Take an existing WPF control and theme it to represent a card, and use attached properties to set its value and card type
- Define a custom control and code in all the above functionality, and define its theme to represent a card
- Combine the above two methods - derive from an existing WPF control and define templates and additional properties on it
The first approach does not allow me to declare discoverable properties of a card on an existing control. While attached properties are great, they are a bit hard to discover. A card's properties are very prominent and frequently used, so I opted for a more out of the box - self contained control.
The second approach requires me to develop a custom control from scratch. So this approach is ruled out as well.
I prefer the third approach where I can leverage some capabilities of an existing WPF control and define my own custom properties on top to provide an out of the box experience. And since this will be a custom control, I can define an out of the box - default theme for this control. Since a playing card can be selected - deselected, I chose to derive from the ToggleButton
control that provides with selection capabilities.
Template support
Playing cards support multiple themes depending on the type and value of the card. This information is captured within the CardTemplate
class, a collection of which is defined as a CardTemplateCollection
.
public sealed class CardTemplate
{
public ControlTemplate Template { get; set; }
public CardValue Value { get; set; }
}
public sealed class CardTemplateCollection : Collection<CardTemplate>
{
}
Deriving from Collection<T>
allows for native XAML serialization, and we can enclose objects easily within a collection tag.
The PlayingCard
class contains CardTemplateCollection
as its CardTemplates
dependency property.
private static DependencyProperty CardTemplatesProperty =
DependencyProperty.Register( "CardTemplates",
typeof(CardTemplateCollection),
typeof(PlayingCard),
new PropertyMetadata(new CardTemplateCollection(),
UpdateTemplate));
Notice how a new CardTemplateCollection
is set as a default property value. This is so because whenever we derive from Collection<T>
and leverage XAML serialization capability, we need a valid object before it is assigned in XAML.
PlayingCard
also contains a method UpdateTemplate
which picks the right template for the card depending upon the card value.
private void UpdateTemplate()
{
CardTemplateCollection templates = CardTemplates;
ControlTemplate selectedTemplate = null;
if (templates != null)
{
foreach (CardTemplate template in templates)
{
if (template.Value == CardValue)
{
selectedTemplate = template.Template;
break;
}
}
}
Template = selectedTemplate;
}
Following is the complete listing of the PlayingCard
class:
public class PlayingCard : ToggleButton
{
private static DependencyProperty CardTypeProperty =
DependencyProperty.Register(
"CardType",
typeof(CardType),
typeof(PlayingCard),
new PropertyMetadata(CardType.Club));
private static DependencyProperty CardValueProperty =
DependencyProperty.Register(
"CardValue",
typeof(CardValue),
typeof(PlayingCard),
new PropertyMetadata(CardValue.Two, UpdateTemplate));
private static DependencyProperty CardTemplatesProperty =
DependencyProperty.Register(
"CardTemplates",
typeof(CardTemplateCollection),
typeof(PlayingCard),
new PropertyMetadata(new CardTemplateCollection(),
UpdateTemplate));
static PlayingCard()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(PlayingCard),
new FrameworkPropertyMetadata(typeof(PlayingCard)));
}
public CardType CardType
{
get { return (CardType)GetValue(CardTypeProperty); }
set { SetValue(CardTypeProperty, value); }
}
public CardValue CardValue
{
get { return (CardValue)GetValue(CardValueProperty); }
set { SetValue(CardValueProperty, value); }
}
public CardTemplateCollection CardTemplates
{
get { return (CardTemplateCollection)GetValue(CardTemplatesProperty); }
set { SetValue(CardTemplatesProperty, value); }
}
private void UpdateTemplate()
{
CardTemplateCollection templates = CardTemplates;
ControlTemplate selectedTemplate = null;
if (templates != null)
{
foreach (CardTemplate template in templates)
{
if (template.Value == CardValue)
{
selectedTemplate = template.Template;
break;
}
}
}
Template = selectedTemplate;
}
private static void UpdateTemplate(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
PlayingCard card = d as PlayingCard;
card.UpdateTemplate();
}
}
Notice we call the UpdateTemplate
method whenever the CardValue
or CardTemplates
properties are changed.
The next step is to define and populate all the templates within the default theme of the PlayingCard
control. But before we embark on that, there are two pieces of visual information we would need in order to theme the card right:
- The sign of the card - Depending on the type of the card, we need the right symbol to appear in the template. This is achieved by
CardTypeToImageConverter
. Basically, we define this converter with a property of type CardImageTypeCollection
which contains objects of type CardImageType
. CardImageType
just contains the CardType
enum (that denotes the type of card: Club, Diamond, Heart, Spade) and a string URI of the related image.
public sealed class CardTypeImage
{
public CardType CardType { get; set; }
public string ImageSource { get; set; }
}
public sealed class CardTypeImageCollection : Collection<CardTypeImage>
{
}
public sealed class CardTypeToImageConverter : IValueConverter
{
public CardTypeToImageConverter()
{
ImageCollection = new CardTypeImageCollection();
}
public CardTypeImageCollection ImageCollection { get; set; }
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
PlayingCard card = value as PlayingCard;
if ((card != null) && (ImageCollection != null))
{
foreach (CardTypeImage cardTypeImage in ImageCollection)
{
if (card.CardType == cardTypeImage.CardType)
{
return cardTypeImage.ImageSource;
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
As can be seen, this converter takes in PlayingCard
as input, and returns the URI of the symbol image from searching its ImageCollection
.
Image of the character if the card represents a character - If PlayingCard
is of value Ace, King, Queen, and Jack, we need access to the right character image in the template. This is delegated to CardTypeToCharacterImageConverter
as follows:
public sealed class CardCharacterImage
{
public CardType CardType { get; set; }
public CardValue CardValue { get; set; }
public string ImageSource { get; set; }
}
public sealed class CardCharacterImageCollection : Collection<CardCharacterImage>
{
}
public sealed class CardTypeToCharacterImageConverter : IValueConverter
{
public CardTypeToCharacterImageConverter()
{
ImageCollection = new CardCharacterImageCollection();
}
public CardCharacterImageCollection ImageCollection { get; set; }
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
PlayingCard card = value as PlayingCard;
if ((card != null) && (IsCharacterCard(card)))
{
foreach (CardCharacterImage cardCharacterImage in ImageCollection)
{
if ((card.CardValue == cardCharacterImage.CardValue) &&
(card.CardType == cardCharacterImage.CardType))
{
return cardCharacterImage.ImageSource;
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private bool IsCharacterCard(PlayingCard card)
{
switch (card.CardValue)
{
case CardValue.Two:
case CardValue.Three:
case CardValue.Four:
case CardValue.Five:
case CardValue.Six:
case CardValue.Seven:
case CardValue.Eight:
case CardValue.Nine:
case CardValue.Ten:
return false;
break;
default:
return true;
break;
}
}
}
Hence depending on the type and value of the character card, this converter returns the right image URI for the card.
Now we have all we need to define our templates for the PlayingCard
control.
First, let's define the CardTypeToImageConverter
shared converter in the resource dictionary that the themes will use:
<Converters:CardTypeToImageConverter x:Key="CardTypeToImageConverter">
<Converters:CardTypeToImageConverter.ImageCollection>
<Converters:CardTypeImageCollection>
<Converters:CardTypeImage CardType="Club"
ImageSource="/CardDeckSample;component/Resources/Club.png"/>
<Converters:CardTypeImage CardType="Diamond"
ImageSource="/CardDeckSample;component/Resources/Diamond.png"/>
<Converters:CardTypeImage CardType="Heart"
ImageSource="/CardDeckSample;component/Resources/Heart.png"/>
<Converters:CardTypeImage CardType="Spade"
ImageSource="/CardDeckSample;component/Resources/Spade.png"/>
</Converters:CardTypeImageCollection>
</Converters:CardTypeToImageConverter.ImageCollection>
</Converters:CardTypeToImageConverter>
Then, let's define the CardTypeToCharacterImageConverter
shared converter:
<Converters:CardTypeToCharacterImageConverter x:Key="CardTypeToCharacterImageConverter">
<Converters:CardTypeToCharacterImageConverter.ImageCollection>
<Converters:CardCharacterImage CardType="Club" CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceClub.png"/>
<Converters:CardCharacterImage CardType="Diamond" CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceDiamond.png"/>
<Converters:CardCharacterImage CardType="Heart" CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceHeart.png"/>
<Converters:CardCharacterImage CardType="Spade" CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceSpade.png"/>
<Converters:CardCharacterImage CardType="Club" CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackClub.png"/>
<Converters:CardCharacterImage CardType="Diamond" CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackDiamond.png"/>
<Converters:CardCharacterImage CardType="Heart" CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackHeart.png"/>
<Converters:CardCharacterImage CardType="Spade" CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackSpade.png"/>
<Converters:CardCharacterImage CardType="Club" CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingClub.png"/>
<Converters:CardCharacterImage CardType="Diamond" CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingDiamond.png"/>
<Converters:CardCharacterImage CardType="Heart" CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingHeart.png"/>
<Converters:CardCharacterImage CardType="Spade" CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingSpade.png"/>
<Converters:CardCharacterImage CardType="Club" CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenClub.png"/>
<Converters:CardCharacterImage CardType="Diamond" CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenDiamond.png"/>
<Converters:CardCharacterImage CardType="Heart" CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenHeart.png"/>
<Converters:CardCharacterImage CardType="Spade" CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenSpade.png"/>
</Converters:CardTypeToCharacterImageConverter.ImageCollection>
</Converters:CardTypeToCharacterImageConverter>
We also need to flip the card signs in the bottom half of the cards vertically, so we capture this in a common style called VerticalFlipStyle
:
<Style TargetType="{x:Type FrameworkElement}" x:Key="VerticalFlipStyle">
<Setter Property="RenderTransform">
<Setter.Value>
<TransformGroup>
<RotateTransform Angle="180"/>
</TransformGroup>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
</Style>
Great! Now we have all the tools that we can make use of in our card templates. So, let's define the templates for each of the possible card values and character cards, starting with the character card template:
<ControlTemplate TargetType="{x:Type local:PlayingCard}" x:Key="CharacterCardTemplate">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Image Source="{Binding Converter={StaticResource
CardTypeToCharacterImageConverter},
RelativeSource={RelativeSource TemplatedParent}}"/>
</Border>
</ControlTemplate>
You can see, in the Source
attribute of the Image
used in the template, we use CardTypeToCharacterImageConverter
(declared as the shared converter above) and pass in a templated parent (PlayingCard
) as the relative source.
Now for each of the values, we declare templates. I will not cover the entire template list here (you can check them out in Generic.xaml in the Themes folder of the source code), but I will mention the template for a card of value "2".
<ControlTemplate TargetType="{x:Type local:PlayingCard}"
x:Key="TwoTemplate">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="32"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="2"
FontSize="26.667"/>
<Image Source="{Binding Converter={StaticResource
CardTypeToImageConverter},
RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="1"
Grid.Column="0"/>
<Image
Source="{Binding Converter={StaticResource CardTypeToImageConverter},
RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="3"
Grid.Column="2"
Style="{StaticResource VerticalFlipStyle}"/>
<TextBlock Grid.Row="4"
Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="2"
FontSize="26.667"
Style="{StaticResource VerticalFlipStyle}"/>
<Grid Grid.Row="2"
Grid.Column="1"
Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Image Source="{Binding Converter={StaticResource
CardTypeToImageConverter},
RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="0"
Height="32"
Width="32"/>
<Image Source="{Binding Converter={StaticResource
CardTypeToImageConverter},
RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="1"
Height="32"
Width="32"
Style="{StaticResource VerticalFlipStyle}"/>
</Grid>
</Grid>
</Border>
</ControlTemplate>
As you can see, we have used CardTypeToImageConverter
to obtain the sign image, and used VerticalFlipStyle
to flip bottom symbols vertically to get a card similar to Figure 1 below.
Figure 1 - Templated spade card of value 2
Finally, we declare the default style and template of PlayingCard
, as below:
<Style TargetType="{x:Type local:PlayingCard}">
<Setter Property="Width" Value="300"/>
<Setter Property="Height" Value="450"/>
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CardTemplates">
<Setter.Value>
<local:CardTemplateCollection>
<local:CardTemplate
Template="{StaticResource CharacterCardTemplate}" Value="Ace"/>
<local:CardTemplate
Template="{StaticResource CharacterCardTemplate}" Value="King"/>
<local:CardTemplate
Template="{StaticResource CharacterCardTemplate}" Value="Queen"/>
<local:CardTemplate
Template="{StaticResource CharacterCardTemplate}" Value="Jack"/>
<local:CardTemplate
Template="{StaticResource TwoTemplate}" Value="Two"/>
<local:CardTemplate
Template="{StaticResource ThreeTemplate}" Value="Three"/>
<local:CardTemplate
Template="{StaticResource FourTemplate}" Value="Four"/>
<local:CardTemplate
Template="{StaticResource FiveTemplate}" Value="Five"/>
<local:CardTemplate
Template="{StaticResource SixTemplate}" Value="Six"/>
<local:CardTemplate
Template="{StaticResource SevenTemplate}" Value="Seven"/>
<local:CardTemplate
Template="{StaticResource EightTemplate}" Value="Eight"/>
<local:CardTemplate
Template="{StaticResource NineTemplate}" Value="Nine"/>
<local:CardTemplate
Template="{StaticResource TenTemplate}" Value="Ten"/>
</local:CardTemplateCollection>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="BorderBrush" Value="#FF0C1A89"/>
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1"
MappingMode="RelativeToBoundingBox" StartPoint="0.5,0">
<GradientStop Color="#FFF1EDED" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="CardType" Value="Diamond">
<Setter Property="Foreground" Value="#FFd40000"/>
</Trigger>
<Trigger Property="CardType" Value="Heart">
<Setter Property="Foreground" Value="#FFd40000"/>
</Trigger>
</Style.Triggers>
</Style>
Notice all the templates added as CardTemplate
values. The UpdateTemplate
method will assign the right template based on the card value. We also define the selected visual state of the PlayingCard
using triggers and render a gradient background. We also use triggers to define the foreground color of red if the card is of type Diamond or Heart. That is all we need to get our PlayingCard
working. For demonstration purposes, I built a complete deck of cards as a card fan and rendered it in a circular panel (credit: I took this panel from the color swatch sample that ships with Microsoft Expression Blend). Also, I bring the selected card to the front by setting its Z-Index.
Figure 2 - Complete card deck with 5 of diamond selected (notice the selection background gradient)
Following is the code-behind for MainWindow
that renders the card deck:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
foreach (CardType type in Enum.GetValues(typeof(CardType)))
{
foreach (CardValue value in Enum.GetValues(typeof(CardValue)))
{
PlayingCard card = new PlayingCard();
card.CardType = type;
card.CardValue = value;
Deck.Children.Add(card);
}
}
Deck.AddHandler(ToggleButton.CheckedEvent,
new RoutedEventHandler(OnCardSelected));
}
private void OnCardSelected(object sender, RoutedEventArgs args)
{
if (_selectedCard != null)
{
_selectedCard.IsChecked = false;
Canvas.SetZIndex(_selectedCard, 0);
}
_selectedCard = args.OriginalSource as PlayingCard;
if (_selectedCard != null)
{
_selectedCard.IsChecked = true;
Canvas.SetZIndex(_selectedCard, 1);
}
}
private PlayingCard _selectedCard;
}
Last words
I hope I was able to provide some insight into the power and simplicity of modeling UI from this example. WPF is a very rich and flexible architecture that allows us to perform wonders with things like templates and styles. It could also be fun at the same time, and so a very fatal addiction! If you have any comments or suggestions, please do not hesitate to leave a note. I also have my blogs at http://akaila.serveblog.net and http://ashishkaila.blogspot.com. Thanks!