OpenLightGroup Blog

rss

Blogs from OpenLightGroup.net


Seeker Sidebar 4 – Styles, Control Templates, and Visual States

Live Example:

http://www.adefwebserver.com/Richard/ooNaEyeDevelopmentv2/ooNaEyeDevelopmentTestPage.html

I’ve been promising to get to Visual States, and this time I’m actually going to do it. In the process I cover creating a templated control from scratch, and technically I achieve my goal of not having to type in the visual states. From there I show how much control a designer has over the many versions of the UI of a templated control, entirely in Blend design view - including how to mess up the xaml if you edit the wrong thing – same choice, different menus. If you don’t already know how to create templated controls, and you want a detailed description with pictures of every step and misstep, read on:

image

Previously I mentioned that it seems to not be possible to add visual states through the Blend designer when you edit a template. In other words you can’t record new visual states, you have to type in the VisualStateManager xaml that defines the VisualStateGroups, etc. That now makes sense to me, as I discuss at the end of this blog, but when the programmer creates the initial templated control it would be nice for her or him to be able to add the visual states by recording them in Blend and anyway, I already wrote the code.

The first part of this blog demonstrates how I went about it:

1. Add the visual components that will make up the templated control to MainPage and add Visual States and Transitions in Blend design view.

2. Create a Silverlight Templated control.

3. Insert the UI from 1 into 2 with a single cut and paste from MainPage.xaml to Generic.xaml, thereby defining the default UI for your new templated control.

But first I want to discuss the mechanics of a templated control, at least as it relates to what I’m about to demonstrate, because I think it makes everything else easier to understand.

Styles and ControlTemplates

Because the emphasis is always on editing templates, the Style / ControlTemplate relationship can be confusing initially. When you first create a templated control, it needs a default ControlTemplate. Now there may be a way to specify a default ControlTemplate, but the way Visual Studio does it is to create a default Style and include a ControlTemplate within the Style, making it the default ControlTemplate. To create new ControlTemplates you edit a copy of the original and save the new version, but in the process a new Style Resource is created which is wrapped around the ControlTemplate Resource. So when you want to create a new version of the Eye control’s UI called KlingonEye, you create a Style named KlingonEye which encapsulates a ControlTemplate containing the xaml that creates the visual representation of KlingonEye, all in Blend design view of course.

So when you use Visual Studio to add a Silverlight Templated Control to the project. It adds a new unnamed Style resource, linked to the new class through its TargetType, to Generic.xaml, which is a Resource Dictionary. The new Style contains a ControlTemplate skeleton to be populated with the xaml that defines the appearance of the control – no different than the xaml on MainPage or in a UserControl – this xaml is just being encapsulated in a way that copies can be easily modified and packaged in new Styles by designers working in Blend design mode.

The new Style is recognized as the default Style because it’s in Generic.xaml.

In addition to the Style Resource entry in Generic.xaml the new class is created with the name you specify and initialization code is added to the constructor to link its default Style to the Style Resource just added to generic.xaml.

For instance in my example I create a templated control named Eyeball. After I create the control through Visual Studio:

image

We get this entry in Generic.xaml

    <Style TargetType="local:Eyeball">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Eyeball">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

And this in Eyeball.cs

