Dial It Up a Notch

Create a Pure XAML Three-way Switch

CoverStory

LANGUAGES: XAML

TECHNOLOGIES: WPF

 

Dial It Up a Notch

Create a Pure XAML Three-way Switch

By Matthew Hess

If my boss had come to me a year ago and said, Hey Matthew, we need a three-way switch like the mode selector on an old stereo, I would have replied, Sure thing, let me see what I can get. Then I would have headed out onto the Internet to find and buy a third-party control that fit the bill.

But last June when my boss really did come and say we needed a three-way switch, I didn t say, Let me see what I can get. I said, Let me see what I can make. What s changed? Very simply: Windows Presentation Foundation (WPF).

In my view, WPF is a game-changing technology that makes custom control development feasible for regular developers like you and me. In the past, I had always considered building one s own controls to be a highly risky endeavor. One could waste months building widgets that didn t work as well as off-the-shelf models. But WPF changes all that. In this article, I ll take you step-by-step through building a custom WPF control that simulates a three-way switch. The result is fewer than one hundred lines of standalone XAML that is fully self-contained, needs no procedural code-behind, and provides a fully functional, animated control. WPF is a wide and deep topic. My hope is that by looking at one custom control in detail I can give you a taste of what s possible with this new technology.

To begin, let s take a look at the result. Figure 1 shows three three-way switches on an XAML page (for fun, I ve set these up to represent the selection for choosing ice cream). You can see in the figure many of the key elements of the three-way switches. For example, the selected item is indicated in two ways: by the font weight and color of the text, as well as by the position of the indicator on the selector dial. What you can t see from the example is additional control features, such as mouseover and click behaviors. In this version of the control, when you mouse over a possible selection, the text becomes bold. And when you click on a selection, the indicator dial immediately changes to point at the new selection.


Figure 1: Three three-way switches on an XAML page.

A big part of the challenge in building custom controls has always been supporting these kinds of behaviors. For a control to behave properly, you need to spend time thinking about low-level plumbing like mouse and keyboard input, enter and focus events, and painting. But WPF frees us from these worries with the concept of ControlTemplates. A ControlTemplate represents the visual presentation of a control separate from the behavior of a control. In WPF, all controls have a template. For example, if you place a WPF Button on an XAML page, at runtime, the Button will be rendered with some template. If you happen to be using a Windows Classic theme, the template will be something named a ClassicBorderDecorator. If you re using the default Windows XP theme, the template will be something named ButtonChrome. You can check out the MSDN Button ControlTemplate Example at msdn2.microsoft.com/en-us/library/ms753328.aspx to see how you can build a template to make a Button render in your own way.

In this article, however, we re going to go a bit beyond simply changing the aesthetics of a control. We re going to take an existing control that encapsulates the behavior we want and give it a control template that seems to turn it into a completely new control. So, thinking about the three-way switch, let s ask the question: what out-of-the-box control encapsulates the behavior we desire? My first thought, and maybe yours, too, was that a three-way switch is really just a fancy set of RadioButton controls. However, it turns out that using a series of RadioButtons presents a problem because we want to apply our template to the container, that is, the group of RadioButtons, as well as to the individual items. So what we really need is some sort of list container control. What is the basic list container control? The ListBox, of course. If you think about it, while the three-way switch doesn t look like a ListBox, it does behave exactly like a ListBox control containing three items set to single-selection mode. So that s what we re going to base our control on. What this project really boils down to is this: make a control template that targets a ListBox and makes it look like a three-way switch.

Step 1: Designing the Dial

Before we delve into our control template, we can begin by doing a little basic XAML design of the dial. I m going to make this control a fixed size. Using a fixed size isn t as flexible as designing a sizeable control, but it is much simpler and it makes positioning the controls much easier (more on that later). Let s begin with a simple XAML Canvas object on which to put our control:

Now, let s add a circle to the left side of our Canvas for the actual switch dial. This is done with an Ellipse object:

      Stroke="Silver"

      Fill="Gray"

      Width="50"

      Height="50"

      Canvas.Left="5"

      Canvas.Top="10" />

Next, let s overlay a thick line on top of the circle to form the indicator of the switch. I ve used rounded end caps to give it the look I want. And because we re using a fixed-size layout, I can use explicit X-Y coordinates to center the line right over the dial ellipse. Note that the line is drawn straight up and down. This will come into play later when we write the triggers to rotate the dial:

      StrokeThickness="8"

      Stroke="Black"

      StrokeStartLineCap="Round"

      StrokeEndLineCap="Round"

      X1="30" Y1="14" X2="30" Y2="56" />

