Over the past month I’ve posted half a dozen min-articles about creating custom controls that can interact with the Visual State Manager, and the underlying engine that makes it work; especially Dependency Properties and the Parts and States Model.
- Intro to Custom Controls Part 1
- Intro to Custom Controls Part 2
- Creating Custom Controls
- Dependency Properties Part 1
- Dependency Properties Part 2
- Dependency Property Precedence
With this background, we’re ready to move on to the steps of implementing visual states in your custom control. The key concept that we’ve been building to is this: there is a strict separation between the logic of your control, and the visuals of your control. The place that this becomes most clear is often when you click on a control.
Visually, many controls respond to a click with some form of visual change. Logically they respond with some behavior- or they raise an even that allows “listeners” to respond. Keeping these separate is key to the Parts and States model.
The Custom Control that we’re going to create (in a whirlwind fashion here, more carefully and slowly in a forthcoming tutorial and videos) is derived from Control, draws its own shape, is designed to be skinnable, and supports the parts and states model.
As such, it defines three CommonStates
It also defines two CustomStates
Notice that because the StateGroups (CommonStates and CustomStates) are orthogonal, there is no problem having a NormalState in both.
The custom control appears as a grey ball when it is in its Normal CommonState and in its Normal CustomState, but its visual appearance changes noticeably when it moves into any of the other states. In addition, I’ve added a template that changes its appearance in its normal state and in its other visual states and to cap it off the control publishes an event for when it is clicked allowing the page that houses the control (templated or not) to respond to the click. <whew!>
While we’ll save the nitty gritty for the tutorial and the videos, the point to focus on here is this: how does the control convey its visual contract to the Visual State Manager (and to Expression Blend?).
That is accomplished using attributes. These are placed above the definition of the CustomControl and constitute the contract between the control and the Visual State Manager; they assert the state groups and the states within those state groups,
1: [TemplatePart ( Name = "Core", Type = typeof( FrameworkElement ) )]
2: [TemplateVisualState( Name = "Normal", GroupName = "CommonStates" )]
3: [TemplateVisualState( Name = "MouseOver", GroupName = "CommonStates" )]
4: [TemplateVisualState( Name = "Pressed", GroupName = "CommonStates" )]
5: [TemplateVisualState( Name = "On", GroupName = "CustomStates" )]
6: [TemplateVisualState( Name = "Norm", GroupName = "CustomStates" )]
7: public class CustomControl : Control
Line 1 defines the one part of our control; we give its name and its type (in this case, the type is FrameworkElement—which is general enough to encompass any UIControll and, moreover any element that might participate in the Silverlight layout system, have a lifetime and might need support for databinding.
The five TemplateVisualState attributes define two state groups: CommonStates and CustomStates.
Converting CLR events into state changes is the job of the custom class. For example, the CLR knows nothing at all about “mouse over” – that is not a state recognized by Windows or Silverlight. However, Silverlight does recognize MouseEnter and MouseLeave, which gives us all we need.
We begin by creating three member variables,
private FrameworkElement corePart;
private bool isMouseOver;
private bool isPressed;
The first of these is for the body of our control, which we’ll obtain immediately upon applying the template,
public override void OnApplyTemplate()
CorePart = GetTemplateChild( “Core” ) as FrameworkElement;
(This of course relies on naming the appropriate element “Core” in the Xaml, which we, of course, do…)
Height=”200″ RenderTransformOrigin=”0.5,0.5″ >
With the CorePart in hand, and our two private variables, we can set the event handlers, and make the conversion from CLR events to visual state changes. But borrowing from smarter developers we’re going to be tricky; we’ll set the event handler for Core when we set Core’s property, and we’ll do so in a fail-safe manner (allowing for the possibility that either Core doesn’t currently exist or that we’ve been handed a null core). Here’s the complete property that fronts the data member,
1: private FrameworkElement CorePart
5: return corePart;
9: FrameworkElement oldCorePart = corePart;
10: if ( oldCorePart != null )
12: oldCorePart.MouseEnter -= new MouseEventHandler( corePart_MouseEnter );
13: oldCorePart.MouseLeave -= new MouseEventHandler( corePart_MouseLeave );
14: oldCorePart.MouseLeftButtonDown -= new MouseButtonEventHandler( corePart_MouseLeftButtonDown );
15: oldCorePart.MouseLeftButtonUp -= new MouseButtonEventHandler( corePart_MouseLeftButtonUp );
17: corePart = value;
18: if ( corePart != null )
20: corePart.MouseEnter += new MouseEventHandler( corePart_MouseEnter );
21: corePart.MouseLeave += new MouseEventHandler( corePart_MouseLeave );
22: corePart.MouseLeftButtonDown += new MouseButtonEventHandler( corePart_MouseLeftButtonDown );
23: corePart.MouseLeftButtonUp += new MouseButtonEventHandler( corePart_MouseLeftButtonUp );
This isn’t as scary as it looks. It just says “if i’m setting the Core property, I first make a copy of my existing member variable. If that member variable is not null, i unregister all its event handlers. Next, if I was given a non-null new corepart, I register event handlers for the new value.
The event handlers look for MouseEnter/MouseLeave and for buttonDown and buttonUp.
These are translated into visual events, and each time I experience one I call a private method called GoToState. Thus,
void corePart_MouseEnter( object sender, MouseEventArgs e )
isMouseOver = true;
GoToState( true );
You can read this: “The clr tells me the mouse just passed over the core, that means I want to enter MouseOver state, set the flag and call my GoToState method, passing in the flag saying I do want to use transitions (that is, I want to go to the new state but using the transition timings so it doesn’t look goofy)
GoToState calls the Visual State Machine and tells it what state to transition to based on (you guessed it) the flags and whether you said to use the transitions or not)
1: private void GoToState( bool useTransitions )
3: if ( isPressed )
5: VisualStateManager.GoToState( this, "Pressed", useTransitions );
8: else if ( isMouseOver )
10: VisualStateManager.GoToState( this, "MouseOver", useTransitions );
14: VisualStateManager.GoToState( this, "Normal", useTransitions );
19: if ( IsOn )
21: VisualStateManager.GoToState( this, "On", useTransitions );
25: VisualStateManager.GoToState( this, "Norm", useTransitions );
When the Visual State Manager goes to the new state it uses the storyboards that you wrote either in your generic.xaml or in your template. For example, generic.xaml’s response to isMosueOver is captured in its VisualState MouseOver which causes the control to bounce in an appealing way,
1: <vsm:VisualState x:Name="MouseOver">
2: <Storyboard x:Key="Bounce" RepeatBehavior="forever" >
9: <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
10: <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="25"/>
11: <SplineDoubleKeyFrame KeyTime="00:00:00.5" Value="0"/>
12: <SplineDoubleKeyFrame KeyTime="00:00:00.75" Value="50"/>
13: <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>
The template takes the same state and does something a bit different with it, as you’d expect,
1: <vsm:VisualState x:Name="MouseOver">
3: <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
8: <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1.25"/>
10: <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
15: <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1.25"/>
I have to confess, this is what I really like about custom controls; the closer you look, the more there is to see. That said, I think we’ve gone about as far as we can without exploring this in a video, so I’ll turn my attention in coming blog posts to some other emerging features.
(This is the run-up to RTW and so posting may not be as reliable as I might like, please bear with us as we work quickly to ensure that RTW happens without a hitch!)