namespace ooNaEyeDevelopment
{
    public class Eyeball : Control
    {
        // The constructor. The first line that sets the default type was inserted when Visual Studio
        // was used to create a Silverlight Templated Control named "Eyeball"
        public Eyeball()
        {
            this.DefaultStyleKey = typeof(Eyeball);
        }
}

Here’s how these two find each other:

1. The Eyeball constructor, when it sets this.DefaultStyleKey to typeof(Eyeball) is saying I want my default style to be the default style for the Eyeball class.

2. In Generic.xaml, when the Style says the TargetType is “local:Eyeball”, it’s saying I am a style that can be applied to the the Eyeball class.

3. Because it’s in the Generic.xaml and because it is unnamed, the style is the default style for “local:Eyeball”. Try putting in another unnamed style with the same TargetType and it either won’t compile or won’t run.

The ControlTemplate within the Style also has its TargetType set to “local:Eyeball”. Turns out that’s not required, at least in this case.

So, that’s the general mechanics of a templated control. To recap, you have a single cs file that defines the runtime behavior of the templated control, although you could probably inherit and apply base class Styles to the subclass. You have a default Style resource which defines the UI of the control through an included ControlTemplate. You can have as many alternate Styles to provide alternate UIs as you care to clone from the original or you can create them from scratch, although you don’t want to go nuts – the UI needs to stay consistent with the code-behind. To apply a different style, include a Style= attribute in the xaml.

Creating the Initial UI

My initial goal is to create a templated control, including visual states, without having to manually type in the VisualStateManager code. In my last blog I pointed out that I wasn’t able to add visual states to a templated control through the Blend designer. I could see and modify the existing states and transitions, but the icon for adding new states was disabled.

So what I’m going to do is:

1. Draw a couple of ellipses that represent an eye on MainPage, use GroupInto to put them in a Canvas.

2. In the Blend designer, create a visual state named ‘Dilated’ which will expand the size of the pupil.

3. In Visual Studio, create a Silverlight Templated Control named Eyeball. This creates Generic.xaml and some other required code.

4. Cut and paste the xaml from MainPage.xaml into the Eyeball ControlTempate in Generic.xaml

Here’s the eye…

image

… and MainPage.xaml

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" xmlns:local="clr-namespace:ooNaEyeDevelopment" xmlns:ooNaControls="clr-namespace:ooNaControls" xmlns:System="clr-namespace:System;assembly=mscorlib" x:Class="ooNaEyeDevelopment.MainPage"
    d:DesignWidth="640" d:DesignHeight="480">
  <Canvas x:Name="LayoutRoot" Background="#FFF4EEEE" HorizontalAlignment="Left" Width="160">
    <Ellipse Fill="White" Stroke="Black" Height="72" Width="96" Canvas.Left="0" Canvas.Top="0"/>
    <Ellipse x:Name="ellipse" Fill="Black" Stroke="Black" Height="32" Width="16" Canvas.Left="40" Canvas.Top="20"/>
  </Canvas>
</UserControl>

Before we can add States we need State groups, so click on the ‘States’ tab, then on the ‘Add state group’ icon.

image

Give the state group a name, like ‘Common’ and click on the ‘Add state’ icon

image

Change the name to ‘Dilated’, click on the properties tab and change the ellipse properties as shown below.

image

Click on ‘Base’ when you’re done, then click on ‘Add State’ again. Name this state ‘Normal’, then click on ‘Base’ to turn off recording. Normal is the visual state of the object before any states are applied. We need it so there will be a state to go to when we want to remove the dilation.

Now we add transitions so the pupil will not expand to full dilation instantaneously. On the ‘Normal’ state, click the ‘Add transition’ icon and select ‘Normal => Dilated’

image

Enter .75. Don’t try to type in the ‘s’. Blend will ignore you if you type in the ‘s’. Here’s what it should look like after you type in .75.

image

Now add another transition for ‘Dilated => Normal’ and set it to .5. You should end up with what you see below.

image

Now if you click on the target state of either transition, ‘Normal’, or ‘Dilated’, a timeline will open up in Objects and Timeline. Click on the ‘play’ arrow that I have circled below and you can see the transition.

image

Wiring Up The Initial UI

Now we’re going to wire up a mouse button down event handler to trigger the state. To create the event handler:

1. Select the control that is the target of the event; give it a meaningful name. In this case we’re selecting the ellipse that represents the eye, so I’m naming it ‘Eye’.

2. Select the Properties tab.

3. Select the Events icon.

4. Double-click on the textbox next to the event you want to handle to take the default name for the method.

image

In the event handler call the VisualStateManager.GoToState method to go to the ‘Dilated’ state. Notice that the object passed to the GoToState method is ‘this’, meaning the UserControl named ‘MainPage’. I also want to point out that the second parameter to GoToState is the name of the state, which in this case is '”Dilated”. But, because the state name is a property of MainPage, we can pass in this.Dilated.Name. This gives us the advantage of Intellisense, meaning we can tell whether this is indeed a property on the object we think it is, but the real advantage is that if we spell the state name incorrectly the code won’t compile. Compare that to passing in “Dilate” and waiting until runtime to discover our state change isn’t working, and then having to track the problem down to the typo. I found out about  using the state name instead of a raw string thanks to András Velvárt -  no-more-magic-strings-with-visualstatemanager-gotostate.aspx, where he also shows you how to wire up an event handler when the animation is complete, access the storyboard, and other things you can do with the state name.

I also wired up a Eye_MouseLeave event to return Eye to the Normal state. One last thing to note,  I also call GoToState in the MainPage constructor to go to the ‘Normal’ state. If I didn’t, then the first transition to Dilated would be instantaneous, because the .75 second transition is defined between Normal and Dilated, so we need to start in the Normal state for that transition to take effect.

namespace ooNaEyeDevelopment
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
          InitializeComponent();
          VisualStateManager.GoToState(this, this.Normal.Name, true);
        }
        private void Eye_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
        {
          VisualStateManager.GoToState(this, this.Dilated.Name, true);
        }
        private void Eye_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
        {
          VisualStateManager.GoToState(this, this.Normal.Name, true);
        }

 

