DarksideCookie

Come to the dark side...we have cookies!

Creating custom controls in Silverlight - part 2

In part one, a button like control was created. It worked like a button, or at least a little. It was a square with a descriptive text and a brand new Click event. Unfortunately that doesn't really make it feel like a button. There is more to a button, especially in Silverlight and WPF. First off, a button normally gives some visual feedback to the user, confirming that it is pressed or that the button is hovering over it. That visual feedback is something that you find in most button implementation, whether it is in Windows Forms, HTML or WPF. The thing that is special with buttons in WPF and Silverlight is that the content of the button - in this case the descriptive text - can be anything. And I mean ANYTHING. It could be a Grid with several controls inside. Not that I would recommend doing some of the things that you CAN do, but it is possible. So in this part of the tutorial, the button will get visual feedback and support for complex content.

Lets start off with the support to add anything as content in the control. To add this support is actually pretty simple. First off the TextBlock, that is in the template now, must be removed. Or actually not so much removed as replaced, replaced with a ContentPresenter. The the inheritance of the control has to be changed from Control to ContentControl. That's all there is to it. Oh..yeah...and remember to remove the Text property as well, since it isn't being used anymore it shouldn't be in there...

<TextBlock Text="{TemplateBinding Text}" 
           HorizontalAlignment="Center" 
           VerticalAlignment="Center"
           Foreground="{TemplateBinding Foreground}"/>

Is replaced with

<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />

And the inheritance of the control is changed to

public class MyButton : ContentControl
{
    ...
}

So...this change now forces a change in the "test button". Change the "test button" from using the Text property to use the newly added Content property instead. It could look something like the following

<btn:MyButton Width="100" Height="50" Background="Yellow" Foreground="White" Click="MyButton_Click">
    <btn:MyButton.Content>
        <TextBlock Text="Hello Content" />
    </btn:MyButton.Content>
</btn:MyButton>

The eventhandler of course has to be changed as well, since it works with the Text property. To have the same effect, the new TextBlock has to be named and then modified from the handler

<btn:MyButton Width="100" Height="50" Background="Yellow" Foreground="White" Click="MyButton_Click">
    <btn:MyButton.Content>
        <TextBlock Text="Hello Content" x:Name="ContentText" />
    </btn:MyButton.Content>
</btn:MyButton>
private void MyButton_Click(object sender, MouseEventArgs e)
{
    ContentText.Text = "Hello again";
}

So...now that the content part has been changed, it is time to take care of the visual changes. In the Silverlight 2 beta, all visual changes where based on animations with specific names that were then called from the controls code. The idea is the same today, but it is a little less "fragile". Animations are still responsible for the visual changes, but they aren't called by name from code anymore. This use of specific named is too "fragile". Instead a new control was introduced, the VisualStateManager. The VisualStateManager is responsible for setting states. A state is a specific visual state. So the different states are defined in the VSM and then the code uses the VSM to set. How does this make it less fragile? Well, this will be shown in a little while.

The cool thing about the VSM is that it supports states and state groups. Only one state within a state group can be set at once. But multiple state groups can have states set. So in the MyButton control, there will be only one state group. But if you have a look at for example the Button control it uses two groups, "FocusStates" and "CommonStates". But as said before, the MyButton control will only have a single state group called "CommonStates". Inside that group, it will have 3 states - "Normal", "MouseOver" and "Pressed". These states might not seem logical to everyone, but the cool thing is that every developer can choose the names of the groups and states by themselves. So in this case...it is like this...comprende...

So...to start using the VSM it has to be added to our xaml. This isn't really hard, but it is in a separate assembly and namespace so we need a new xml namespace in the xaml.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyButton"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
    >
...

Next it is time to add the state groups and states. This can be done from 2 directions. Either setting them in code first and them create the xaml, or starting from xaml and then doing the code. It doesn't really matter as long as the different groups and states are already set on paper somewhere... In this case, it will start in the xaml. The VSM is added inside the root Grid like this

<Grid Background="{TemplateBinding Background}">
    <vsm:VisualStateManager.VisualStateGroups>
        <vsm:VisualStateGroup x:Name="CommonStates">
            <vsm:VisualState x:Name="Normal" />
            <vsm:VisualState x:Name="MouseOver" />
            <vsm:VisualState x:Name="Pressed" />
            </vsm:VisualState>
        </vsm:VisualStateGroup>
    </vsm:VisualStateManager.VisualStateGroups>
    ...
</Grid>

Now that all the group and all the states are in there, it is time to add the actual visual changes. This is done by adding animations inside the VisualState elements. So select the states that should be handled and add a Storyboard inside that VisualState. In this case the Normal state will be left empty, that will make it return to the original layout when that state is set. So to handle the MouseOver and Pressed states, the Xaml is changed like this

