Digging Into Custom Controls
Last night’s post was something of a preface, but let’s get started.
[ For those of you who crave the details, the code, the feel of bits between your fingers, watch for a series of videos on this subject to be released in the next couple weeks with source in VB and C# ]
As I started to say last night, the key distinction in writing custom controls in Silverlight as opposed to other GUI environments is the strict division between logic and visuals embodied in the Parts and States Model.
As an aside, this is where we always point out that there is nothing in Silverlight that requires or enforces that you implement your custom control using the Parts and States model, but it is the model recommended by Microsoft, and it is the model understood and supported by Expression Blend. The fact is, I can’t imagine creating a custom control that does not conform to the P&S model except to show that it can be done.
A Brief Introduction to the Parts and States Model
The key concept behind the P&S model is that your control will have a strict separation of logic from visuals, and the visuals will be managed by the Visual State Manager which will need to know (a) what States might the control be in (states are defined in just a moment) and (b) what parts of the control might be under VSM control.
States are familiar to those who’ve worked with Templates, and in truth, if you haven’t you want to stop right here and go do that. I posted three videos on styles and templates that will get you started as well as a few useful blog entries
From a P&S model perspective, a control is either in a state or transitioning from one state to another. The Visual State Manager is responsible for running the storyboard associated with your control being in a given state (such as MouseOver).
If you are templating an existing control, the states have been enumerated already, you can’t add new states unless you create a custom control. More on that in a moment
Parts
Controls are of course made up of many parts (little p) but from a P&S perspective they aren’t considered Parts unless they will be called by methods of the control itself.
For example, the ScrollBar is a control available in the Silverlight toolbox. From the P&S view point it can be decomposed into four Parts.
- · Down Repeat Button
- · Up Repeat Button
- · Scroll Bar
- · Thumb
While there may be other elements in a Scrollbar, these are the Parts, because these elements are the only elements that other elements of the Scrollbar must address directly.
Many controls, for that support the P&S model, such as Button, have no parts at all (!)
Creating the Contract
When you create a custom control in Silverlight you create a “contract” stating “this part is under the domain of the VSM” and the rest is considered logic that is on the “other side of the wall.”
Attributes are a mechanism to store metadata within a .NET program. You can see an example in this excerpt from a Ratings control, which can be “lit” or not depending on the user’s action. (For more on attributes see any good book on C# or VB )
1: [TemplatePart( Name = "Core", Type = typeof( FrameworkElement ) )]
2:
3: [TemplateVisualState( Name = "Normal", GroupName = "CommonStates" )]
4: [TemplateVisualState( Name = "MouseOver", GroupName = "CommonStates" )]
5: [TemplateVisualState( Name = "Pressed", GroupName = "CommonStates" )]
6:
7: [TemplateVisualState( Name = "Lit", GroupName = "RatingStates" )]
8: [TemplateVisualState( Name = "Norm", GroupName = "RatingStates" )]
9:
10: public class RatingControl : Control
This snippet shows six attributes being added to a new Custom Control. The first is the only “Part” named “Core” (stolen directly from Karen Corby). The next three are the three “common states” this new control will support. Notice that they share the GroupName of “CommonStates”. Finally, on lines 7 and 8 are the two RatingStates of Lit and Norm.
The Contract Divides Logic from Visuals
These few lines draw a powerful contract that the developer and designers can rely on, as can Expression Blend. They state clearly that the “Core” object (to be created in Xaml) will be under the management of internal methods as a Part, that the new control will have two state groups, and it enumerates the states within each group.
Further, the class definition shows that our new control derives from the base class Control.
Implementing the Contract
It is up to me now to implement the contract. The steps to getting here were:
- Create a Silverlight Application and choose Project Type Web Site
- Right click on the solution and Add New Project of type Silverlight Class Library
- Add a new class to the Class Library (I named it Rating) which generates Rating.cs
- Throw away Class1.cs which was created for you
- Right click on the Class Library Project and choose Add->New Item. Pick the Silverlight User Control template and name it generic.xaml. It must have that name (!)
- Throw away generic.xaml.cs
Here’s where we are
- Rating.cs will contain the code for your custom control, along with the meta-data to create the contract for the Parts and States model
- generic.xaml will contain the default appearance of your control (in Xaml)
- Once your control is created you will make an instance of it in the Page.xaml of the project you created back in step 1 above.
1: <UserControl x:Class="BookRater1.Page"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:Controls="clr-namespace:ClassLibrary;assembly=ClassLibrary"
5: xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
6: Width="600" Height="400">
7:
8: <Grid x:Name="LayoutRoot" Background="White">
9: <Grid.RowDefinitions>
10: <RowDefinition Height=".5*" />
11: <RowDefinition Height=".5*" />
12: </Grid.RowDefinitions>
13: <Grid.ColumnDefinitions>
14: <ColumnDefinition Width=".5*" />
15: <ColumnDefinition Width=".5*" />
16: </Grid.ColumnDefinitions>
17:
18: <Controls:RatingControl x:Name="Rating1" Grid.Row="0" Grid.Column="0" />
19: <Controls:RatingControl x:Name="Rating2" Grid.Row="1" Grid.Column="0" Template="{StaticResource RatingControlControlTemplate1}" /> 20: </Grid>
21: </UserControl>
A Few Things To Notice
Remember that this is a view of Page.xaml – the page that is using the custom control.
- On line 4 you set up the namespace for the class library.
- On lines 7 and 8 you create two instances of the custom control, the second of which overrides the default appearance by using a template, just as you might do with any other control (we’ve not seen the creation of that template yet)
What is in Rating.cs and generic.xaml?
generic.xaml
1: <ResourceDictionary
2: xmlns= -- Many of these -->
3: <Style TargetType="controls:RatingControl">
4: <Setter Property="Template">
5: <Setter.Value>
6: <ControlTemplate TargetType="controls:RatingControl">
7: <Grid x:Name="LayoutRoot">
8: <Grid.Resources>
9: <Storyboard x:Key="UnLight" >
10: <DoubleAnimation
11: Storyboard.TargetName="Core"
12: Storyboard.TargetProperty="(UIElement.Opacity)"
13: Duration="0:0:0.01" From="1" To=".5"/>
14: </Storyboard>
15: <Storyboard x:Key="Light" >
16: <!-- -->
17: </Storyboard>
18: <Storyboard x:Key="Bounce" RepeatBehavior="forever" >
19: <DoubleAnimationUsingKeyFrames
20: BeginTime="00:00:00"
21: Duration="00:00:01"
22: Storyboard.TargetName="Core"
23: Storyboard.TargetProperty="(UIElement.RenderTransform).
24: (TransformGroup.Children)[3].(TranslateTransform.Y)">
25: <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
26: <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="25"/>
27: <SplineDoubleKeyFrame KeyTime="00:00:00.5" Value="0"/>
28: <SplineDoubleKeyFrame KeyTime="00:00:00.75" Value="50"/>
29: <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>
30: </DoubleAnimationUsingKeyFrames>
31: </Storyboard>
32: <Storyboard x:Key="Dip" >
33: <!-- -->
34: </Storyboard>
35: </Grid.Resources>
36: <vsm:VisualStateManager.VisualStateGroups>
37: <vsm:VisualStateGroup x:Name="CommonStates">
38: <vsm:VisualState x:Name="Normal" />
39: <vsm:VisualState x:Name="MouseOver"
40: Storyboard="{StaticResource Bounce}"/> 41: <vsm:VisualState x:Name="Pressed"
42: Storyboard="{StaticResource Dip}"/> 43: </vsm:VisualStateGroup>
44: <vsm:VisualStateGroup x:Name="RatingStates">
45: <vsm:VisualState x:Name="Norm"
46: Storyboard="{StaticResource UnLight}" /> 47: <vsm:VisualState x:Name="Lit"
48: Storyboard="{StaticResource Light}" /> 49: </vsm:VisualStateGroup>
50: </vsm:VisualStateManager.VisualStateGroups>
51: <Ellipse
52: x:Name="Core"
53: Width="200"
54: Height="200" RenderTransformOrigin="0.5,0.5" >
55: <Ellipse.RenderTransform>
56: <TransformGroup>
57: <ScaleTransform/>
58: <SkewTransform/>
59: <RotateTransform/>
60: <TranslateTransform/>
61: </TransformGroup>
62: </Ellipse.RenderTransform>
63: <Ellipse.Fill>
64: <RadialGradientBrush>
65: <GradientStop Color="#FFFFD954" Offset="0.004"/>
66: <GradientStop Color="#FFE9F515" Offset="1"/>
67: <GradientStop Color="#FFF1F712" Offset="0.911"/>
68: </RadialGradientBrush>
69: </Ellipse.Fill>
70: </Ellipse>
71: </Grid>
72: </ControlTemplate>
73: </Setter.Value>
74: </Setter>
75: </Style>
76: </ResourceDictionary>
This file has been cut down, but you can see that it looks very much like a standard template file. We begin the substantive work on line 8 creating a Resources sections. In here we create a Storyboard for each of the behaviors we might want in a given state. That is, if we have decided that the behavior when we hover over the custom control will be for it to bounce up and down hyperactively we would create the storyboard for that here in the resources area (as we do on lines 18-31).
After the Resources (line 35) we define the Visual State Groups (lines 36-50) and within each of the groups, the visual states. The job here is to assign the appropriate story board to each of the states.
Finally, on line 51 we create our custom control’s default appearance, including the named Part, “Core” which is the ellipse defined on lines 51 to 70. In this simplified example that happens to be the only object in the control, but more complex controls may have many unnamed elements as well.
Rating.cs
The code file for our class defines both the logic and the enabling (private) code for the translation of CLR events to states that the VSM will recognize. It is also here that we apply either the default look (generic.xaml) or the templated look that was requested when the control was instantiated in page.xaml. This is done, essentially by calling firing the base class’s OnApplyTemplate event.
We extract the named part from the Xaml and hold onto it in a member variable, as we’ll use it quite a bit and then we tell the control to GoToState, a private helper method that checks other member variables and determines how to call the Visual State Manager’s static GoToState method,
1: public class RatingControl : Control
2: { 3: private FrameworkElement corePart;
4: private bool isMouseOver;
5: private bool isPressed;
6: public event RoutedEventHandler Click;
7: public RatingControl()
8: { DefaultStyleKey = typeof(RatingControl); } 9:
10: public override void OnApplyTemplate()
11: { 12: base.OnApplyTemplate();
13: CorePart = (FrameworkElement)GetTemplateChild("Core"); 14: GoToState(false);
15: }
16:
17: private void GoToState(bool useTransitions)
18: { 19: if (isPressed)
20: { VisualStateManager.GoToState(this, "Pressed", useTransitions); } 21: else if (isMouseOver)
22: { VisualStateManager.GoToState(this, "MouseOver", useTransitions); } 23: //...
24: }
25: //...
26: }
The two major missing pieces are converting the CLR events to the VSM events and the sneaky fact that the setter for the private member CorePart doesn’t just set the CorePart but it also unregisters its old event handlers and registers its new event handlers for MouseEnter, MouseLeave, MouseLeftButtonDown and MouseButtonUp. This latter step lets us accomplish the former step with event handlers like this
1: void corePart_MouseEnter(object sender, MouseEventArgs e)
2: { 3: isMouseOver = true;
4: GoToState(true);
5: }
6:
7: void corePart_MouseLeave(object sender, MouseEventArgs e)
8: { 9: isMouseOver = false;
10: GoToState(true);
11: }
Even walking through it fairly carefully it can get very confusing; there are some pretty complex attachments going on. Thus, rather than add insult to injury I’ll stop here and recap and then wait until the first video where you can see the pieces working together before going any further.
Putting It Together
In a nutshell, you have 5 files working together when all is done.
- In the ClassLibrary, Rating.cs which defines the logic and methods as well as the attributes of the custom control. The attributes define the contract, and are what make the control skinnable with the assistance of tools like Blend
- Also in the ClassLibrary is a file that must be named generic.xaml that defines the default look for your custom control (in xaml).
- In your application there are three files (as usual) that do the heavy lifting: Page.xaml, Page.xaml.cs and App.xaml. They do their normal jobs here. That is: Page.xaml makes an instance of the control and may or may not include a Template statement asking for a Template in Page.xaml or (more likely) in App.xaml to override the default look and feel
- Page.xaml.cs contains the logic of the application (not of the control) just as it always does
- App.xaml may have a template for your new Custom control just as it may have a template for button or checkbox.
More soon.