Let’s go ahead and encapsulate the eye in preparation for turning it into a templated control. Select the two ellipses. Right click on them, select GroupInto / Canvas.

image

 

Creating the Templated Control and Populating the ControlTemplate

This is where we create a templated control named Eyeball, described at the top of the blog under the discussion of Styles and ControlTemplates.

Now we’re going to copy the xaml from main page into the control template, with one small change. Here’s the xaml from MainPage, notice that the VisualStateManager code is outside the Canvas that encapsulates the two ellipses.

<Canvas x:Name="LayoutRoot" Background="#FFF4EEEE" HorizontalAlignment="Left" Width="320">
    <VisualStateManager.VisualStateGroups>
      ...
    </VisualStateManager.VisualStateGroups>
    <Canvas Height="72" Width="96">
  <Ellipse x:Name="Eye" Fill="White" Stroke="Black" Height="72" Width="96" Canvas.Left="0" Canvas.Top="0" MouseEnter="Eye_MouseEnter" MouseLeave="Eye_MouseLeave"/>
        <Ellipse x:Name="ellipse" Fill="Black" Stroke="Black" Height="32" Width="16" Canvas.Left="40" Canvas.Top="20"/>
    </Canvas>
</Canvas>

In the ControlTemplate, there is no outer Canvas to wrap around the VisualStateManager code, so we put it inside the Canvas that we used to group our Eyeball controls previously. It probably would have been simpler to forget the grouping and just include LayoutRoot as the copy from MainPage to Generic.xaml, but it’s not a big deal one way or another. The important thing is VisualStateManager code must be wrapped in a FrameworkElement.

 

<ControlTemplate TargetType="local:Eyeball">
    <Canvas Height="72" Width="96">
        <VisualStateManager.VisualStateGroups>
           ...
        </VisualStateManager.VisualStateGroups>
        <Ellipse x:Name="Eye" Fill="White" Stroke="Black" Height="72" Width="96" Canvas.Left="0" Canvas.Top="0"/>
        <Ellipse x:Name="ellipse" Fill="Black" Stroke="Black" Height="32" Width="16" Canvas.Left="40" Canvas.Top="20"/>
    </Canvas>
</ControlTemplate>

 

Now if we edit the [Eyeball default] style (1) template (2), we can see that the visual states and transitions are visible in the states window (3).

image

Wiring The Templated Control Event Handlers to Invoke the States – Psych!!!!

However, the code to invoke these states is back in MainPage.xaml.cs. We need to wire up new event handlers in the Eyeball class, so let’s do it the same way we did on MainPage.

But what’s this?  We select the ‘Eye’ ellipse (1),  the Properties tab (2), and the Events icon (3), but there are no events. Just this discouraging message (4).

image

The problem is that the visual elements and the code no longer share the same class, so there is no way to wire the events at design time, by which I mean binding the events in xaml, as in this example from MainPage.xaml.  The MouseEnter and MouseLeave attributes were inserted when we selected the “Eye” under Objects and Timelines, selected Properties/Events, and then double-clicked on the textboxes for those events.

<Ellipse x:Name="Eye" Fill="White" Stroke="Black" 
     ...
     MouseEnter="Eye_MouseEnter" MouseLeave="Eye_MouseLeave"
