span.sup { vertical-align:text-top; }

Wicked Code

Craft Custom Controls for Silverlight 2

Jeff Prosise

Code download available at:WickedCode2008_08.exe(585 KB)

This article discusses:

  • The WPF control model
  • Creating control templates
  • Deriving controls
  • Adding events
This article uses the following technologies:
Silverlight 2

This article is based on the Beta 2 version of Silverlight 2. All information herein is subject to change.

Contents

Step 1: Create a New Silverlight Project
Step 2: Derive from Control (or ContentControl)
Step 3: Create a Control Template
Step 4: Create a Default Control Template
Step 5: Add Template Bindings
Step 6: Replace TextBlock with ContentPresenter
Step 7: Add a Click Event
Step 8: Add Visual States
The Completed Control

One of the many features that distinguishes Silverlight™ 2 from Silverlight 1.0 is support for controls. Silverlight 2 features a rich and robust control model that is the basis for the controls included in the platform and for third-party control packages. You can also use this control model to build controls of your own. But for developers unfamiliar with the Windows® Presentation Foundation (WPF) control model, building that first Silverlight custom control can be a daunting experience. At the time of this writing—just before the release of Silverlight 2 Beta 2—there was precious little documentation available, and a quick Web search turned up few tutorials to show the way. While I'm on that topic, it's a good time to note that although I am using Beta 2, there will likely be further changes before the final release.

When learning to write custom controls for a new platform, I often begin by trying to duplicate some of the built-in controls: buttons, listboxes, and so on. They may seem simple on the outside, but controls such as these invariably expose key features of the control model and test one's knowledge of the same. Plus, you can't build a super-duper-multicolor-multithreaded-all-in-one-do-it-all widget control if you can't first build a simple push button.

The best way to learn about Silverlight 2 custom controls is to build one—step by step, piece by piece. You not only learn about the parts that make up a control, but how the parts fit together. The following tutorial describes how to build a SimpleButton control that duplicates important aspects of the built-in Button control's look and behavior and provides a helpful first look at control development, Silverlight style.

Step 1: Create a New Silverlight Project