Lastly, we need to draw the little white dot on the end of the indicator. This is an interesting drawing problem, and there are several ways you might think to handle it. A key consideration is that we re going to need to rotate this dot around the center point of the dial. To make the rotation easy, I chose to render this dot as a line of exactly the same length and position as the indicator. However, the indicator dot line is set to be dashed with a StrokeDashArray and StrokeDashCap that makes it so we only get a single oval dot exactly where we want it:

      StrokeThickness="6"

      Stroke="White"

      StrokeStartLineCap="Round"

      StrokeDashCap="Round"

      StrokeEndLineCap="Round"

      StrokeDashArray=".3, 10"

      X1="30" Y1="14" X2="30" Y2="56" />

Now we have a Canvas with an Ellipse and two lines on it. If you were to place this XAML snippet on an XAML page with a white background, you d see something like the dial shown in Figure 2.


Figure 2: The dial by itself.

Step 2: Building the ListBox ControlTemplate

This is perhaps the key step to this entire project. Here s where we take the little dial we designed, place it into a ControlTemplate, and make it display list items. We begin with an XAML snippet that names our ControlTemplate and targets it to a ListBox. I ve placed our Canvas here, as well:

      TargetType="{x:Type ListBox}">

     

     

      

Now we re going to place one more visual element on our Canvas: a StackPanel. As you may know, the StackPanel is a container control that allows us to arrange a series of items so they flow. In this template, it plays a critical role because the StackPanel is set with the property IsItemsHost = True . The IsItemsHost property is specific to ControlTemplates that target ItemsControls, such as a ListBox because it tells the control where the contained items are supposed to go. In other words, this StackPanel is where our list of three selectable items is going to be rendered. I ve nudged the StackPanel over to the right to make room for the switch dial, and I ve also set it so the list items will be stacked vertically on top of each other and aligned to the left:

      Name="itemsPanel"

      Orientation="Vertical"

      VerticalAlignment="Center"

      HorizontalAlignment="Left"

      IsItemsHost="True"

      Canvas.Left="50"

      Canvas.Top="7"/>

At this point, you could actually apply this ControlTemplate to a ListBox and it would work. In other words, you could do something like:

     

     

     

If you did, you d see something like the example shown in Figure 3. The items in the ListBox would display and be selectable, but the indicator dial would not rotate and the items would have the default styling depending on your Windows theme.


Figure 3: Functional, but incomplete, template.

Step 3: Animating the ListBox ControlTemplate

Now comes the fun: animating the dial. At first you might wonder how this can be done without some procedural code-behind. The answer is: Triggers. In XAML, Triggers behave very much like event sinks. There are several different kinds of Triggers in WPF, but for these control template Triggers, we want to identify a property we care about and a value we care about. We then can write a Trigger that will fire when that property changes to that value. Within the Trigger, we can set other properties as we see fit. In this case, the property I want to target is the SelectedIndex property of the ListBox. The values I will target are 0, 1, and 2, because this is a three-way switch. And the properties I want to set are the RenderTransform properties of the indicator lines, which will allow us to rotate them.

Before we actually write the Triggers, it will be helpful to define some RotateTransform objects as resources of the ControlTemplate. This creates slightly more efficient code and simplifies our Trigger syntax quite a bit. So, within the ControlTemplate tags, we ll add a ControlTemplate.Resources section that contains three RotateTransform objects: one for a 45 rotation (position 0), one for a 90 rotation (position 1), and one for a 135 rotation (position 2):

      

            CenterX="30"

            CenterY="35"

            Angle="45" />

      

            CenterX="30"

            CenterY="35"

            Angle="90" />

      

            CenterX="30"

            CenterY="35"

            Angle="135" />

Now we re ready for the Triggers themselves. Rather than belabor the discussion, let me simply show you the code, which, after a little inspection, is fairly self-explanatory. Here is one of the three Triggers we need:

      

            Property="RenderTransform"

            Value="{StaticResource rotate45}" />

      

            Property="RenderTransform"

            Value="{StaticResource rotate45}" />