/>

And the two event handlers were inserted in MainPage.xaml.cs:

private void Eye_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
}
private void Eye_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
}

But now the c# code and the xaml are intentionally disconnected; they could have made it possible for Blend to wire-up the events at design time, but probably not a good idea. Adding event handlers to xaml in a template just seems wrong; probably because it would introduce a dependency between the template and the code. There’s no guarantee that a class that inherits from Eyeball will implement the event handlers, which would break any templates that contained event binding attributes.

Wiring The Templated Control Event Handlers to Invoke the States The Easy Way – No Really, I’m Not Kidding this Time

So the events must be wired up at runtime. First I want to show you the very simplest way to do that, without reference to Parts and States.

The Control class supports Mouse Events and any events not handled by the component element defined in the ControlTemplate will bubble up to the Eyeball class, so we can just add Event Handlers to the Eyeball Object events. We know the visual state names, so we can go to those states in the event handlers. The drawbacks are

(1) We have to hard-code the visual state names

(2) We detect events over the whole control surface, not the individual element where the event occurred.

namespace ooNaEyeDevelopment
{
    public class Eyeball : Control
    {
        // We no longer have design-time visibility of the VisualStates, so we just have to use the raw strings. 
        // It's always a good idea to use constants, but the code will still compile if we spell one wrong.
    const string NORMAL_STATE_NAME = "Normal";
        const string DILATED_STATE_NAME = "Dilated";
        public Eyeball()
        {
            this.DefaultStyleKey = typeof(Eyeball);
            // In general I like to do almost everything in the the 'loaded' event. It's not always necessary, but if
            // you get in the habit then you won't forget and put code in the constructor that shouldn't be run until the
            // object is loaded.
            this.Loaded += new RoutedEventHandler(Eyeball_Loaded);
        }
        void Eyeball_Loaded(object sender, RoutedEventArgs e)
        {
            // Instead of handling events on the "Eye" ellipse we're handling them on the entire control. We can do
            // this because the Control call supports Mouse event handling in expectation that a UI will be attached
            // through a control template. The events then bubble up from the elements in the Control template, so any 
            // MouseEvents on EyeContainer, Eye, or Pupil will end up being handled by the Eyeball class.
            this.MouseEnter += new MouseEventHandler(Eyeball_MouseEnter);
            this.MouseLeave += new MouseEventHandler(Eyeball_MouseLeave);
            // We try to go to the Normal state, but this won't work because the template has not been loaded yet
            VisualStateManager.GoToState(this, NORMAL_STATE_NAME, true);
        }
        void Eyeball_MouseLeave(object sender, MouseEventArgs e)
        {
            // We're passing a string directly instead of a VisualState Name property, because we don't have a VisualState reference
            VisualStateManager.GoToState(this, NORMAL_STATE_NAME, true);
        }
        void Eyeball_MouseEnter(object sender, MouseEventArgs e)
        {
            VisualStateManager.GoToState(this, DILATED_STATE_NAME, true);
        }
    }
}

Another shortcoming of this code is that the call to go to the Normal state in Eyeball_Loaded will have no effect. The Control class still has to wire up the ControlTemplate, and since the VisualStateManager xaml is in the ControlTemplate, the Normal State is not defined yet. Fortunately we can determine when it is available because the OnApplyTemplate method is called by Control when it wires up the code to the xaml in the ControlTemplate. We always call base.OnApplyTemplate anytime we override this method, but the point here is the Normal state is now defined. Note that the third parameter is false. This tells GoToState whether to use transitions or not. It doesn’t really matter in this case, but we are initializing at this point, not going from one state to another, so we specify false, which I should have done in the example above.

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    VisualStateManager.GoToState(this, NORMAL_STATE_NAME, false);
}

 

Wiring The Templated Control Event Handlers to Invoke the States The Parts and States Way

