Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Power of Templates in Control Development Within Windows Presentation Foundation

0.00/5 (No votes)
31 Oct 2011 1  
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 visual aspect of the card as per the sign and the value of the card.

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 CardTypeToImageConverters to obtain sign image and used VerticalFlipStyle to flip bottom symbols vertically to get a card similar to Figure 1 below.

275869/CardImage.png

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.

275869/CardDeck.png

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.

/// <summary> 
/// Interaction logic for MainWindow.xaml 
/// </summary> 
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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here