This Trigger will rotate 45 both the indicator line and the indicatorDot line when the SelectedIndex of the ListBox is zero. In the final code, we actually have two more Triggers much like this: one for SelectedIndex=1 and one for SelectedIndex=2, which rotate the lines by 90 and 135 , respectively.

At this point our main ControlTemplate is complete. It s a good idea at this point to gather all our code snippets together in one place so you can see what the template looks like in its totality (see Listing One).

Step 4: The ListItem Template

With the ControlTemplate for the ListBox in hand, our work is almost done but not quite. Whenever you are designing a control template for an ItemsControl like a ListBox, you can also design a template for the items contained in the control. This actually turns out to be quite important, because if you don t explicitly template the contained items, you are likely to get unintended visual effects. For example, when an item in a ListBox is selected, the background color of the item (under Windows Classic) is dark blue, as you can see in Figure 3. That is definitely not what we want. In our case, we want the background of our selected item to stay the same, and for the font to become bold and yellow, among other things. So, to take control of the presentation of the list items as well as the container, we need a ControlTtemplate for them.

This ControlTemplate is quite a bit simpler than the one for the ListBox itself, so I ll simply present it whole and then discuss it (see Figure 4).

      TargetType="{x:Type ListBoxItem}">

     

            BorderThickness="0"

            Padding="3"

            Background="Black" >

            

                  Text="{TemplateBinding ContentControl.Content}"

                  Foreground="White" />

     

      

            

                  

                        Property="TextBlock.FontWeight"

                        Value="Bold" />

            

            

                  

                        Property="TextBlock.Foreground"

                        Value="Yellow" />

                  

                        Property="TextBlock.FontWeight"

                        Value="Bold" />

            

     

Figure 4: The ListBoxItem ControlTemplate.

The visual layout of the item template is fairly simple: a Border control with a TextBlock within it. The Border allows us to set some padding around the text. The TextBlock is where the text of the ListItem is actually displayed. How does the TextBlock know to display the text of the ListItem? This is achieved through the magical incantation: Text= {TemplateBinding ContentControl.Content} . Lastly, the ItemTemplate includes a few Triggers that handle the MouseOver and Selected properties to change the font color and weight, appropriately. Now when you mouse over a list item, the font turns bold; when a list item is selected, the font turns bold and yellow.

Step 5: Using Styles to Apply the Templates

At this point, you could apply these two templates to our ListBoxes and ListBoxItems, directly. A sample ListBox might look something like that shown in Figure 5.

      

             Template="{StaticResource threeWaySwitchItemTemplate}"/>

      

             Template="{StaticResource threeWaySwitchItemTemplate}"/>

      

             Template="{StaticResource threeWaySwitchItemTemplate}"/>

Figure 5: A sample ListBox might look something like this.

Fortunately, there is a better way. In addition to a Template property, most WPF controls have a Style property. Sometimes people get confused about the difference between Template and Style and when to use which. A Style is really a much simpler thing than a Template. A Style lets you bundle a series of property settings under a name and apply them as a group. For example, if you wanted a series of TextBoxes to all have bold, yellow, Verdana text, you could create a Style representing those property settings and apply it to each TextBox rather than all the properties. Because the Style can be a resource, it makes your code a bit more efficient (but mainly it makes your XAML a lot more concise).

The interesting thing is that you can use a Style to apply a Template because a Template is nothing more than another property setting on a control. This is very convenient because it allows you to bundle your Template with other property settings you may wish to control; on the whole, this makes for some very elegant XAML. I strongly recommend that people consider using a Style to apply their Templates. In our case, we actually can make use of two Styles: one for the ListBoxItems and one for the ListBox (see Figure 6).

Figure 6: The Styles for the ListBoxItems and the ListBox.

Note that the Style for the ListBox not only sets the ListBox template, but sets the ItemContainerStyle, as well which is the Style to apply to items in the list. This is pretty nifty. The Style for the ListBox applies a Style to the ListBoxItems it contains. And the Style for those ListBoxItems applies a Template to those items. Now all we have to do is apply the top-level Style to the ListBox and everything cascades down quite elegantly:

     

     

     

Now our three-way switch is ready for use. In Figure 1 I showed three of these against a black background. To achieve that effect, I simply placed three of the ListBoxes above on a WrapPanel on a Page with a black background. Voil ! The full, standalone XAML file is available for download; see end of article for details.

Discussion and Conclusion