So this is as far as we can go without Parts and States, or I should say, as far as we can go while pretending we don’t know the names of the visual elements. First of all, the Parts and States attributes are for Blend’s benefit, so they can be shown in the Parts panel. So for the Eyeball class code that works with the elements it’s neither here nor there whether the Parts and States model is used. Second of all, the names used both in the Parts and States attributes and by C# to access the elements are manually typed in. The upshot is that you can spell the name wrong and the compiler wont catch it. And best practices call for ignoring any missing parts if at all possible and continuing without them, which means you’ll only be able to tell you spelled the name wrong if you notice that some particular visual element is missing, either a part or a transition most likely, so pay attention to spelling and check that first if something doesn’t work. Use constants so you only have one opportunity to misspell each name.

So, assuming we spell the names right, the Eyeball class can get and bind the visual elements at run-time, which it must do for more sophisticated behavior, such as detecting Mouse Events on the “Eyeball” ellipse instead of just anywhere on the entire templated control. References to the individual elements, including the VisualStates,  can be retrieved with calls to GetTemplateChild(). passing the name of the element, such as (FrameworkElement)GetTemplateChild(“Eye”), or (VisualState)GetTemplateChild(“Dilated”).

Let’s start by giving our elements names that we will use in the TemplatePart attribute entries in Eyeball.cs and making sure they match the names we use when we call GetTemplateChild().

image

Then create the TemplatePart attributes in Eyeball.cs. Note the use of constants.

namespace ooNaEyeDevelopment
{
    [TemplatePart(Name = Eyeball.EYE_CONTAINER, Type = typeof(FrameworkElement))]
    [TemplatePart(Name = Eyeball.EYE, Type = typeof(FrameworkElement))]
    [TemplatePart(Name = Eyeball.PUPIL, Type = typeof(FrameworkElement))]
  
    public class Eyeball : Control
    {
    public const string EYE_CONTAINER = "EyeContainer";
    public const string EYE = "Eye";
    public const string PUPIL = "Pupil";
    
        public Eyeball()
        {
            this.DefaultStyleKey = typeof(Eyeball);
        }
    }
}

Build the project; now when you edit the template if you click on Parts and States (1), you’ll see the names appear (2) and that they match the names you gave the elements (3).

image

Now to wire-up the events. everything I could say is pretty much covered in the comments. We’re back to using VisualState name properties, but we still have to spell the state names correctly or we won’t get the VisualState reference when we call GetTemplateChild, which as I may have mentioned, will not be caught by the compiler :

namespace ooNaEyeDevelopment
{
    // Use constants for the names here, for Blend to see, and for actually accessing the elements
    [TemplatePart(Name = Eyeball.EYE_CONTAINER, Type = typeof(FrameworkElement))]
    [TemplatePart(Name = Eyeball.EYE, Type = typeof(FrameworkElement))]
    [TemplatePart(Name = Eyeball.PUPIL, Type = typeof(FrameworkElement))]
    public class Eyeball : Control
    {
        // The element names
        public const string EYE_CONTAINER = "EyeContainer";
        public const string EYE = "Eye";
        public const string PUPIL = "Pupil";
        // The visual state names
        public const string DILATED_STATE_NAME = "Dilated";
        public const string NORMAL_STATE_NAME = "Normal";
        // To hold references to the visual elements we need to access
        FrameworkElement _EyeContainer = null;
        VisualState _DilatedState = null;
        VisualState _NormalState = null;
        // The constructor. The first line that sets the default type was inserted when Visual Studio
        // was used to create a Silverlight Templated Control named "Eyeball"
        public Eyeball()
        {
            this.DefaultStyleKey = typeof(Eyeball);
            this.Loaded += new RoutedEventHandler(Eyeball_Loaded);
        }
        void Eyeball_Loaded(object sender, RoutedEventArgs e)
        {
            // Normally we'd wire up events here, but in a templated control the parts are not yet assecible,
            // so we do it in OnApplyTemplate();
        }
        void Eyeball_MouseLeave(object sender, MouseEventArgs e)
        {
            // Any missing parts or states should be ignored
            if (_NormalState != null)
                VisualStateManager.GoToState(this, _NormalState.Name, true);
        }
        void Eyeball_MouseEnter(object sender, MouseEventArgs e)
        {
            if (_DilatedState != null)
                VisualStateManager.GoToState(this, _DilatedState.Name, true);
        }
        public override void OnApplyTemplate()
        {
            // Since we override this method, we have to call it on the class we inherited from
            base.OnApplyTemplate();
            // Get references to the parts we need to access
            _EyeContainer = (FrameworkElement)GetTemplateChild(EYE_CONTAINER);
            _DilatedState = (VisualState)GetTemplateChild(DILATED_STATE_NAME);
            _NormalState = (VisualState)GetTemplateChild(NORMAL_STATE_NAME);
            // Initialize the state to Normal
            if (_NormalState != null)
                VisualStateManager.GoToState(this, _NormalState.Name, false);
            // Wire up the mouse events on the "Eyeball" ellipse
            if (_EyeContainer != null)
            {
                _EyeContainer.MouseEnter += new MouseEventHandler(Eyeball_MouseEnter);
                _EyeContainer.MouseLeave += new MouseEventHandler(Eyeball_MouseLeave);
            }
        }
    }
}