The first step in creating a custom control is to fire up Visual Studio® 2008 (make sure you've installed the Silverlight add-in for Visual Studio so you can create Silverlight projects) and create a project. Normally you'd create a Silverlight class library project so the control can be compiled into its own assembly and added as a reference to projects that will use it. I'll take a slightly different path and create a Silverlight application project so the control can be built and used from within the same project. Therefore, let's begin by creating a new Silverlight application project named SimpleButton­Demo, as shown in Figure 1. Answer "yes" when Visual Studio offers to create a Web project to go with the Silverlight project.

fig01.gif

Figure 1 Creating the SimpleButtonDemo Project

Step 2: Derive from Control (or ContentControl)

The next step is to add a C# class representing the control. At a minimum, the control class should derive from the Silverlight System.Windows.Controls.Control class in order to inherit basic control functionality. However, it can also derive from Control derivatives such as ContentControl and ItemsControl. Many of the built-in controls derive directly or indirectly from ContentControl, which adds a Content property that allows the control's content—for example, the content on the face of a push button—to be customized. The ListBox control, in contrast, derives from ItemsControl, which implements the basic behavior of a control that presents a collection of items to the user. We'll derive from ContentControl since we're implementing a button.

Use the Visual Studio Add New Item command to add a new C# class to the SimpleButtonDemo project. Name the file Simple­Button.cs. Then open SimpleButton.cs and modify the Simple­Button class so that it derives from ContentControl:

namespace SimpleButtonDemo
{
    public class SimpleButton : ContentControl
    {
    }
}

At this point, you've implemented a bare-bones custom control—one that can be instantiated with a declaration in a XAML document. To demonstrate, add the following statement to Page.xaml:

<custom:SimpleButton />

In order for Silverlight to make sense of this declaration, you also need to add the following attribute to Page.xaml's root User­Control element:

xmlns:custom="clr-namespace:SimpleButtonDemo; assembly=SimpleButtonDemo"

As you can see here, clr-namespace identifies the namespace in which the SimpleButton class is defined, and assembly identifies the assembly containing the control. In this example, the control assembly and the application assembly are one and the same. If Simple­Button were implemented in a separate assembly named MyControls.dll, you'd set assembly equal to "MyControls" instead. The code in Figure 2 shows the contents of Page.xaml after these modifications are made. Incidentally, you don't have to use custom as the prefix for custom controls; you could just as easily use foo or your company name instead.

Figure 2 Page.xaml

<UserControl x:Class="SimpleButtonDemo.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo"
    Width="400" Height="300">

    <Grid x:Name="LayoutRoot" Background="White">
      <custom:SimpleButton />
    </Grid>

</UserControl>

You can see what your efforts have wrought so far by launching either of the test pages (SimpleButtonDemoTestPage.aspx or Simple­ButtonDemoTestPage.html) within the Simple­ButtonDemo_Web project that Visual Studio added to the solution. Figure 3 shows how SimpleButtonDemoTestPage.html looks in the browser. Granted, there's not much to write home about just yet, but that will change in the next step.

fig03.gif

Figure 3 Presenting the SimpleButton Control (Click the image for a larger view)

Step 3: Create a Control Template

The reason you found yourself staring at a blank browser window in the previous step is that even though SimpleButton was instantiated, it didn't render a UI. You can remedy that by modifying the SimpleButton declaration in Page.xaml to include a control template. The code in Figure 4 shows the modified control declaration.

Figure 4 Modified Control Declaration

<custom:SimpleButton>
  <custom:SimpleButton.Template>
    <ControlTemplate>
      <Grid x:Name="RootElement">
        <Rectangle x:Name="BodyElement" Width="200" Height="100"
          Fill="Lavender" Stroke="Purple" RadiusX="16" RadiusY="16" />
        <TextBlock Text="Click Me" HorizontalAlignment="Center"
          VerticalAlignment="Center" />
      </Grid>
    </ControlTemplate>
  </custom:SimpleButton.Template>
</custom:SimpleButton>

The declaration now initializes the control's Template property, which defines the control's visual tree, to include a Rectangle and a TextBlock positioned inside a 1-row, 1-column Grid. Launch SimpleButtonDemoTestPage.html in your browser again and the output will look very different (see Figure 5). SimpleButton now has a face!

fig05.gif

Figure 5 The SimpleButton Control (Click the image for a larger view)

Step 4: Create a Default Control Template

It's unreasonable to require developers using your control to define their own control templates. A custom control should have a default template so even a simple declaration like the one in Figure 2 will display something on the page. Providing a default template doesn't preclude someone from overriding it with a template like the one in Figure 4, but it does make your control a lot friendlier by removing the requirement that a template be provided.

The mechanism used to define a default template for a custom control is borrowed from WPF. You begin by adding a file named Generic.xaml to the control project. (Yes, it has to be named Generic.xaml. Case doesn't matter, but the name itself does.) Then you define a style in Generic.xaml that uses a property setter to assign a value to the control's Template property. The Silverlight runtime automatically looks for Generic.xaml in the control assembly (where it is embedded as a resource) and applies the style to control instances. In addition to defining a default template, the style also can assign default values to other control properties such as Width and Height.

To see for yourself, use the Visual Studio Add New Item command to add a text file named Generic.xaml to the SimpleButtonDemo project. Then replace the contents of Generic.xaml with the code in Figure 6. Generic.xaml now includes an unnamed style that is applied to all instances of SimpleButton (note the TargetType attribute). That style includes a default value for SimpleButton's Template property that is identical to the Template value explicitly assigned to the control in Figure 5.

Figure 6 Generic.xaml

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid x:Name="RootElement">
            <Rectangle x:Name="BodyElement" Width="200" Height="100"
              Fill="Lavender" Stroke="Purple" RadiusX="16" RadiusY="16" />
            <TextBlock Text="Click Me" HorizontalAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

With a default template in place, go back to SimpleButton.cs and add the following statement to the class constructor:

this.DefaultStyleKey = typeof(SimpleButton);

Then open Page.xaml and modify the control declaration so that it looks again like this:

<custom:SimpleButton />

Launch the test page in your browser and the control should look exactly as it did before. But this time, obtaining that look was much simpler.

Step 5: Add Template Bindings

One problem with SimpleButton as it exists right now is that property values assigned to the control aren't honored by the control template. In other words, if the control were declared as follows, it would still have a width of 200 and height of 100 because these values are hardcoded into the control template:

<custom:SimpleButton Width="250" Height="150" />

One of the most important features of Silverlight 2 from a control developer's perspective is template binding. Template bindings allow property values assigned to a control to flow down into the control template and are declared in XAML using the {TemplateBinding} markup extension. Rather than define the Width and Height properties of the Rectangle that forms the body of Simple­Button using hardcoded values like this:

Width="200" Height="100"

You should define them this way instead:

Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"

Now the width and height assigned to the control will also be the width and height assigned to the Rectangle.

Figure 7 shows a revised version of Generic.xaml that assigns default values to the Width, Height, and Background properties inherited from the base class, and that uses template bindings to reference these property values in the control template.

Figure 7 Generic.xaml Revised

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Width" Value="200" />
    <Setter Property="Height" Value="100" />
    <Setter Property="Background" Value="Lavender" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid x:Name="RootElement">
            <Rectangle x:Name="BodyElement"
              Width="{TemplateBinding Width}"
              Height="{TemplateBinding Height}"
              Fill="{TemplateBinding Background}"
              Stroke="Purple" RadiusX="16" RadiusY="16" />
            <TextBlock Text="Click Me"
              HorizontalAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

Test the modified control template by editing the control declaration in Page.xaml as follows:

<custom:SimpleButton Width="250" Height="150" Background="Yellow" />

The output is illustrated in Figure 8. The TemplateBindings are a huge, important step in the right direction because now you have instances of SimpleButton honoring the property values that you assign to them.

fig08.gif

Figure 8 The Improved SimpleButton Control

Step 6: Replace TextBlock with ContentPresenter

The fact that SimpleButton derives from ContentControl means it has a Content property that developers can use to customize the content on the face of the button—at least in theory. The built-in Button control can be customized with the XAML in Figure 9 to produce the look shown in Figure 10.

Figure 9 Customizing the Button Control

<Button Width="250" Height="150">
  <Button.Content>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <Ellipse Width="75" Height="75" Margin="10">
        <Ellipse.Fill>
          <RadialGradientBrush GradientOrigin="0.25,0.25">
            <GradientStop Offset="0.25" Color="White" />
            <GradientStop Offset="1.0" Color="Red" />
          </RadialGradientBrush>
        </Ellipse.Fill>
      </Ellipse>
      <TextBlock Text="Click Me" VerticalAlignment="Center" />
    </StackPanel>
  </Button.Content>
</Button>

fig10.gif

Figure 10 Button with Customized Content

But try the same with SimpleButton and you'll quickly find that the content is still simple text. In fact, you can't even set Content="Test" to change the button text because the control template currently contains a hardcoded TextBlock with hardcoded text.

You can fix this deficiency by replacing the TextBlock in Simple­Button's default control template with a ContentPresenter, as shown in Figure 11. A TextBlock can only render text, but a ContentPresenter can render any XAML assigned to the control's Content property. With these changes in place, the XAML SimpleButton declaration in Figure 12 produces the output in Figure 13. SimpleButton now supports two levels of customization. Its entire visual tree can be redefined with a custom template, or just its content can be redefined using the Content property. Moreover, you can change the button text with a simple Content attribute. SimpleButton is beginning to behave more and more like a real button control.

Figure 11 Generic.xaml Once Again

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Width" Value="200" />
    <Setter Property="Height" Value="100" />
    <Setter Property="Background" Value="Lavender" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid x:Name="RootElement">
            <Rectangle x:Name="BodyElement"
              Width="{TemplateBinding Width}"
              Height="{TemplateBinding Height}"
              Fill="{TemplateBinding Background}"
              Stroke="Purple" RadiusX="16" RadiusY="16" />
            <ContentPresenter Content="{TemplateBinding Content}"
              HorizontalAlignment="Center" VerticalAlignment="Center"
              FontSize="{TemplateBinding FontSize}" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

Figure 12 SimpleButton with Custom Content

<custom:SimpleButton Width="250" Height="150">
  <custom:SimpleButton.Content>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <Ellipse Width="75" Height="75" Margin="10">
        <Ellipse.Fill>
          <RadialGradientBrush GradientOrigin="0.25,0.25">
            <GradientStop Offset="0.25" Color="White" />
            <GradientStop Offset="1.0" Color="Red" />
          </RadialGradientBrush>
        </Ellipse.Fill>
      </Ellipse>
      <TextBlock Text="Click Me" VerticalAlignment="Center" />
    </StackPanel>
  </custom:SimpleButton.Content>
</custom:SimpleButton>

fig13.gif

Figure 13 SimpleButton with Customized Content

Step 7: Add a Click Event

The next step in bringing SimpleButton to life is to have it fire Click events. Implementing events in Silverlight controls is generally no different than implementing events in ordinary Microsoft® .NET Framework classes: you simply declare the events in the control class and then write code to fire them.

One aspect of event firing that is slightly different in Silverlight, however, is the fact that Silverlight supports routed events. In WPF, event routing has a richer connotation in that events can travel up and down the visual tree; in Silverlight, events only travel up, an action known as "bubbling." Routed events are defined by the RoutedEventHandler delegate, and routed event handlers receive a RoutedEventArgs object that contains a Source property identifying the object that fired the event (which will be different from the sender parameter passed to the event handler if the event was originally fired by an object deeper in the visual tree). The built-in Button control's Click event is a routed event, so SimpleButton's should be, too.

Figure 14 shows the modifications you must make to the control class in SimpleButton.cs to fire routed Click events. Simple­Button's constructor now registers a handler for MouseLeftButtonUp events. The handler fires a Click event provided there is at least one registered listener. Traditional button controls only fire Click events if button-up events are preceded by button-down events. That logic was omitted from SimpleButton to keep the source code as simple as possible.

Figure 14 SimpleButton with Support for Click Events

public class SimpleButton : ContentControl
{
    public event RoutedEventHandler Click;

    public SimpleButton()
    {
        this.DefaultStyleKey = typeof(SimpleButton);
        this.MouseLeftButtonUp += new MouseButtonEventHandler
            (SimpleButton_MouseLeftButtonUp);
    }

    void SimpleButton_MouseLeftButtonUp(object sender,
        MouseButtonEventArgs e)
    {
        if (Click != null)
            Click(this, new RoutedEventArgs());
    }
}

To test Click events, you can modify the control declaration in Page.xaml as follows:

<custom:SimpleButton Content="Click Me" Click="OnClick" />

Then add the event handler shown next to Page.xaml.cs. Clicking the SimpleButton should now produce an alert box containing the word "Click!"—proof positive that the event was fired and handled correctly:

protected void OnClick(Object sender, RoutedEventArgs e)
{
    System.Windows.Browser.HtmlPage.Window.Alert("Click!");
}

Step 8: Add Visual States

Two key components of Silverlight controls are visual states and visual state transitions. Visual states define a control's appearance in various states: how it looks when it's pressed, when the mouse enters it, when it's disabled, and so on. Visual state transitions define how a control transitions from one visual state to another: for example, from the "normal" state to the "pressed" state.

Silverlight 2 Beta 2 introduced a new component known as the Visual State Manager (VSM) to simplify states and state transitions and to facilitate better tool support. With the VSM, you use VisualState objects encapsulating Storyboards to define states, and Visual­Transition objects to define transitions. Then you use the Visual­StateManager class's static GoToState method to transition the control to the specified state in response to user stimuli.

Figure 15 shows how to modify Generic.xaml to leverage the VSM. SimpleButton defines two states: Normal and MouseOver. These are declared with <vsm:VisualState> elements. The Normal state doesn't require a Storyboard because it represents the control in its default state. The MouseOver state uses a ColorAnimation to change the control's background color—actually, the Fill property of the rectangle representing the body of the control—to pink. The ColorAnimation's Duration property is 0 because the length of the animation is driven by the Duration property of the corresponding VisualTransition. Note the xmlns:vsm attribute added to the <ResourceDictionary> element to enable declarative use of VisualStateManager.

Figure 15 The Completed Generic.xaml

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo"
  xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Width" Value="200" />
    <Setter Property="Height" Value="100" />
    <Setter Property="Background" Value="Lavender" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid>
            <vsm:VisualStateManager.VisualStateGroups>
              <vsm:VisualStateGroup x:Name="CommonStates">
                <vsm:VisualStateGroup.Transitions>
                  <vsm:VisualTransition To="Normal" Duration="0:0:0.2"/>
                  <vsm:VisualTransition To="MouseOver" Duration="0:0:0.2"/>
                </vsm:VisualStateGroup.Transitions>
                <vsm:VisualState x:Name="Normal" />
                <vsm:VisualState x:Name="MouseOver">
                  <Storyboard>
                    <ColorAnimation Storyboard.TargetName="BodyElement"
Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)"
To="Pink" Duration="0" />
                  </Storyboard>
                </vsm:VisualState>
              </vsm:VisualStateGroup>
            </vsm:VisualStateManager.VisualStateGroups>
            <Rectangle x:Name="BodyElement"
              Width="{TemplateBinding Width}"
              Height="{TemplateBinding Height}"
              Fill="{TemplateBinding Background}"
              Stroke="Purple" RadiusX="16" RadiusY="16" />
            <ContentPresenter Content="{TemplateBinding Content}"
              HorizontalAlignment="Center" VerticalAlignment="Center"
              FontSize="{TemplateBinding FontSize}" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

Figure 16 shows the completed SimpleButton class, now modified to use the states and transitions declared in Generic.xaml. The class itself is also decorated with [TemplateVisualState] attributes indicating which visual states the control supports. (These attributes aren't required, but they do enable tools such as Expression Blend™ to provide a richer design-time experience.) The class constructor wires MouseEnter and MouseLeave events to a pair of handlers, and the handlers transition the control state utilizing VisualStateManager.GoToState. Now the control turns pink when the mouse enters and goes back to its original color when the mouse leaves.

Figure 16 The Completed SimpleButton Class

[TemplateVisualState(Name = "Normal", GroupName = "GroupCommon")]
[TemplateVisualState(Name = "StateMouseOver", GroupName = "GroupCommon")]
public class SimpleButton : ContentControl
{
    public event RoutedEventHandler Click;

    public SimpleButton()
    {
        DefaultStyleKey = typeof(SimpleButton);
        this.MouseLeftButtonUp +=
            new MouseButtonEventHandler(SimpleButton_MouseLeftButtonUp);
        this.MouseEnter +=
            new MouseEventHandler(SimpleButton_MouseEnter);
        this.MouseLeave +=
            new MouseEventHandler(SimpleButton_MouseLeave);
    }

    void SimpleButton_MouseLeftButtonUp(object sender,
        MouseButtonEventArgs e)
    {
        if (Click != null)
            Click(this, new RoutedEventArgs());
    }

    void SimpleButton_MouseEnter(object sender, MouseEventArgs e)
    {
        VisualStateManager.GoToState(this, "MouseOver", true);
    }

    void SimpleButton_MouseLeave(object sender, MouseEventArgs e)
    {
        VisualStateManager.GoToState(this, "Normal", true);
    }
}

The Completed Control

The finished source code for SimpleButton is included with the download that accompanies this article. You could refine the control even further by implementing additional properties and state animations and perhaps sprucing up the UI with a glassed appearance. Nonetheless, SimpleButton contains all the key elements of a Silverlight control, and if you can embrace what you've read here, you should have no problem building controls of your own.

Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET. He's also cofounder of Wintellect (wintellect.com), a software consulting and education firm that specializes in the .NET Framework. Have a comment on this column? Contact Jeff at wicked@microsoft.com.