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)
2 Aug 2010 1  
Illustrates UI modelling in WPF, leveraging templates.

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!

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