This programmatic access to VisualStates opens up all kinds of possibilities. For one thing, you now have access to the storyboard, so you can add an event handler to be called when the animation(s) complete. It’s as simple as this:

public override void OnApplyTemplate()
{
    ...
    _DilatedState = (VisualState)GetTemplateChild(DILATED_STATE_NAME);
    if (_DilatedState != null)
        _DilatedState.Storyboard.Completed += new EventHandler(Storyboard_Completed);
    ...
}
void Storyboard_Completed(object sender, EventArgs e)
{
    MessageBox.Show("Fully Dilated");
}
 

You can even change or replace the storyboard / animations programmatically, but with great power comes great responsibility. One of the main virtues of Visual States is that they can be manipulated by the designer. I’m not really sure when it would be appropriate to modify them at run-time except possibly by using behaviors so the designer can specify properties to control what the changes are. For instance, the duration of a transition may be set depending on run-time conditions, but the designer should be able to control what values to use. As an example, how long does it take for a Seeker to go from the passive state to the chasing state if his energy level is 50%? Specify that relationship and other conditions in a behavior and the designer can specify, at design time, the duration value to be used at runtime.

Or so it seems to me. I’m still trying to sort out the implications in my own mind.

Instantiating the New Templated Control

So, onwards. We can now add an Eyeball templated control to MainPage. If necessary open it up by selecting it under the Solution (1), clicking on design mode (2), clicking on the Resources tab (3), Displaying the styles in Generic.xaml (4), and clicking on the [Eyeball default] style and dragging it onto the page (6).

image

Creating Clones of the Default Control Template and Modifying The Visual States

And now we get to the point, changing the Visual States in Blend by creating new ControlTemplates. Actually, we’ll be creating new Styles that wrap around the ControlTemplates.

There are two ways you can go about this, with two different results. In our case, we want to create a new copy of the ControlTemplate wrapped in the Eyeball default style so we can modify it to make alternative versions of the Eyeball UI. In order to apply that new template it’s best to put it in a new Style resource, and as it turns out, sometimes when we select ‘Edit Template / Edit a copy…’, we end up with a new style wrapped around our new copy of the template. But notice I said sometimes, which is important, because the other times we end up just with a copy of the ControlTemplate with no style wrapped around it.

When this happens you may not notice, or at least I don’t, until it’s inconvenient to fix. I’ve ended up with the template in my Default Style pointing to a new ControlTemplate resource, which is a real pain to fix, so it’s best to pay attention to what you’re actually editing. Hopefully this will help you avoid at least this one gotcha:

The Way You’d Probably Pick but Probably Don’t Want

First I’ll show you the approach you’d most logically pick but that you probably don’t want, as you end up with a new ControlTemplate resource but no new Style:

Select Generic.xaml (1), design view (2), Resources tab (3), Generic.xaml resources (4), [Eyeball default] Edit Resource (5), right-click Style (6),  select ‘Edit a Copy…’ (7)

Fimage

 

And here’s what you’ll see:

image

As you can see, it clearly states ‘Create ControlTemplate Resource’, but if you’ve ended up with a new Style after a similar chain of selections, you might not notice that the caption is different this time, proceed, and end up with at best a ControlTemplate Resource that you’ll have to manually wrap in a style and at worst an altered [Eyeball default] in generic.xaml.

The Way You Probably Do Want

Now I’ll describe the way to end up with a new style wrapped around a ControlTemplate and try to make clear the difference from what we just tried. Then I’ll demonstrate.

The answer is to select an instantiated Eyeball templated control. After that the steps are pretty much identical. The difference:

(Above) I selected a Style resource that can be applied to an Eyeball and edited a copy of the ControlTemplate inside that Style.

(Below) I’m selecting an Eyeball instance and editing a copy of the ControlTemplate wrapped inside the Style that is applied to the Eyeball. In this case it’s the default style.

Since we need an instantiation, we go to MainPage.xaml (1), select design view (2), right-click the Eyeball object (3) select ‘Edit Template / Edit a Copy…’

image

And sure enough, the first thing we do is create a new Style Resource:

image

I went ahead and changed the name to ooNaEyeStyle and created a new ResourceDictionary named Eyeball Dictionary.xaml. The other two options would stick the xaml in App.xaml (available to entire application) or the current page. A Resource Dictionary can be moved from project to project.

So, why the difference in results when we selected edit a copy of the template in both cases? There’s probably a reason, but I just haven’t run across it yet. On the face of it it seems pretty arbitrary. I guess they needed some way to let you create a named ControlTemplate Resource sans Style, and this is how they provided it.

And in the case where you do create a new Style, it’s kind of weird how you go about it. The selection you make is to edit a copy of the template. The result is a new style wrapped around the template. But that’s probably what you really want. Why? Because within a Style, the Template is just another property, so if you put your new ControlTemplate inside a new Style, then you can conveniently include other properties inside the style that will be applied at the same time. So although the emphasis is constantly on ‘edit a copy of the template’, when the template is actually applied, it will be through the style created as you see above. I probably go overboard on this, but it took me a lot of inadvertent trial-and-error (I thought I knew what I was doing but I did not) before I could keep track of where I was and what I was editing fairly consistently.

So after we hit OK we’re presented with the following. We’re editing a style (1) named ooNaEyeStyle (2) in EyeballDictionary.xaml (3), and we see three breadcrumbs (4) with the one named ‘Template’ currently selected:

image

The breadcrumbs let you move between levels and edit each one, which can be confusing if you’re not used to it, and even if you are:

image  image

So, moving on, I select the Eye component (1) in Objects and Timeline. The ‘Template’ Breadcrumb caption changes to ‘Eye’ (2). Parts are visible in the Parts panel (3), and if I select the Properties tab (4) I can see the Eye properties.