<vsm:VisualStateManager.VisualStateGroups>
    <vsm:VisualStateGroup x:Name="CommonStates">
        <vsm:VisualState x:Name="Normal" />
        <vsm:VisualState x:Name="MouseOver">
            <Storyboard>
                <ColorAnimation Storyboard.TargetName="BackgroundBrush" 
                    Storyboard.TargetProperty="Color" To="Red" />
                <DoubleAnimation Storyboard.TargetName="BackgroundBrush" 
                    Storyboard.TargetProperty="Opacity" To=".5" />
            </Storyboard>
        </vsm:VisualState>
        <vsm:VisualState x:Name="Pressed">
            <Storyboard>
                <ColorAnimation Storyboard.TargetName="BackgroundBrush" 
                    Storyboard.TargetProperty="Color" To="DarkRed" />
                <DoubleAnimation Storyboard.TargetName="BackgroundBrush" 
                    Storyboard.TargetProperty="Opacity" To=".5" />
            </Storyboard>
        </vsm:VisualState>
    </vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>

That gives the button some seriously ugly effects, but it will at least make some visual changes... The only problem now, is that the time it takes for the animation is too slow. To counter this, it is possible to add a VisualTransitions telling the VSM how much time to spend on each animation. The Transitions property is set like this

<vsm:VisualStateManager.VisualStateGroups>
    <vsm:VisualStateGroup x:Name="CommonStates">
        <vsm:VisualStateGroup.Transitions>
            <vsm:VisualTransition To="Normal" GeneratedDuration="0:0:0.2"/>
            <vsm:VisualTransition To="MouseOver" GeneratedDuration="0:0:0.2"/>
            <vsm:VisualTransition To="Pressed" GeneratedDuration="0:0:0.2"/>
        </vsm:VisualStateGroup.Transitions>
        ...
</vsm:VisualStateManager.VisualStateGroups>

That is all the Xaml needed. Now it is time to start setting the states from code. The first change that is needed is to handle one more event. The event in question is the MouseEnter event. That changes the constructor to look like the following

public MyButton()
{
    DefaultStyleKey = typeof(MyButton);
    this.MouseEnter += new MouseEventHandler(MyButton_MouseEnter);
    this.MouseLeave += new MouseEventHandler(MyButton_MouseLeave);
    this.MouseLeftButtonDown += new MouseButtonEventHandler(MyButton_MouseLeftButtonDown);
    this.MouseLeftButtonUp += new MouseButtonEventHandler(MyButton_MouseLeftButtonUp);
}

The handler is all empty for now. It will only be used for the state handling. Now...the next part is to start handling the states. To set a specific state, all that needs to be done is to call VisualStateManager.GoToState(). It takes three parameters. First the object to set the state on, then the name of the state and finally if a transition should be used. This is kind of cool, because it makes it possible to set the states of other objects and not only your own object...be careful though... So...lets set the state in the different handlers. No...lets not do that. What should be done is to create a method that handles the actual setting of the state. That way all state setting is handled in one place. To support this method, there needs to be some way of telling it what to set. How about sending in the state to the method. No...then you are back to the distributed handling of states... Add a couple of state bools in the class. Then the handlers can set the state bool that is relevant and then just call a state setting method and let it figure out what to do. Something like this

bool _mouseOver = false;
bool _mousePressed = false;

private void SetState()
{
    if (_mousePressed)
        VisualStateManager.GoToState(this, "Pressed", true);
    else if (_mouseOver)
        VisualStateManager.GoToState(this, "MouseOver", true);
    else
        VisualStateManager.GoToState(this, "Normal", true);
}

After adding this simple method it is possible to implement the handlers. All they have to do is set the bool values and then call SetState(). In this control this seems like "a lot" of extra work, but in a control with several state groups and and more states, it makes it a lot simpler to handle different state combinations. The handlers should look something like this

void MyButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    _mousePressed = true;
    SetState();
}
void MyButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    if (_mousePressed)
    {
        OnClick(e);
    }
    _mousePressed = false;
    SetState();
}
void MyButton_MouseEnter(object sender, MouseEventArgs e)
{
    _mouseOver = true;
    SetState();
}
void MyButton_MouseLeave(object sender, MouseEventArgs e)
{
    _mousePressed = false;
    _mouseOver = false;
    SetState();
}

That's it! Well...more or less. I was talking about how fragile the old state handling system was with specifically named animations...this doesn't seem a lot better. Well, so far it isn't. The only difference is that we get some state help from the VSM with groups and things. But it is still state names with strings that we have to figure out if we change the template. The solution to this is attributes. There are two attributes that help us when it comes to templating through tools like Expression Blend. TemplateVisualState and TemplatePart...by setting these attributes on our control, tools like Blend can automatically figure out what parts and states are available and add tooling support for it. The TemplateVisualState has 2 properties, GroupName and Name. They are both strings and define what groups and what states the controls code use. The TemplatePart attribute is a little different. It tells the tool what "parts" it needs in the template and what types they are.

Parts? What? I get states, but what is parts... Well, say that your control expects there to be a button somewhere in the template so that it can handle the user clicking it. Well, if somebody changes the template for the control, they have no idea that there should be that specific button and what it should be named unless they are told. The Slider control is a perfect example. The Slider consists of a bunch of different parts that it needs to work. The attributes for the Slider looks like this

[TemplatePart(Name="VerticalTrackLargeChangeIncreaseRepeatButton", Type=typeof(RepeatButton)), 
TemplatePart(Name="VerticalThumb",Type=typeof(Thumb)), 
TemplatePart(Name="HorizontalTemplate", Type=typeof(FrameworkElement)), 
TemplatePart(Name="HorizontalTrackLargeChangeIncreaseRepeatButton", Type=typeof(RepeatButton)), 
TemplatePart(Name="HorizontalTrackLargeChangeDecreaseRepeatButton", Type=typeof(RepeatButton)), 
TemplatePart(Name="HorizontalThumb", Type=typeof(Thumb)), 
TemplatePart(Name="VerticalTemplate", Type=typeof(FrameworkElement)), 
TemplatePart(Name="VerticalTrackLargeChangeDecreaseRepeatButton", Type=typeof(RepeatButton)),
TemplateVisualState(Name="Normal", GroupName="CommonStates"), 
TemplateVisualState(Name="Focused", GroupName="FocusStates"), 
TemplateVisualState(Name="Unfocused", GroupName="FocusStates"),  
TemplateVisualState(Name="MouseOver", GroupName="CommonStates"), 
TemplateVisualState(Name="Disabled", GroupName="CommonStates")]

That's a lot of states and parts. So how to know what parts to add in you template if there wasn't TemplatePart attributes. The important thing when adding this attribute is to use as broad a type as possible. If you use a Rectangle in the default template, but only use features defined in UIElement, you should define the TemplatePart type to be UIElement. This makes it possible to create the widest variety of templates for the control. Again, the Slider is a perfect example. It defines the HorizontalTemplate as FrameworkElement...that is very early in the inheritance chain for Silverlight, making it possible to be VERY creative with the HorizontalTemplate.

How does a control use the parts it has defined? Well, the MyButton control wasn't complicated enough to use parts, but here is how it should have been done. Since the parts are defined in the template, it isn't possible to code against them straight off. The best way to get hold of the parts is by overriding the OnApplyTemplate() method. This is called as soon as the template is applied to the control, totally ignoring if the template if the default one or one defined by the user. Inside this method it is possible to get hold of named elements, parts, inside the template by calling GetTemplateChild(). This method will of course work outside of this method as well, as long as it is called after the template has been applied. It takes the name of the part as parameter and returns object. Use the "as" operator and beware of the very possible possibility of getting null back, since the template might not define that part. If a part is missing, there is a choice to make. Can the control work without it or should an exception be thrown. Well...it depends on the control... It could look like this

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    FrameworkElement fe = GetTemplateChild("MyPart") as FrameworkElement;
    if (fe != null)
    {
        fe.MouseEnter += ...
    }
    ...
}

Now, there are a couple of tips and convention that some people talk about. First of all, in WPF there is some convention that the name of a part should start with "PART_". In Silverlight this doesn't seem to be considered important. When dissasembling Microsofts own controls, this convention isn't followed... It's up to you...
The next thing would be to make the name of the parts into static fields... Well, that I could sort of agree with and accept. It could look something like this

[TemplatePart(Name=MyButton.MyPartName, Type=typeof(UIElement))]
public class MyButton : ContentControl
{
    public const string MyPartName = "MyPartName";
    ...    
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        FrameworkElement fe = GetTemplateChild(MyButton.MyPartName) as FrameworkElement;
        ..
    }
    ...
}

But once again, that is up to the developer (you). Having it like this makes it less fragile to typing errors.

That concludes my 2 part tutorial about creating custom controls. It is far from exhausting, but should give you enough information to get started building your own controls. Supporting templates makes your controls so much more useful, since the control can be used over and over again with the same functionality, but appearing to be a totally new and good looking control. But since all "built in" Silverlight controls are templatable, make sure you aren't creating unnecessary controls. Try to re-template existing controls instead, since this is less work and less likely to introduce bugs in the system.

By the way, try adding the MyButton control to a page and use Blend to work with the template. By doing this, you will see that Blend automatically picks up the VisualState attributes and add them so that you can create the animations. Just add the control to a page, open the page in Blend and right-click the control and choose "Edit Control Parts (Template) > Create Empty". Even if the new template is empty, it will have the different states available in the States pane.

Thank you for reading and hope you got something good out of it. Don't hesitate to ask questions or add comments about the tutorial!

Posted: Feb 20 2009, 17:35 by ZeroKoll | Comments (2) |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: Silverlight
Manage post: :)

Comments (2) -

Paul United Kingdom said:

Excellent post!!! This has helped me plenty, a quick note though - the suggestion at the bottom of the post to make the fields static causes a syntax error:

public static readonly string MyPartName = "MyPartName";

'An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type'


Change these fields to constants to conform to this way of thinking:

public const string MyPartName = "MyPartName";

Paul

# May 27 2009, 12:42

ZeroKoll said:

Thanks Paul for pointing that out. You are of course completely correct... Code sample updated!

# May 28 2009, 10:34
Comments are closed