Background
WPF is a very powerful framework in terms of flexibility in defining the 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 a 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 existing control. While attached properties are great, they are a bit hard to discover. 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 box experience. And since this will be a custom control, I can define out of the box – default theme for this control. Since a playing card can be selected – deselected, I choose to derive from ToggleButton
control that provides with selection capabilities.
Template Support
Playing card supports multiple themes depending on the type and value of the card. This information is captured within CardTemplate
class, a collection of which is defined as 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 one can enclose objects easily within 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 on 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 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 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 type of card: Club, Diamond, Heart, Spade) and the 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 UARI 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 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>
Now, 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 card signs in the bottom half of 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 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 Source
attribute of Image used in the template, we use CardTypeToCharacterImageConverter
(declared as shared converter above) and pass in the templated parent (PlayingCard
) as 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 list 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 both CardTypeToImageConverter
s to obtain 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 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 selected visual state of the PlayingCard
using triggers and render a gradient background. We also use triggers to define foreground color of red if 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 render it in a circular panel (Credit: I took this panel from 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 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);
}
}
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 one 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. Thanks!