Silverlight Status Indicator Control

In this article I’m going to create the control similar to the status indicator control which is used for displaying KPIs in the Business Intelligence Studio.

Here is the screenshot of the ComboBox in BI Studio:

The concept of KPI is rather complex, but all that you need to know is that it is displayed differently depending on the state of the value:

  • Good state (green)
  • Moderate state (yellow)
  • Bad state (red)

So let’s open Visual Studio, create new Silverlight Application, make right mouse button click on the project, select Add New Item and then Silverlight Templated Control:

Very often it is difficult to choose between User Control and Templated Control, but in this example I use the second control because it allows to change styles and control templates.

After adding new item you have two files: the code file and the file Generic.xaml which contains default styles of all templated controls:

Now I start to implement behavior of the control.

StatusIndicator.cs

This control will have the following properties:

  • Value – any number which can be displayed or can be ignored by style implementation
  • Status – the number which affects the way of displaying and the state of the control

And following states:

  • Good
  • Moderate
  • Bad
  • Unknown

I will start from the properties. The Value property doesn’t affect anything and is supposed to be displayed as is.

Type inside the StatusIndicator class the symbols `propdp`, press the Tab key two times and change the half-finished property to the following property:

        public string Value
        {
            get { return (string)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value", typeof(string), typeof(StatusIndicator), new PropertyMetadata(null));

The 3rd parameter is the type of the current control (StatusIndicator).
The 4th parameter contains the default value (it is null as a rule) and optional parameter which is a delegate to the function that is called when the property is changed (I will use it in the definition of the Status property below).

The Status property is slightly different. It looks almost the same except of the callback function in the PropertyMetadata parameter:

        public int? Status
        {
            get { return (int?)GetValue(StatusProperty); }
            set { SetValue(StatusProperty, value); }
        }

        public static readonly DependencyProperty StatusProperty =
            DependencyProperty.Register("Status", typeof(int?), typeof(StatusIndicator), new PropertyMetadata(null, OnStatusPropertyChanged));

The callback function is static, but I need to call a method of the instance of the class:

        private static void OnStatusPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ((StatusIndicator)sender).UpdateStatusState();
        }

        private const string StatusGroup = "StatusStates";
        private const string GoodState = "Good";
        private const string ModerateState = "Moderate";
        private const string BadState = "Bad";
        private const string UnknownState = "Unknown";

        private void UpdateStatusState()
        {
            if (this.Status == null)
                VisualStateManager.GoToState(this, UnknownState, true);
            else if (this.Status.Value > 0)
                VisualStateManager.GoToState(this, GoodState, true);
            else if (this.Status.Value == 0)
                VisualStateManager.GoToState(this, ModerateState, true);
            else if (this.Status.Value < 0)
                VisualStateManager.GoToState(this, BadState, true);
        }

The UpdateStatusState method is used for updating the visual state of the control. If-else clauses can be implemented in different way, but I have decided to map positive values to the good state and negative values to the bad state, it is quite logical and conventional.
To change the state of the control, I use the VisualStateManager.GoToState method with has the following parameters:

  1. control – I use the ‘this’ keyword because the state is changed for the current control. It is possible to change a state of another control, but I don’t think that it is a good idea.
  2. stateName – any string with the name of the state
  3. useTransitions – there is such concept in the Expression Blend where you can define a transition which define time to wait before a change between states occurs. Actually I have never used them so I can’t explain why  we need to use them.

Another important remark: visual states must be set after the loading of the control template. To make sure that visual states are applied to the control, it is necessary to call necessary methods after applying the control template:

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            this.UpdateStatusState();
        }

And at the end add this attributes to the class. They are optional and the control will work without them, but maybe they are used somewhere.

    [TemplateVisualState(GroupName = StatusGroup, Name = UnknownState)]
    [TemplateVisualState(GroupName = StatusGroup, Name = GoodState)]
    [TemplateVisualState(GroupName = StatusGroup, Name = ModerateState)]
    [TemplateVisualState(GroupName = StatusGroup, Name = BadState)]
    public class StatusIndicator : Control

The code is finished, now we can implement the default template.
Themes/Generic.xaml

There already is the style for the control, but it contains only one Border. I added the TextBlock which displays the Value property, the ViewBox control which enlarge the text if it is necessary, and 4 empty visual states:

<Style TargetType="local:StatusIndicator">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:StatusIndicator">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="StatusStates">
                                <VisualState x:Name="Unknown" />
                                <VisualState x:Name="Good" />
                                <VisualState x:Name="Moderate" />
                                <VisualState x:Name="Bad" />
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Viewbox>
                            <TextBlock x:Name="ValueTextBlock" Text="{TemplateBinding Value}" />
                        </Viewbox>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

All that remains is to define the behavior of states. Let it be, for example, the changing of the Foreground property of the TextBlock control.

    <VisualStateGroup x:Name="StatusStates">
        <VisualState x:Name="Unknown" />
        <VisualState x:Name="Good">
            <Storyboard>
                <ColorAnimation Duration="0" To="Green" Storyboard.TargetName="ValueTextBlock"
                                                        Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)" />
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Moderate">
            <Storyboard>
                <ColorAnimation Duration="0" To="Yellow" Storyboard.TargetName="ValueTextBlock"
                                                        Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)" />
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Bad">
            <Storyboard>
                <ColorAnimation Duration="0" To="Red" Storyboard.TargetName="ValueTextBlock"
                                                        Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)" />
            </Storyboard>
        </VisualState>
    </VisualStateGroup>

That’s all, now you can add it to the MainPage.xaml:

<local:StatusIndicator Value="42" Status="1" Width="30" Height="30" />

Also you can change the template of the control to display shapes instead of number:

        <ControlTemplate x:Key="ShapesTemplate" TargetType="local:StatusIndicator">
            <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="StatusStates">
                        <VisualState x:Name="Unknown">
                            <Storyboard>
                                <DoubleAnimation To="1" Storyboard.TargetName="UnknownShape" Storyboard.TargetProperty="Opacity" />
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="Good">
                            <Storyboard>
                                <DoubleAnimation To="1" Storyboard.TargetName="GoodShape" Storyboard.TargetProperty="Opacity" />
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="Moderate">
                            <Storyboard>
                                <DoubleAnimation To="1" Storyboard.TargetName="ModerateShape" Storyboard.TargetProperty="Opacity" />
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="Bad">
                            <Storyboard>
                                <DoubleAnimation To="1" Storyboard.TargetName="BadShape" Storyboard.TargetProperty="Opacity" />
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
                <Viewbox>
                    <Grid>
                        <Rectangle x:Name="BadShape" Opacity="0" Fill="Red" Width="40" Height="40"/>
                        <Path x:Name="ModerateShape" Opacity="0" Data="M20,0 L0,40 L40,40 Z" Fill="Yellow"/>
                        <Ellipse x:Name="GoodShape" Opacity="0" Fill="Green" Width="40" Height="40"/>
                        <Ellipse x:Name="UnknownShape" Opacity="0" Fill="Gray" Width="40" Height="40"/>
                    </Grid>
                </Viewbox>
            </Border>
        </ControlTemplate>

Set this template to the control in the MainPage.xaml:

<local:StatusIndicator Value="42" Status="1" Width="30" Height="30" Template="{StaticResource ShapesTemplate}" />

Here is the completed source code: SilverlightTemplatedControls.zip

Advertisements

One Response to Silverlight Status Indicator Control

  1. Anonymous says:

    Congratulations Man

    You are really good !!!

    Greetings from Barcelona

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: