This is the third part in what begins to look like a series of fun posts on Silverlight. The previous parts can be found here:
In this part I will be showing you how you can create a highlights control. It’s a control that you can use to show various items. The highlights control will animate the items using a cross-fade animation. You can add any content you like, so this control should be pretty flexible in its use.
I still haven’t got the space to show a demo, but here’s a screenshot to give you an idea of what the control looks like if you have it fully rigged with content.
Diving right in: Setting up the structure of the control
For this sample, you will need a new Silverlight class library project. In this project, there will be two controls, the HighlightDisplay control and the HighlightItemContainer control. The highlights control is a modified ItemsControl with some added logic to control the cross-fading animation. As you can see in the screen shots, it will perform a fade-in of the first item and cross-fade between each item in the collection. The HighlightItemContainer contains some additional behavior needed to control the cross-fading effect as well.
To create the basic structure, you need two templated controls. The first control is the HighlightDisplay control, which will contain the items to be displayed. The second control is the HighlightItemContainer control. This control will be wrapped around each of the items and contains the necessary logic for the animations used in the sample.
Building the HighlightDisplay control
The Highlight display control houses the logic to control the loop that will iterate all the items and invoke the animation logic on each of them. The animation logic however will not be contained in the items provided by the user of the control. This would force him/her to follow a certain pattern to add a highlight to the control. It’s not exactly user-friendly. Also it’s not very good for the stability of the control. One false move and the control would crash.
Instead of letting the user specify an animation for each of the items, the animation is contained in a container that is wrapped around each of the user’s items.
The basic structure of the control is shown below. As you can see it’s pretty basic.
1: /// <summary>
2: /// Displays several highlights in a loop
3: /// </summary>
4: public class HighlightDisplay : ItemsControl
5: {
6: #region Constructors
7: 
8: /// <summary>
9: /// Initializes a new instance of the <see cref="HighlightDisplay"/> class.
10: /// </summary>
11: public HighlightDisplay()
12: {
13: this.DefaultStyleKey = typeof(HighlightDisplay);
14: }
15:
16: #endregion
17: }
Before I get to the item container itself, I’m first going to show you how you can modify an items control to generate your custom item containers for each of the items that is added to the control.
The first step you need to take is to override the IsItemItsOwnContainerOverride(object item) method. This method tells the ItemsControl whether the given item is an item container, or merely content. This step is important, because if this method returns False, there will be no item container generated for a given item. In the HighlightDisplay control, this method will return True if the given item is an HighlightItemContainer control, since that’s the control we need.
1: /// <summary>
2: /// Displays several highlights in a loop
3: /// </summary>
4: public class HighlightDisplay : ItemsControl
5: {
6: // ...
7: 
8: #region Item container generation overrides
9: 
10: /// <summary>
11: /// Determines if the specified item is (or is eligible to be) its own container.
12: /// </summary>
13: /// <param name="item">The item to check.</param>
14: /// <returns>
15: /// true if the item is (or is eligible to be) its own container; otherwise, false.
16: /// </returns>
17: protected override bool IsItemItsOwnContainerOverride(object item)
18: {
19: return item is HighlightItemContainer;
20: }
21: 
22: #endregion
23: 
24: // ...
25: }
The second step is to generate a new item container when one is required for a given item. This is done by overriding the GetContainerForItemOverride() method. In this method you can create a new item container for a specific item. In the case of the HighlightDisplay control, it will create a new HighlightItemContainer.
1: /// <summary>
2: /// Displays several highlights in a loop
3: /// </summary>
4: public class HighlightDisplay : ItemsControl
5: {
6: // ...
7: 
8: #region Item container generation overrides
9: 
10: /// <summary>
11: /// Creates or identifies the element that is used to display the given item.
12: /// </summary>
13: /// <returns>
14: /// The element that is used to display the given item.
15: /// </returns>
16: protected override DependencyObject GetContainerForItemOverride()
17: {
18: var itemContainer = new HighlightItemContainer();
19: return itemContainer;
20: }
21:
22: #endregion
23: 
24: // ...
25: }
The third step is to prepare the item container for the item to displayed. You can do this by overriding the PrepareContainerForItemOverride(DependencyObject element,object item) method. In this method you will need to do two things. First, you need to keep track of the item container. This is required later for the animation logic to work. Also you will need to do this here, because this is the location where you will have the final item container available. Remember that the GetContainerForItemOverride() method is not called when the user added an item which on its own is a container? Getting a hold of the container here, makes sure that you don’t need to come up with less pretty methods to get a hold of a container. I’ve also added support for the ItemTemplate property, which makes the control a little bit more complete.
1: /// <summary>
2: /// Displays several highlights in a loop
3: /// </summary>
4: public class HighlightDisplay : ItemsControl
5: {
6: // ...
7: 
8: #region Item container generation overrides
9: 
10: /// <summary>
11: /// Prepares the specified element to display the specified item.
12: /// </summary>
13: /// <param name="element">The element used to display the specified item.</param>
14: /// <param name="item">The item to display.</param>
15: protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
16: {
17: // Add the container to the item containers collection
18: HighlightItemContainer itemContainer = (HighlightItemContainer)element;
19: _itemContainers.Add(itemContainer);
20: 
21: if (this.ItemTemplate != null)
22: {
23: var contentElement = this.ItemTemplate.LoadContent();
24: itemContainer.DataContext = item;
25: itemContainer.Content = contentElement;
26: }
27: else
28: {
29: itemContainer.Content = item;
30: }
31: }
32: 
33: #endregion
34: 
35: // ...
36: }
The last step in customizing the ItemsControl is to override the ClearContainerForItemOverride(DependencyObject element,object item) method. In this method you can clean up any container related logic that you added in the PrepareContainerForItemOverride method.
1: /// <summary>
2: /// Displays several highlights in a loop
3: /// </summary>
4: public class HighlightDisplay : ItemsControl
5: {
6: // ...
7: 
8: #region Item container generation overrides
9:
10: /// <summary>
11: /// Undoes the effects of the <see cref="M:System.Windows.Controls.ItemsControl.PrepareContainerForItemOverride(System.Windows.DependencyObject,System.Object)"/> method.
12: /// </summary>
13: /// <param name="element">The container element.</param>
14: /// <param name="item">The item.</param>
15: protected override void ClearContainerForItemOverride(DependencyObject element, object item)
16: {
17: // Remove the container from the item containers collection
18: _itemContainers.Remove((HighlightItemContainer)element);
19: }
20: 
21: #endregion
22: 
23: // ...
24: }
25:
To make the control complete, you will need to modify the ControlTemplate for the HighlightDisplay in themes/Generic.xaml that you can find in the Control project.
1: <Style TargetType="local:HighlightDisplay">
2: <Setter Property="Template">
3: <Setter.Value>
4: <ControlTemplate TargetType="local:HighlightDisplay">
5: <Border Background="{TemplateBinding Background}"
6: BorderBrush="{TemplateBinding BorderBrush}"
7: BorderThickness="{TemplateBinding BorderThickness}">
8: <ItemsPresenter />
9: </Border>
10: </ControlTemplate>
11: </Setter.Value>
12: </Setter>
13: <Setter Property="ItemsPanel">
14: <Setter.Value>
15: <ItemsPanelTemplate>
16: <Grid/>
17: </ItemsPanelTemplate>
18: </Setter.Value>
19: </Setter>
20: <Setter Property="ItemTemplate">
21: <Setter.Value>
22: <DataTemplate>
23: <Border BorderThickness="20" BorderBrush="Black">
24: <ContentPresenter Content="{Binding}"/>
25: </Border>
26: </DataTemplate>
27: </Setter.Value>
28: </Setter>
29: </Style>
There are some important parts in the ControlTemplate of the HighlightDisplay control. The ItemsPresenter automatically uses the Items collection of the control, to display the items. You can use this inside any of the layout containers (Grid, StackPanel, Canvas) to display a set of items. The control in its own has no graphical representation, aside from the fact that it will add any items in the items collection to the layout container it’s part of. In this case the ItemsPresenter will add a set of HighlightItemContainer controls to the Grid. This will cause them to overlap, but this is okay, since there will be only two items active at a given moment.
Building the HighlightItemContainer control
The role of the HighlightItemContainer is to serve as a container for content specified by the user. The HighlightItemContainer control contains logic required to make the cross-fade animation work. Anything else that you find in the control is pure cosmetics.
The basic structure of the control is displayed below. The item container is just as basic as the HighlightDisplay control, nothing fancy really.
1: /// <summary>
2: /// Container for the highlight items
3: /// </summary>
4: [TemplatePart(Name = "PART_ContainerGrid", Type = typeof(Panel))]
5: public class HighlightItemContainer : ContentControl
6: {
7: #region Constructors
8: 
9: /// <summary>
10: /// Initializes a new instance of the <see cref="HighlightItemContainer"/> class.
11: /// </summary>
12: public HighlightItemContainer()
13: {
14: this.DefaultStyleKey = typeof(HighlightItemContainer);
15: }
16: 
17: #endregion
18: }
All complicated things in this control can be found in the XAML. The template of the HighlightItemContainer control contains several things, that are best explained using a schematic.
The control is basically a Grid with a ContentPresenter added to it. The ContentPresenter will automatically present the content of the control, without adding additional presentation to it. The special bits with this control are in the animations and the OpacityMask. The OpacityMask determines which part of the control is visible. If for example, you add a LinearGradientBrush to it that fades from white to transparent, you will get a fade-out effect. The more transparent the mask, the less you see of the control itself.
The two animations contained in the HighlightItemContainer control control the gradient stops of the opacity mask. The animations will move them from the startpoint to the endpoint of the control. This is the same for the fade-in animation as well as the fade-out animation. The difference between them is the amount of transparency for each gradient stop.
The fade-in animation will start with two completely transparent gradient stops and animate them using the following four stages.
The fade-out animation does the same thing, but with the gradient stops reversed. So to make the picture complete, here’s the schematic for the fade-out animation.
Both animations are kept as resources of the grid and can be accessed through its Resources collection. The full control template for the HighlightItemContainer can be found below.
1: <Style TargetType="local:HighlightItemContainer">
2: <Setter Property="Template">
3: <Setter.Value>
4: <ControlTemplate TargetType="local:HighlightItemContainer">
5: <Grid x:Name="PART_ContainerGrid" HorizontalAlignment="Stretch"
6: VerticalAlignment="Stretch" Visibility="Collapsed">
7: <Grid.Resources>
8: <Storyboard x:Name="FadeInAnimation">
9: <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="PART_ContainerGrid">
10: <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
11: </ObjectAnimationUsingKeyFrames>
12: <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="PART_ContainerGrid">
13: <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
14: <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0.3"/>
15: <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1"/>
16: <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
17: </DoubleAnimationUsingKeyFrames>
18: <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="PART_ContainerGrid">
19: <EasingColorKeyFrame KeyTime="0" Value="Transparent"/>
20: <EasingColorKeyFrame KeyTime="0:0:0.1" Value="Transparent"/>
21: <EasingColorKeyFrame KeyTime="0:0:0.3" Value="Transparent"/>
22: <EasingColorKeyFrame KeyTime="0:0:0.4" Value="White"/>
23: </ColorAnimationUsingKeyFrames>
24: <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Offset)" Storyboard.TargetName="PART_ContainerGrid">
25: <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
26: <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
27: <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0.7"/>
28: <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
29: </DoubleAnimationUsingKeyFrames>
30: <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="PART_ContainerGrid">
31: <EasingColorKeyFrame KeyTime="0" Value="Transparent"/>
32: <EasingColorKeyFrame KeyTime="0:0:0.1" Value="White"/>
33: <EasingColorKeyFrame KeyTime="0:0:0.3" Value="White"/>
34: </ColorAnimationUsingKeyFrames>
35: </Storyboard>
36: <Storyboard x:Name="FadeOutAnimation">
37: <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="PART_ContainerGrid">
38: <EasingColorKeyFrame KeyTime="0" Value="White"/>
39: <EasingColorKeyFrame KeyTime="0:0:0.1" Value="White"/>
40: <EasingColorKeyFrame KeyTime="0:0:0.4" Value="White"/>
41: <EasingColorKeyFrame KeyTime="0:0:0.5" Value="Transparent"/>
42: </ColorAnimationUsingKeyFrames>
43: <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Offset)" Storyboard.TargetName="PART_ContainerGrid">
44: <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
45: <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
46: <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="0.7"/>
47: <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
48: </DoubleAnimationUsingKeyFrames>
49: <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="PART_ContainerGrid">
50: <EasingColorKeyFrame KeyTime="0" Value="White"/>
51: <EasingColorKeyFrame KeyTime="0:0:0.1" Value="Transparent"/>
52: <EasingColorKeyFrame KeyTime="0:0:0.4" Value="Transparent"/>
53: <EasingColorKeyFrame KeyTime="0:0:0.5" Value="Transparent"/>
54: </ColorAnimationUsingKeyFrames>
55: <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="PART_ContainerGrid">
56: <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
57: <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0.3"/>
58: <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
59: <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
60: </DoubleAnimationUsingKeyFrames>
61: <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="PART_ContainerGrid">
62: <DiscreteObjectKeyFrame KeyTime="0:0:0.5" Value="Collapsed"/>
63: </ObjectAnimationUsingKeyFrames>
64: </Storyboard>
65: </Grid.Resources>
66: <Grid.OpacityMask>
67: <LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
68: <GradientStop Color="Transparent" Offset="0"/>
69: <GradientStop Color="Transparent"/>
70: </LinearGradientBrush>
71: </Grid.OpacityMask>
72: <ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
73: Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
74: </Grid>
75: </ControlTemplate>
76: </Setter.Value>
77: </Setter>
78: </Style>
Controlling the animation
The basic control structure in the previous section is rather unpractical, because the opacity mask makes the items invisible to the user. For the control to work you need to add additional logic for the animation.
Timing the animations
Switching between each of the higlights in the Items collection property of the HighlightDisplay control is done by timer-controlled logic. For you need to add a Timer to the control. The timer is activated when a new item is added to the collection and the control isn’t already active.
1: /// <summary>
2: /// Activates the timer when the collection is changed and the timer isn't already started
3: /// </summary>
4: protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
5: {
6: base.OnItemsChanged(e);
7: 
8: if (!_isTimerActive)
9: {
10: ActivateTimer();
11: }
12: }
13: 
14: /// <summary>
15: /// Activates the highlight timer.
16: /// </summary>
17: private void ActivateTimer()
18: {
19: // Set the timer to the specified duration
20: // Allow a little bit of delay here, to compensate for the loading behavior of the control.
21: // Otherwise the containers that need to be animated aren't there yet.
22: _timer = new Timer(OnHighlightTimerElapsed, null,
23: 10L, (long)Duration.TotalMilliseconds);
24: 
25: _currentItemIndex = 0;
26: _isTimerActive = true;
27: 
28: if (_itemContainers.Count > _currentItemIndex)
29: {
30: _itemContainers.ElementAt(_currentItemIndex).FadeIn();
31: _currentItemIndex++;
32: }
33: }
The ActivateTimer method initializes the timer and specifies the OnHighlightTimerElapsed(object state) method as the callback that gets invoked when the timer elapses. The timer initialization logic uses a duration dependency property, which the user can use to specify the time between each transition. The timer callback logic is shown below.
1: /// <summary>
2: /// Called when highlight timer is elapsed.
3: /// </summary>
4: /// <param name="state">The state.</param>
5: private void OnHighlightTimerElapsed(object state)
6: {
7: // Use the dispatcher, because this timer will be running on a background thread.
8: Dispatcher.BeginInvoke(UpdateCurrentItem);
9: }
The timer initialization logic also specifies a callback method. This method looks somewhat different from what you would do in a normal .NET Application. It uses the Dispatcher associated with the control, to invoke the actual logic to move to the next highlight. This is required, because the timer callback is invoked on a background thread. Background threads in Silverlight are not allowed to do anything that is remotely related to the user interface, so you need the Dispatcher to get from the background thread, back to the UI thread.
The update logic is responsible for fading out the old item and fading in the new item. The method looks more complicated than it really is, the most important bit is in the first two lines. These two lines make sure that if you are at the beginning of the list, the previous item will be retrieved from the end of the list. You can find the UpdateItem method below.
1: /// <summary>
2: /// Updates the current item.
3: /// </summary>
4: private void UpdateCurrentItem()
5: {
6: if (_itemContainers.Count == 0)
7: {
8: return;
9: }
10: 
11: // Find the previous item in the items container collection.
12: // Automatically correct for a collection of exactly 1 item
13: int previousItemIndex = Math.Max(_currentItemIndex > 0 ? _currentItemIndex - 1 : _itemContainers.Count - 1, 0);
14: var previousItemContainer = _itemContainers[previousItemIndex];
15: 
16: Console.WriteLine("Previous item index: " + previousItemIndex.ToString());
17: Console.WriteLine("Current item index: " + _currentItemIndex.ToString());
18: 
19: var currentItemContainer = _itemContainers[_currentItemIndex++];
20: 
21: // Cross-fade the previous item that was displayed and new item to display
22: previousItemContainer.FadeOut();
23: currentItemContainer.FadeIn();
24: 
25: // Reset the current item index to zero if the end of the list is reached
26: if (_currentItemIndex >= _itemContainers.Count)
27: {
28: _currentItemIndex = 0;
29: }
30: }
Note: I found an edge case while testing the control where the code would crash when there’s only one item in the collection. I’ve worked around this by adding Math.Max(calculatedPreviousItemIndex,0); Maybe not the most pretty fix, but it works and it’s fast to write down 😛
Controlling the fade-in and fade-out animations
You may have noticed that the UpdateItem method uses the FadeIn and FadeOut methods on the HighlightItemContainer control. These two methods access the storyboards that I described earlier.
To make accessing the storyboards a bit more efficient, you need to add the following method override to the HighlightItemContainer control.
1: /// <summary>
2: /// When overridden in a derived class, is invoked whenever application code or internal processes
3: /// (such as a rebuilding layout pass) call <see cref="M:System.Windows.Controls.Control.ApplyTemplate"/>.
4: /// In simplest terms, this means the method is called just before a UI element displays in an application.
5: /// For more information, see Remarks.
6: /// </summary>
7: /// <remarks>This method links the layout template to the internal animation mechanics</remarks>
8: public override void OnApplyTemplate()
9: {
10: Panel grid = this.GetTemplateChild("PART_ContainerGrid") as Panel;
11: 
12: if (grid == null)
13: {
14: throw new InvalidOperationException(
15: Properties.Resources.CannotFindContainerGrid);
16: }
17: 
18: _fadeInAnimation = grid.Resources["FadeInAnimation"] as Storyboard;
19: _fadeOutAnimation = grid.Resources["FadeOutAnimation"] as Storyboard;
20: }
The FadeIn and FadeOut methods themselves are pretty basic. They each call the Begin method of the Storyboard for the corresponding animation.
Conclusion
The highlight control on its own isn’t a full Silverlight application, but I think you can put it to good use as a sort of extra on a normal webpage. The control supports data binding, which can come in handy if you want to bind it to a collection of highlights retrieved from a SOAP or WCF RIA service.
At the very least I hope this article provided you with some inspiration as to how you can use Silverlight to spice things up on your website.
You can find the demo code here: