Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight Custom Control - Number Tumbler

0.00/5 (No votes)
26 Apr 2009 1  
Presents the design and interface considerations together with implementation details for NumberTumbler custom control in Silverlight 3 Beta

Introduction

A couple of RIAs I developed shared some similar requirements in one particular aspect: for the key calculation result UI, as well as the critical numbers displayed to the users, it's user-friendly to animate the numbers when it changes, because the tumbling effects bring attention to the changing fields, helping the user to make more sense of their inputs. The NumberTumbler custom control abstracts out those generic logics and visuals (default) for animating changing numbers, it can be easily re-used in different Silverlight applications.

This article presents the design and interface considerations together with implementation details for NumberTumbler custom control in Silverlight 3 Beta. It also makes notes about some lessons learned during the custom control development process. The companion downable code includes a Visual Studio 2008 SP1 solution with 3 projects: the ASP.NET Web Project that wraps the Silverlight 3 application, a Silverlight 3 application project that demonstrates the capabilities of the NumberTumbler control, and finally, the NumberTumbler custom control class library project. Although this is all about a specific custom control, the process and techniques are applicable to other custom control development.

If you have Silverlight 3 Beta plug-in installed, you can take a look at the sample Silverlight 3 Beta application. If you are interested about how it works, read on. :-)

What Does NumberTumbler Do?

Usually, the calculated or monitored number field in RIA UI is a read only label-like field, and its text property is data bound to the application data model, say model.calcResult. The application business logic processes user data, then updates model.calcResult value. Whenever model.calcResult changes, Silverlight Data Binding engine will notify the UI element to update the display. It's a quick and sudden display update, sometimes user does not get a chance to notice the update. NumberTumbler has all the regular data binding capabilities, whenever model.calcResult changes, it not only tumbles the display number from previous value to the updated one, it also presents some subtle visual indicator to the users about whether the number is going up or down ---- useful when monitoring a stock quote for example.

At the high level, no changes to application logic at all, calculation or monitoring mechanism runs as usual, data binding expression as before, just replace the regular label-like control with the NumberTumbler, it encapsulates all the logic and state manages within itself, and your application will provide better experiences for "changing numbers".

NumberTumbler can still have customized look and feel, we get this customization for free by Custom Control. Additionally, common attributes, like font, size, color, etc., will be customizable as well. At the minimum, when no concrete property value is set to the instance, NumberTumbler will function with default values.

As for the tumbling effects itself, the duration of the number animation from previous value to a new value is customizable too. Because some application may need a longer duration to show the calculation in progress, others may require brief updates visually.

The default Control Template of NumberTumbler also provides the "subtle" visual indicator when it's tumbling: the left hand side red blinking down arrow and the right hand side green blinking up arrow. Those visual indicators will be invisible when there are no number updates.

That's pretty much all the "jobs" NumberTumbler will do, let's build it.

Control Contract

The control contract is the program interface that dictates how application code interacts with NumberTumbler. Based on the specification listed in the above section, NumberTumbler should provide 2 custom data bindable properties, one for the number it displays (Amount property), another for how long the tumbling should last (TumbleSeconds property). Since these properties are data bindable and participate in animations, they are implemented as 2 public Dependency Properties. Fig.1. shows the code in NumberTumbler.cs:

Fig.1. Public Bindable Properties in NumberTumbler
#region Amount Dependency Property ---- Interaction
public double Amount
{
	get { return (double)GetValue(AmountProperty); }
	set { this.FromAmount = this.Amount; SetValue(AmountProperty, value); }
}

// Using a DependencyProperty as the backing store for Amount. 
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty AmountProperty =
	DependencyProperty.Register("Amount", typeof(double), typeof(NumberTumbler),
	new PropertyMetadata(new PropertyChangedCallback
		(NumberTumbler.OnAmountPropertyChange)));

private static void OnAmountPropertyChange
	(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
	NumberTumbler ctrl = d as NumberTumbler;
	ctrl.OnAmountChange((double)e.NewValue);
}
#endregion

#region TumbleSeconds Dependency Property
public double TumbleSeconds
{
	get { return (double)GetValue(TumbleSecondsProperty); }
	set { SetValue(TumbleSecondsProperty, value); }
}

// Using a DependencyProperty as the backing store for TumbleSeconds.  
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty TumbleSecondsProperty =
	DependencyProperty.Register("TumbleSeconds", 
		typeof(double), typeof(NumberTumbler),
	new PropertyMetadata(2.0, new PropertyChangedCallback
		(NumberTumbler.OnTumbleSecondsPropertyChange)));

private static void OnTumbleSecondsPropertyChange
	(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
	NumberTumbler ctrl = d as NumberTumbler;
	ctrl.OnDurationChange();
}
#endregion

Both properties have callback methods registered, corresponding instance methods (OnAmountChange and OnDurationChange) will be invoked whenever property value changes. This callback mechanism is crucial to start the tumbling animation when Silverlight data bind engine notifies NumberTumbler the Amount just changed, we'll see the details when we get to the animation logic section.

One thing worth paying some attention to is how the default value of 2 is registered via PropertyMetadata for TumbleSeconds, this would allow the tumble animation to run in 2 seconds even when the application code doesn't provide the concrete value for TumbleSeconds. It's also valuable when the property is data bound to other UI element, more on this when we discuss the demo project.

What above is the program contract for NumberTumbler. It's also important to provide "Designer Contract" to allow designers and tools to better understand the control structures, and enable the skinning and templating support in Expression Blend.

NumberTumbler's "designer contract" is still based on the requirements listed in the section above. It needs a part to display the number, together with 3 visual states: Normal, TumbleUp and TumbleDown:

Fig.2. Control Contract for Parts and States
[TemplatePart(Name = "TumblerViewer", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "TumblerDownMark", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "TumblerUpMark", Type = typeof(FrameworkElement))]

[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "TumbleUp", GroupName = "CommonStates")]
[TemplateVisualState(Name = "TumbleDown", GroupName = "CommonStates")]
public class NumberTumbler : Control
{
	...
}

Although Silverlight Custom Control attributed parts and states are not required by the runtime, in NumberTumbler's case, the TumblerViewer part is intended to display the number (Amount property) visually, it's required by the control logic. Without the TumblerDownMark or TumblerUpMark parts, there would be no visual indicator when number moves down or up, but the number is still tumbling and displayed, the essential functionality still works. But if the TumblerViewer part is missing, no number will be displayed. The code in override OnApplyTemplate method will make sure TumblerViewer part's existence:

Fig.3. Make sure parts exist
public override void OnApplyTemplate()
{
	base.OnApplyTemplate();

	this._tumblerViewer = (FrameworkElement)GetTemplateChild("TumblerViewer");
	if (null == this._tumblerViewer)
		throw new NotImplementedException("Template Part TumblerViewer 
			is required to display the tumbling number.");

	VisualStateManager.GoToState(this, "Normal", false);
}

When reviewing the "jobs" that NumberTumbler should do listed in the last section, most of them are covered by the program public properties and the 3 parts and 3 states in designer contract. What about the customizations for font, size, color, etc.? Those properties are inherited from the base class (Control class), so we get them for free.

Actually, if the base class is TextBlock, we could inherit more useful properties, like WordWraps, etc.

From a technical perspective, it makes more sense to derive NumberTumbler from TextBlock, rather than Control, because NumberTumbler only needs to extend the functionality in TextBlock. Unfortunately, Silverlight 3 Beta's TextBlock is a sealed class. There are more sealed control classes than I expected, I don't quite understand why TextBlock needs to be sealed, this is the first lesson learned for building custom control: only a handful of Silverlight Controls included in framework can be derived from, many of them are sealed. The base class of your custom control is usually Control, ContentControl or ItemsControl. Wish this will change in Silverlight 3 RTW.

Default Control Template

Now we defined NumberTumbler control's program and designer contract. It's time to provide the default control template in generic.xaml under themes folder in the NumberTumbler class library project. The default control template provides all the parts and states listed in the class attributes, every visual tree element in the template can be customized by application skinning or in Expression Blend:

Fig.4. generic.xaml skeleton code for the default control template
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
  xmlns:hanray="clr-namespace:Hanray">

 <!-- Built-In Style for NumberTumbler -->
  <Style TargetType="hanray:NumberTumbler">
  <Setter Property="Template">
  <Setter.Value>
  <!-- ControlTemplate -->
  <ControlTemplate TargetType="hanray:NumberTumbler">
  <!-- Template's Root Visual -->
  <Grid x:Name="LayoutRoot">
  <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
  <!--TumblerDownMark PART-->
  <Image x:Name="TumblerDownMark" Margin="10,10,0,10"
  Source="NumberTumbler;component/themes/downimg.png" />

 <!--TumblerViewer PART-->
  <TextBlock x:Name="TumblerViewer" Margin="0,10,0,10"
  HorizontalAlignment="Center" VerticalAlignment="Center"
  Text="{TemplateBinding AmountStr}" />

 <!--TumblerUpMark PART-->
  <Image x:Name="TumblerUpMark" Margin="0,10,10,10"
  Source="NumberTumbler;component/themes/upimg.png" />

 </StackPanel>


 <!--VisualStateManager-->
  <VisualStateManager.VisualStateGroups>

 <!--CommonStates StateGroup-->
  <VisualStateGroup x:Name="CommonStates">

 <!--CommonStates States-->
  <VisualState x:Name="Normal">
  ...
  </VisualState>

  <VisualState x:Name="TumbleUp">
  ...
  </VisualState>

 <VisualState x:Name="TumbleDown">
  ...
  </VisualState>


  </VisualStateGroup>
  </VisualStateManager.VisualStateGroups>

  </Grid>
  </ControlTemplate>
  </Setter.Value>
  </Setter>
  </Style>
  </ResourceDictionary>

You may have noticed the Text property of TumblerView part (TextBlock) is bound to AmountStr instead of Amount by TemplateBinding. Why not TemplateBinding to the public Amount property and use IValueConverter instance to convert the double type Amount to the right format in string? Like:

Fig.5. TemplateBinding Experiment
<Grid.Resources>
	<hanray:AmountFormatter x:Key="amtFormatter" />
</Grid.Resources>

<!--vText PART and IValueConverter-->
<TextBlock x:Name="TumblerViewer" Canvas.Top="0" Canvas.Left="0"
   HorizontalAlignment="Center" VerticalAlignment="Center"
   Text="{TemplateBinding Amount, Converter={StaticResource amtFormatter}, 
	ConverterParameter=\{0:n\}}" />