At several places in this process you may have noticed that I took some shortcuts. For example, this three-way switch isn t sizeable and it has a fixed-layout design. Likewise, it relies on the Tag property of the items in the list being 0, 1, or 2 which means your ListBox instances have to conform to this standard. Also, it s only a three-way switch. What if you needed a four- or five-way switch?

In addition, none of the control s visual aspects are exposed through properties. What if you wanted the items listed on the right? Or the bottom? What if you wanted selected items red, not yellow, and italic, not bold? What if you wanted the dial to be beveled or raised? Certainly, if you were designing a switch control for resale, you d need to take all these things into consideration. The result would need to be a sizeable n-way switch with a raft of settable properties to customize its look and feel.

So have I cheated? Have I taken too many shortcuts? I don t think so and here s why. If you were planning to sell your control, yes, you d need to do all that extra work to make the control highly flexible. But as I suggested at the outset, one thing that is powerful and compelling about WPF is that it allows us to design UI controls that do exactly what we need. And if what we need is a three-way switch that looks and behaves a certain way, why program it to do any more than that? The three-way switch I demonstrated is deployed in a live enterprise application and it s working just fine. If tomorrow my boss said to me, Hey Matthew, now we need a four-way switch, would I rebuild the three-way switch as an extensible n-way switch? No way! I d simply copy and paste my hundred lines of XAML and tweak it. It would take 10 minutes instead of 10 days. That s the beauty of WPF: Once you get the hang of it, you can quickly build exactly what you need no more, no less.

Of course, there are many interesting ways you might think to improve this three-way switch. You could use a background image or some of WPF s 3D drawing capabilities to make a more realistic control. You could play a custom sound in the selected index changed event. You could even trap some mouse events on the dial and make it directly responsive to mouse input. I hope you ll consider this simple control as a starting place.

I d like to offer a little challenge to asp.netPRO readers. The application described in this article is cute, but it seems somehow incomplete. Once the user has made their flavor, scoops, and container selection, wouldn t it be appropriate for the application to somehow serve them some tasty visual ice cream? Now there s a WPF challenge for you! I would be very interested to see what folks can come up with. If you send me some code, I ll do my best to feature the wackiest, prettiest, and most impressive WPF ice cream implementations in a future article.

The XAML file referenced in this article is available for download.

Matthew Hess is a software developer in Albuquerque, NM. He grew up on a diet of Delphi but has since switched to almost pure .NET. You can reach Matthew with your questions, corrections, suggestions and humor at [email protected]

Begin Listing One The complete ListBox ControlTemplate

      TargetType="{x:Type ListBox}">

      

            

                  CenterX="30"

                  CenterY="35"

                  Angle="45" />

            

                  CenterX="30"

                  CenterY="35"

                  Angle="90" />

            

                  CenterX="30"

                  CenterY="35"

                  Angle="135" />

      

      

            

                  Orientation="Vertical"

                  VerticalAlignment="Center"

                  HorizontalAlignment="Left"

                  IsItemsHost="True"

                  Canvas.Left="50"

                  Canvas.Top="7"/>

     

                  Stroke="Silver"

                  Fill="Gray"

                  Width="50"

                  Height="50"

                  Canvas.Left="5"

                  Canvas.Top="10" />

     

                  StrokeThickness="8"

                  Stroke="Black"

                  StrokeStartLineCap="Round"

                  StrokeEndLineCap="Round"

                  X1="30" Y1="14" X2="30" Y2="56" />

     

                  StrokeThickness="6"

                  Stroke="White"

                  StrokeStartLineCap="Round"

                  StrokeDashCap="Round"

                  StrokeEndLineCap="Round"

                  StrokeDashArray=".3, 10"

                  X1="30" Y1="14" X2="30" Y2="56" />

      

      

            

                  

                        Property="RenderTransform"

                        Value="{StaticResource rotate45}" />

                  

                        Property="RenderTransform"

                        Value="{StaticResource rotate45}" />

            

            

                  

                        Property="RenderTransform"

                        Value="{StaticResource rotate90}" />

                  

                        Property="RenderTransform"

                        Value="{StaticResource rotate90}" />

            

            

                  

                        Property="RenderTransform"

                        Value="{StaticResource rotate135}" />

                  

                        Property="RenderTransform"

                        Value="{StaticResource rotate135}" />

            

      

End Listing One

 

 

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish