Daily Archive for June 23rd, 2008

Spain!

On Thursday, I leave for a week and a half vacation to Spain.  This will be my first "first world" international vacation in a while…  (Actually since I went to Barcelona & Prague four and half years ago. )

I’m looking forward to simply relaxing and enjoying the beauty (and wine) of Spain.  On the other hand, I’m so NOT looking forward to  the dollar <-> euro exchange rate.

  spain

The plan is split time between Barcelona and San Sebastian, with a side trip to see the Guggenheim in Bilbao.

I’m also making planning on taking a lot of pictures.  :) 

However, I’ve got a new resolution to take my time post-processing them when I get back.  I usually just rush through it, and end up getting burnt out on.  So, expect a long trickle of Spanish Photography!

Quick Facts:

  • money:
    • 1.00 euro = 1.552 usd (OUCH)
  • local time:
    • UTC + 1 (noon in seattle => 11:23pm in spain)
  • weather: 

Parts & States Model with VisualStateManager (Part 3 of 4)

This is the third post in a four part series on Silverlight 2’s Parts & States control model.

Last time, you learned how to reskin an existing control using VisualStateManager.  In this post, you’ll see how to build up a Parts & States-based custom control.  We’ll also explore how you can create more sophisticated visual transitions.

(Series Link:  Part 1, Part 2, Part 3, Part 4)

 

VisualStateManager

We’ve saw it briefly in the last post, but let’s formally introduce VisualStateManager.  :)

VSM

VisualStateManager is the class responsible for control visual state management.  The “visual” modifier in that sentence is important - the control logic remains in charge of the logical state machine.

VSM exposes two main pieces of PME:

  • a VisualStateGroups attached property
    • This property is set on the control template’s root visual and contains all the visual states & transitions for that skin
  • a static GoToState() method 
    • This method causes VisualStateManager to transition the control’s visuals from one visual state to another.

Last time, we concentrated on the VisualStateGroups property in XAML.  Today, we’ll dig into how the control code leverages that GoToState() method.

WeatherControl

The custom control that we’ll be looking at today is a simple WeatherControl. The shell of the control code can be found below.  (Note: For readability, I’ve collapsed some of the code snippets.  You can find the full sample code here.)

   1: public class WeatherControl : Control
   2: {
   3:  
   4:     public WeatherControl()
   5:     {
   6:         DefaultStyleKey = typeof(WeatherControl);
   7:     }
   8:  
   9:     // OnApplyTemplate()
  10:     public override void OnApplyTemplate()
  11:     {
  12:         base.OnApplyTemplate();
  13:     }
  14:  
  15:     // Temperature DP
  16:     public static readonly DependencyProperty TemperatureProperty = = DependencyProperty.Register("Condition", typeof(Condition), typeof(WeatherControl),null);
  17:     public string Temperature
  18:     {
  19:         get { ... }
  20:         set { ... } 
  21:     }
  22:  
  23:     // Condition DP
  24:     public static readonly DependencyProperty ConditionProperty = DependencyProperty.Register("Condition", typeof(Condition), typeof(WeatherControl), new PropertyMetadata(new PropertyChangedCallback(WeatherControl.OnConditionPropertyChanged)));
  25:     public Condition Condition
  26:     {
  27:         get { ... }
  28:         set { ... }
  29:     }
  30:  
  31:     // ConditionDescription DP
  32:     public static readonly DependencyProperty ConditionDescriptionProperty = DependencyProperty.Register("ConditionDescription", typeof(string), typeof(WeatherControl), null);
  33:     public string ConditionDescription
  34:     {
  35:         get { ... }
  36:         set { ... }
  37:     }
  38:  
  39:     // Property change notification
  40:     private static void OnConditionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  41:     {
  42:         WeatherControl weather = d as WeatherControl;
  43:         ...
  44:         weather.OnWeatherChange(null);
  45:     }
  46:  
  47:     // OnWeatherChange virtual
  48:     protected virtual void OnWeatherChange(RoutedEventArgs e)
  49:     {
  50:     }
  51:  
  52: }

 

You can see that our WeatherControl…

  • is a custom control, deriving from Control.
  • defines its own built-in style, as indicated by the DefaultStyleKey.
  • has 3 public dependency properties: 
    • Temperature
    • Condition
    • ConditionDescription

In order to make our WeatherControl skinnable with VSM, we need to:

  • define a control contract
  • discover & manipulate parts
  • wire up appropriate state changes using VisualStateManager

Here we go!

Defining the Control Contract

The control code is responsible for documenting the control contract.  This means it should declare any and all expected Parts and States.  This is done using class level metadata:

   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="Sunny", GroupName="WeatherStates")]
   8: [TemplateVisualState(Name="PartlyCloudy", GroupName="WeatherStates")]
   9: [TemplateVisualState(Name="Cloudy", GroupName="WeatherStates")]
  10: [TemplateVisualState(Name="Rainy", GroupName="WeatherStates")]
  11: public class WeatherControl : Control
  12: {
  13:     ...
  14: }

In the above snippet, there are two attribute classes:

  • TemplatePartAttribute
    • Specifies the name of the part & expected type
  • TemplateVisualStateAttribute
    • Specifies the name of the state & its associated state group

This metadata is not used by the runtime.  However, it is leveraged by tools like Expression Blend for their skinning support. 

These attributes on the WeatherControl give rise to this control bill of materials:

contract

Now, let’s see how the control code manipulates Parts.

Discovering Parts

Parts are named elements in the template and need to be manually discovered by the control code.  This is done in the OnApplyTemplate() virtual, which is called whenever a new template is applied.

   1: // OnApplyTemplate
   2: public override void OnApplyTemplate()
   3: {
   4:     base.OnApplyTemplate();
   5:  
   6:     CorePart = (FrameworkElement)GetTemplateChild("Core");
   7: }
   8:  
   9: // private CorePart property
  10: private FrameworkElement CorePart
  11: {
  12:     get
  13:     {
  14:         return corePart;
  15:     }
  16:  
  17:     set
  18:     {
  19:         FrameworkElement oldCorePart = corePart;
  20:  
  21:         if (oldCorePart != null)
  22:         {
  23:             oldCorePart.MouseEnter -= new MouseEventHandler(corePart_MouseEnter);
  24:             oldCorePart.MouseLeave -= new MouseEventHandler(corePart_MouseLeave);
  25:             oldCorePart.MouseLeftButtonDown -= new MouseButtonEventHandler(corePart_MouseLeftButtonDown);
  26:             oldCorePart.MouseLeftButtonUp -= new MouseButtonEventHandler(corePart_MouseLeftButtonUp);
  27:         }
  28:  
  29:         corePart = value;
  30:  
  31:         if (corePart != null)
  32:         {
  33:             corePart.MouseEnter += new MouseEventHandler(corePart_MouseEnter);
  34:             corePart.MouseLeave += new MouseEventHandler(corePart_MouseLeave);
  35:             corePart.MouseLeftButtonDown += new MouseButtonEventHandler(corePart_MouseLeftButtonDown);
  36:             corePart.MouseLeftButtonUp += new MouseButtonEventHandler(corePart_MouseLeftButtonUp);
  37:         }
  38:     }
  39: }

To find a named element inside of the template, you use the GetTemplateChild() helper method.

In the above example, we discover the “Core” part, which we will use to determine when the control should go into the MouseOver or Pressed states.  Note that the setter logic is resilient to the Core part not being in the template.  This is important, because a control needs to be robust enough to handle a part that is missing or not yet been added.

 

Initiating State Changes

The control code is responsible for telling VisualStateManager when a visual state change should occur.  Therefore, it must maintain the logical state machine that is associated with the visual state machine.

All of Silverlight 2’s built-in controls create a simple helper method to assist with the state changes.  We recommend that you follow a similar pattern:

   1: // GoToState() helper
   2: private void GoToState(bool useTransitions)
   3: {
   4:     //  Go to states in NormalStates state group
   5:     if (isPressed)
   6:     {
   7:         VisualStateManager.GoToState(this, "Pressed", useTransitions);
   8:     }
   9:     else if (isMouseOver)
  10:     {
  11:         VisualStateManager.GoToState(this, "MouseOver", useTransitions);
  12:     }
  13:     else
  14:     {
  15:         VisualStateManager.GoToState(this, "Normal", useTransitions);
  16:     }
  17:  
  18:     //  Go to states in WeatherStates state group
  19:     if (Condition ==  Condition.PartlyCloudy)
  20:     {
  21:         VisualStateManager.GoToState(this, "PartlyCloudy", useTransitions);
  22:     }
  23:     else if (Condition == Condition.Sunny)
  24:     {
  25:         VisualStateManager.GoToState(this, "Sunny", useTransitions);
  26:     }
  27:     else if (Condition == Condition.Cloudy)
  28:     {
  29:         VisualStateManager.GoToState(this, "Cloudy", useTransitions);
  30:     }
  31:     else
  32:     {
  33:         VisualStateManager.GoToState(this, "Rainy", useTransitions);
  34:     } 
  35: }

The GoToState helper method contains a series of if statements which determine the current visual states.  It then tells VisualStateManager to initiate the appropriate state change.  This is done with the static public static bool VisualStateManager.GoToState(Control control, string stateName, bool useTransitions) method.

As you can see, this method…

  • has 3 parameters:
    • control: instance of the control
    • stateName: name of the visual state to go to
    • usetTransitions: flag to determine whether transitions should be run in this state change
  • returns a bool
    • It returns true if the state name was found and false otherwise.
  • is a no op if…
    • the control was already in the passed in visual state
    • the visual state cannot be found

Most control authors will call their GoToState() helper in 3 places:

  • OnApplyTemplate() with no transitions. 
    • When the control first appears, we generally want it to just appear in the appropriate state, and not transition into it.
  • In certain property change notification handlers
  • In certain event handlers

For our WeatherControl, we add these calls:

   1: // OnApplyTemplate
   2: public override void OnApplyTemplate()
   3: {
   4:     base.OnApplyTemplate();
   5:  
   6:     CorePart = (FrameworkElement)GetTemplateChild("Core");
   7:  
   8:     GoToState(false);
   9: }
  10:  
  11: // Property Change Notifications
  12: protected virtual void OnWeatherChange(RoutedEventArgs e)
  13: {
  14:     GoToState(true);
  15: }
  16:  
  17: // Event Handlers
  18: void corePart_MouseEnter(object sender, MouseEventArgs e)
  19: {
  20:     isMouseOver = true;
  21:     GoToState(true);
  22: }
  23:  
  24: void corePart_MouseLeave(object sender, MouseEventArgs e)
  25: {
  26:     isMouseOver = false;
  27:     GoToState(true);
  28: }
  29:  
  30: void corePart_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  31: {
  32:     isPressed = true;
  33:     GoToState(true);
  34: }
  35:  
  36: void corePart_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  37: {
  38:     isPressed = false;
  39:     GoToState(true);
  40: }

For the WeatherControl, we need to initiated state changes when:

  • the template is first applied
  • the Condition property is changed
  • mouse events are raised from the Core part

 

Adding a Built-In Style

So now we have our control logic! 

I’ve cooked up a very fun (if I do say so myself) ControlTemplate to show off our WeatherControl.  The “fun” makes the template a bit long, however.  Here’s the editted version:

   1: <!-- VisualStateManager -->
   2: <vsm:VisualStateManager.VisualStateGroups>
   3:  
   4:     <!-- CommonStates StateGroup-->
   5:     <vsm:VisualStateGroup x:Name="CommonStates">
   6:         
   7:         <!-- CommonStates States-->
   8:         <vsm:VisualState x:Name="Normal"/>
   9:         <vsm:VisualState x:Name="MouseOver"><Storyboard> ... </Storyboard></vsm:VisualState>
  10:         <vsm:VisualState x:Name="Pressed"><Storyboard> ... </Storyboard></vsm:VisualState>
  11:  
  12:         <!-- CommonStates Transitions-->
  13:         <vsm:VisualStateGroup.Transitions>
  14:             <vsm:VisualTransition GeneratedDuration="0:0:.6"/>
  15:             <vsm:VisualTransition To="Pressed" GeneratedDuration="0:0:.4"/>
  16:             <vsm:VisualTransition From="Pressed" GeneratedDuration="0:0:.4"/>
  17:         </vsm:VisualStateGroup.Transitions>
  18:  
  19:     </vsm:VisualStateGroup>
  20:  
  21:     <!-- WeatherStates StateGroup-->
  22:     <vsm:VisualStateGroup x:Name="WeatherStates">
  23:  
  24:         <!-- WeatherStates States-->
  25:         <vsm:VisualState x:Name="Sunny"/>
  26:         <vsm:VisualState x:Name="PartlyCloudy""><Storyboard> ... </Storyboard></vsm:VisualState>
  27:         <vsm:VisualState x:Name="Cloudy"><Storyboard> ... </Storyboard></vsm:VisualState>
  28:         <vsm:VisualState x:Name="Rainy"><Storyboard> ... </Storyboard></vsm:VisualState>
  29:  
  30:         <!-- WeatherStates Transitions-->
  31:         <vsm:VisualStateGroup.Transitions>
  32:             <vsm:VisualTransition GeneratedDuration="0:0:.3"/>
  33:         </vsm:VisualStateGroup.Transitions>
  34:  
  35:     </vsm:VisualStateGroup>
  36:  
  37: </vsm:VisualStateManager.VisualStateGroups