Unfortunately, the above "technically correct" approach throws AG_E_UNKNOWN_ERROR exception in runtime. After removing Converter and ConverterParameter from the TemplateBinding expression, the exception is gone, but no display for the Amount at all.

This is the second lesson learned: In default ControlTemplate's TemplateBinding, if the source property data type is not the same type as the target property, then another "medium" property can help to work around it by converting the type. The source property Amount's type is double, the target Text property type is string, so I create a private property, AmountStr, as string, to make the TemplateBinding working correctly.

Tumbling Animation Logics

Now we have default ControlTemplate defined in generic.xaml based on the pre-defined control contract, let's design the tumbling animation logics.

Obviously, since the public property Amount data type is double, it's very easy to calculate interpolated Amount by a DoubleAnimation to simulate the "tumbling", and the animation StoryBoard duration is set by the value of TumbleSeconds property.

Fig.6. Set up the Tumbler
private Storyboard _tumblerBorard = null;
private DoubleAnimation _tumbler = null;
private void PrepareTumbler()
{
	if (null != _tumblerBorard)
	{
		_tumblerBorard.Stop();
		return;
	}

	_tumblerBorard = new Storyboard();
	_tumbler = new DoubleAnimation();
	SetTumbleDuration();

	_tumbler.Completed += new EventHandler(_tumbler_Completed);

	Storyboard.SetTarget(_tumbler, this);
	Storyboard.SetTargetProperty(_tumbler, 
		new PropertyPath("NumberTumbler.TumbleAmount"));

	_tumblerBorard.Children.Add(_tumbler);
}

private void _tumbler_Completed(object sender, EventArgs e)
{
	VisualStateManager.GoToState(this, "Normal", false);
}

private void SetTumbleDuration()
{
	Duration duration = new Duration(TimeSpan.FromSeconds(this.TumbleSeconds));
	_tumbler.Duration = duration;
}

The SetTumbleDuration() method is executed not only by PrepareTumbler() method, but also invoked by the property change callback method (OnTumbleSecondsPropertyChange). PrepareTumbler() method makes sure the StoryBoard and DoubleAnimation instance are instantiated on demand and only once, and SetTumbleDuration() method will always set the new value of TumbleSeconds property to the DoubleAnimation's Duration.