`image

So we make a few simple changes and end up with something like this:

image

You can check out what the Eyeball control looks like with the ooNaEyeStyle applied by clicking on the [Eyeball] breadcrumb (1) or clicking on the Edit Level icon a couple of times (2).

And this is what you should see:

image

This is because when Blend creates the new style, it also applies that style to the Eyeball object you selected initially, so instead of just an Eyeball object you’re seeing:

<local:Eyeball Canvas.Left="32" Canvas.Top="96" Style="{StaticResource ooNaEyeStyle}"/>

Let’s take it out for a spin:

image

Well the good news is that the changes worked, and so does the event handler hooked up the the Storyboard completed event; the bad news is that the Dilated state is messed up. Fortunately that’s easy to fix:

Select the Dilated state (1), which turns on Dilated state recording (2). Select the Pupil element (3) and the Properties tab (4). Change the Top property to 16 (5) and to make things a little more interesting, throw in a rotation of 180 (6).

image

To be honest, I took the transitions out because I messed them up when I originally tried to add the changes. I accidentally edited the transitions instead of the state and I couldn’t seem to straighten it out. Since the original transitions were so simple I just deleted them and now I’m going to add them back in. If you don’t mess up like I did, you shouldn’t have to. At any rate, if I select the Dilated transition (1), the timeline opens and I can select the play button (2) to see the transition play. If I click on the Normal transition above I can see it play the other way. So I’m able to confirm that my rotation plays out in both directions.

image

So if the rotation is set on the Dilated state, and not the transition, and if you’re wondering why the Angle is set to 180 when the Dilated transition is selected, I did too, until I looked at the timeline and realized it reflects the angle at the end of the transition. It’s tricky recording states and transitions. Usually you want to make sure you’re on a state and not a transition when recording is on. You would only record a transition if you wanted some visual change to happen in only one direction. So you have to pay attention to the recording state – I’ve had to delete transitions more than once because it’s easier than trying to back out the edits. Get in the habit of clicking on Base to get out of recording state when you’re done.

Also, you don’t want to be recording the Normal state. If you find you want to change the appearance after a transition to Normal, then get out of recording entirely and click on the object in Objects and Timeline to edit its base appearance. Normal should normally  reflect the object before any states were applied, although I’m sure there are exceptions that I haven’t thought of.

I added another couple of Eyeballs with a style I derived from ooNaEyeStyle. I call it SeekerEyeStyle. The lower one is dilated. Notice how I can layer on effects. The stock Eyeball just enlarges the size of the pupil. The ooNaEyeStyle adds in pupil rotation. Then SeekerEyeStyle adds color change to the pupil and the eye and counter rotation and enlargement of the eye itself. So you could have a whole ‘taxonomy’ of  styles, such as

1. Default Style (Naked Eyeball)

2. Primate

3. Human

4. Child

 

Notice that every variation was done through the blend designer. I didn’t even have to edit any xaml files.

Ok, I think that’s enough. If you’re wondering why I didn’t cover the ‘States’ part of ‘Parts & States’, it’s because so far Blend has supported everything I need to do with States, so I’m still trying to figure out what adding TemplateVisualState Attribute entries would add, maybe they’re not long necessary in Blend 3.  Here’s an example from a blog by Karen Corby, from a series that I highly recommend, even though it’s for Silverlight 2.

http://scorbs.com/2008/06/23/parts-states-model-with-visualstatemanager-part-3-of-4/

[TemplateVisualState(Name="Sunny", GroupName="WeatherStates")]
[TemplateVisualState(Name="PartlyCloudy", GroupName="WeatherStates")]
[TemplateVisualState(Name="Cloudy", GroupName="WeatherStates")]  
[TemplateVisualState(Name="Rainy", GroupName="WeatherStates")]

 

Finis

Hopefully this project has shown you how to easily create templated controls with visual states and how to create variations by copying and modifying ControlTemplates in Blend. I understand now why Blend won’t let you create new Visual States when editing a template. The class has no way of knowing about this new state, so how will it be invoked?  And maybe that’s why you need Visual Studio to create a Templated Control in one step. You can do it in Blend by creating a class and adding the necessary code to linked it up to the default style in Generic.xaml, which you’d also have to add manually. But the designer should ordinarily not need to add the initial templated control. The original control is created by the programmer at which time the default style and Control Template for the control are created. It’s up to the designer to supply the UI by populating the ControlTemplate in the default state, and to create variations on the UI by cloning new Styles that wrap ControlTemplates with new and different xaml, visual states, and transitions. However the one shortcoming is that the visual states need to make it into the ControlTemplate somehow, and it should be up to the programmer to put them there, because it’s the programmer who defines what states the C# runtime code handles. My example provides a way to do that without typing it in manually.

But that’s a minor point. The important thing is that with templated controls the designer’s control over the UI is infinitely extensible, all through Blend and without touching any xaml directly, but always with that option.

The project contains some UserControls and templated controls where I was experimenting with making the individual elements in the ControlTemplate templated controls in themselves. So the pupil would be a templated control that knows how to dilate itself. That works fine, except it’s unable to keep itself centered. That’s where run-time manipulation of the state transitions could come in. The “post-Dilation” center point would be calculated at run time and a transform would be modified accordingly. Feel free to check them out and do what you want with them.

The exciting concept to me is to have a framework of intelligent templated controls, so that each has the intelligence built in to handle the appropriate tasks. Such as an identi-kit with an infinitely flexible set of facial parts, each with the intelligence to arrange itself in the appropriate visual configuration. Eyebrows that arch. foreheads that furrow, mouths that smile and frown. And faces that can adopt expressions such as surprise or anger by telling each component part to go to the appropriate state. I hope to take advantage of that concept as I convert the ooNaBoreans to templated controls.

Download Code:

http://www.adefwebserver.com/Richard/ooNaEyeDevelopmentv2/ooNaEyeDevelopme_v2.0.zip





Comments are closed.