Since Silverlight does not support triggers in XAML yet, we need to set up our own triggers to start the tumbling DoubleAnimation. Whenever the public property Amount changes, property callback method OnAmountPropertyChange() will call instance method OnAmountChange() method. (see Fig.1.) If it's the initial time to display Amount, OnAmountChange() simply displays the number without animation, otherwise it calls StartTumbler() to show the tumbling effect:

Fig.7. Tumble triggers
private bool _firstRun = true;
protected virtual void OnAmountChange(double newAmount)
{
	this.ToAmount = newAmount;
	if (_firstRun)
	{
		this.TumbleAmount = this.ToAmount;
		_firstRun = false;
	}
	else
		StartTumbler();
}

private void StartTumbler()
{
	string stateName = "Normal";
	if (this.FromAmount < this.ToAmount)
		stateName = "TumbleUp";
	else if (this.FromAmount > this.ToAmount)
		stateName = "TumbleDown";
	else
		this.TumbleAmount = this.ToAmount;

	if (stateName != "Normal")
	{
		this.PrepareTumbler();

		this._tumbler.From = this.FromAmount;
		this._tumbler.To = this.ToAmount;

		this._tumblerBorard.Begin();
	}

	VisualStateManager.GoToState(this, stateName, false);
}

Notice the StoryBoard's target property is not Amount, it's TumbleAmount instead. This is the third lesson I learned: the animated property should not be the public property that serves as the animation trigger. If it is, then every interpolated value changes in timelime will cause the trigger and animation to start again, this is definitely not we intended. The solution is to add 3 more private properties (all in double type): when the public Amount changes, its previous value will copy to FromAmount; the updated new Amount will be the value of ToAmount; and lastly, whenever TumbleAmount is interpolated by the DoubleAnimation, its property change callback method (OnTumbleAmountPropertyChange)) will update AmountStr, which is data bound to the TumblerViewer part as a string:

Fig.8. Tumble wires
private static void OnTumbleAmountPropertyChange
	(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
	NumberTumbler ctrl = d as NumberTumbler;
	ctrl.AmountStr = String.Format(CultureInfo.InvariantCulture, 
			"{0:n}", (double)e.NewValue);
}

Use the NumberTumbler Control

There is nothing special about how to reference the control assembly in the demo project References list, also need to specify the namespace of the NumberTumbler control in XAML, those steps are the same to all referenced custom control in a Silverlight application. Since only the Amount property is required (TumbleSeconds defaults to 2 seconds, remember? See Fig.1.), the XAML is very simple:

Fig.9. Use NumberTumbler in XAML
<hanray:NumberTumbler x:Name="numTumbler"
	Amount="2009.04"
    Margin="100" HorizontalAlignment="Center"
	FontWeight="Bold" Foreground="Navy" FontSize="26">
</hanray:NumberTumbler>

We have no custom code for Margin, HorizontalAlignment, FontWeight, FontSize and Foreground properties, they are inherited from Control baseclass.

To experiment with some new features in Silverlight 3 Beta, the downable demo project code uses element to element binding for TumbleSeconds property. No additional code is in the code behind CS file to make the two way binding work between the two controls:

Fig.10. Element to element binding for TumbleSeconds property
<hanray:NumberTumbler x:Name="numTumbler"
  Amount="2009.04" TumbleSeconds="{Binding Text, ElementName=tumbleSecs}"
  Margin="100" HorizontalAlignment="Center"
  FontWeight="Bold" Foreground="Navy" FontSize="26">
  </hanray:NumberTumbler>

<Canvas Margin="0,0,0,10" />

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
	VerticalAlignment="Center" >
  <TextBlock Text="Tumbling Duration: " />
  <TextBox x:Name="tumbleSecs" Text="{Binding TumbleSeconds, 
	ElementName=numTumbler}" Width="30" Margin="0,-4, 0, 0" />
  <TextBlock Text=" seconds." />
  </StackPanel>

The demo application also has some UI element to change the Amount up and down programmatically, give it a try!

Conclusions

By building this little simple custom control, I feel it's not as easy as building a user control in Silverlight. Although the custom controls have advantages in customization and reusability, the lessons learned in sealed class and TemplateBinding data types are worth some attention when you build new custom controls. Hopefully, Microsoft will make Silverlight custom control easier to build in future releases. But before it happens, if you need one little control to tumble the changing numbers in UI, this one can help to get started!

History

  • 2009.04.20 - First draft
  • 2009.04.26 - Ready to review

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here