In Part 1 of this series, I introduced how Script# can be used to write C# code against the Silverlight 1.0 APIs and compile it down to Javascript. The goal of the overall series is to build a photo viewer that displays a list of photos returned from Flickr visually presented in a carousel.
Part 1 focused on building a hello-world'ish photo control (we'll eventually use it as the template for carousel items). In this 2nd part, I'll assume the basics I covered, and focus on building the carousel itself. At the end of this post, the carousel will simply be hardcoded to animate some colored rectangle elements. In the next installment, I'll add concepts like data-binding and templating.
Step 1: Create the XAML to define the carousel user interface
Just like before, we'll start with the XAML, saved into a file called Carousel.xaml in the web site.
<Canvas xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="640" Height="200" Background="White">
<Canvas x:Name="carousel" Width="640" Height="200" />
</Canvas>
The XAML for the carousel itself is very simple. A carousel is pretty much an empty panel that happens to arrange its items in a specific circular arrangement. Correspondingly, the visual representation is simply a container canvas element named "carousel" that will hold the individual canvas elements at runtime. The control code will pick up the dimensions of the carousel control as specified the XAML.
Step 2: Create the carousel control
Let's start writing some code, since most of the carousel functionality is about creating the items and arranging them dynamically at runtime. We'll add a new class, called Carousel to our existing Class Library project from part 1. Here is the code for this control:
public sealed class Carousel : IDisposable {
private const double MinScale = 0.5;
private const double MaxScale = 1.5;
private const double AngleIncrement = -0.02;
private const double ItemWidth = 40;
private const double ItemHeight = 40;
private Canvas _containerCanvas;
private ArrayList _items;
private double _width;
private double _height;
private double _radiusX;
private double _radiusY;
private double _centerX;
private double _centerY;
private double _currentAngle;
private double _angleIncrement;
public Carousel(Canvas rootCanvas) {
_containerCanvas = (Canvas)rootCanvas.FindName("carousel");
_width = _containerCanvas.Width;
_height = _containerCanvas.Height;
_radiusX = _width / 2 - ItemWidth;
_radiusY = _height / 2 - ItemHeight;
_centerX = _width / 2 - ItemWidth / 2;
_centerY = _height / 2 - ItemHeight / 2;
_angleIncrement = AngleIncrement;
CreateItems();
ArrangeItems();
}
private void CreateItems() {
_items = new ArrayList();
// Hardcoded items for now
string[] colors =
new string[] { "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000" };
for (int i = 0; i < colors.Length; i++) {
_items.Add(new CarouselItem(this, colors[i], _containerCanvas));
}
}
private void ArrangeItems() {
_currentAngle += _angleIncrement;
int itemCount = _items.Length;
for (int i = 0; i < itemCount; i++) {
double itemAngle = 2 * Math.PI * i / itemCount + _currentAngle;
double x = _radiusX * Math.Cos(itemAngle) + _centerX;
double y = _radiusY * Math.Sin(itemAngle) + _centerY;
double proximity = y / _height + 0.25;
double opacity = Math.Min(proximity, 1);
double scale = Math.Min(MinScale + proximity, MaxScale);
bool active = (y >= _centerY);
((CarouselItem)_items[i]).Update(x, y, Math.Floor(y), scale, opacity, active);
}
}
}
Once the carousel items have been created they are arranged along a circular path. The items that are further behind are scaled down, and have their opacity as well as z-index reduced. The items that are closer to the front are scaled up, and have greater opacity, and are higher in the z-index. This gives the illusion of the carousel feel despite being implemented as 2D scene.
The individual carousel items are represented by a CarouselItem class. For part 2, basically the items are hardcoded, and each item is represented visually as a colored square.
internal sealed class CarouselItem : IDisposable {
private Carousel _carousel;
private Canvas _item;
public CarouselItem(Carousel carousel, string color, Canvas containerCanvas) {
_carousel = carousel;
string xamlFormat =
@"<Canvas Width=""40"" Height=""40"" RenderTransformOrigin=""0.5,0.5"">
<Canvas.RenderTransform>
<ScaleTransform />
</Canvas.RenderTransform>
<Rectangle Width=""40"" Height=""40"" Fill=""{0}"" />
</Canvas>";
string xaml = String.Format(xamlFormat, color);
_item = (Canvas)containerCanvas.GetHost().Content.CreateFromXaml(xaml, /* createNamescope */ true);
containerCanvas.Children.Add(_item);
}
public void Update(double left, double top, int zIndex, double scale, double opacity, bool active) {
Canvas.SetLeft(_item, left);
Canvas.SetTop(_item, top);
Canvas.SetZIndex(_item, zIndex);
_item.Opacity = opacity;
((ScaleTransform)_item.RenderTransform).ScaleX = scale;
((ScaleTransform)_item.RenderTransform).ScaleY = scale;
_item.IsHitTestVisible = active;
}
}
Each item is creates its own UI, a canvas element and adds that element to the container (which happens to be the carousel).
At this point here is the XAML DOM we've created (the tree on the left) and the associated script objects (the tree on the right). This provides the visual representation of the semantics created with the XAML and code written so far. The interesting thing will be to compare this with the updated tree once we've added templating in the next part.

Step 3: Animate the carousel items
To complete the carousel effect, we need to animate the items along the circular path. This can be a fixed continuous animation (like I'll implement for simplicity), or can be based on user interaction (eg. clicking an item moves the item to the front, or the carousel can spin in the clockwise/counter-clockwise direction based on the relative placement of the mouse along the horizontal extent of the carousel)
The first thing to do is to create Storyboard with a duration set to 0 seconds. Listening to the Completed event on this Storyboard will allow us to implement a per-frame callback. This technique allows us to run a bit of animation logic once for each frame rendered to the screen. Here are the code additions.
public sealed class Carousel : IDisposable {
private Storyboard _updateStoryboard;
private object _updateCompletedCookie;
public Carousel(Canvas rootCanvas, string name) {
_updateStoryboard = UIFactory.CreateStoryboard(rootCanvas, "updateStoryboard",
null, null,
"Duration", "0:0:0");
_containerCanvas.Resources.Add(_updateStoryboard);
_updateCompletedCookie =
_updateStoryboard.AddEventListener(TimelineEvent.Completed,
new EventHandler(OnUpdateStoryboardCompleted));
}
private void ArrangeItems() {
...
_updateStoryboard.Begin();
}
public void Dispose() {
_updateStoryboard.RemoveEventListener(TimelineEvent.Completed, _updateCompletedCookie);
}
private void OnUpdateStoryboardCompleted(object sender, EventArgs e) {
ArrangeItems();
}
}
Notice that in the completed event of the Storyboard, the ArrangeItems is called. This increments the position of the items, and re-lays them out as it did before. In addition, it restarts the storyboard, so that the animation logic will render again on the next frame, which results in the continuous animation.
The call to UIFactory.CreateStoryboard is interesting. Script# provides a small set of helpers to create objects such as storyboards, brushes, transforms etc. so you don't have to construct XAML snippets complete with xmlns declarations and then call CreateFromXaml. This is a new addition in Script# 0.4.3.0. It will be interesting to hear what other helper utility APIs you'd like when working against raw Silverlight APIs.
Step 4: Add support for user selection
At this point we have a continually rotating carousel. However, we do want to allow the user to select a particular item, and in order to facilitate that we want to stop the carousel animation when the user hovers over a particular item. When the user is done with the item, we want to resume the regular animation.
The CarouselItem class needs to be updated to track mouse enter and mouse leave events for the canvas it is associated with. When the mouse enters the item, it will tell the carousel to slow down to a stop, and when the mouse leaves the item, it will direct the carousel to speed up. Here are the additions to the CarouselItem control.
internal sealed class CarouselItem : IDisposable {
private Carousel _carousel;
private Canvas _item;
private object _mouseEnterCookie;
private object _mouseLeaveCookie;
public CarouselItem(ColorCarousel carousel, string color, Canvas containerCanvas) {
_mouseEnterCookie = _item.AddEventListener(InputEvent.MouseEnter, new EventHandler(OnMouseEnter));
_mouseLeaveCookie = _item.AddEventListener(InputEvent.MouseLeave, new EventHandler(OnMouseLeave));
}
public void Dispose() {
if (_item != null) {
_item.RemoveEventListener(InputEvent.MouseEnter, _mouseEnterCookie);
_item.RemoveEventListener(InputEvent.MouseLeave, _mouseLeaveCookie);
_item = null;
}
}
private void OnMouseEnter(object sender, EventArgs e) {
_carousel.SlowDown();
}
private void OnMouseLeave(object sender, EventArgs e) {
_carousel.SpeedUp();
}
}
Rather than simply stopping the carousel animation and bring it to a screeching (or jerky) halt, we want to slow it down, and correspondingly we want to restart it by speeding it up gradually. To accomplish this we'll do a couple of things - we'll continue animating for a little longer (0.2 seconds) at a slower speed, and then come to a full stop. This subtle animation behavior results in a much more smooth perception. In order to implement this, we'll create a second storyboard with Duration set to 0.2 seconds. During this period we'll animate the items at a slower rate by choosing to increment their angle by half the regular amount.
public sealed class Carousel : IDisposable {
private Storyboard _delayedActionStoryboard;
private object _delayedActionCompletedCookie;
private string _delayedAction;
public Carousel(Canvas rootCanvas, string name) {
_delayedActionStoryboard =
UIFactory.CreateStoryboard(rootCanvas, "delayedActionStoryboard",
null, null,
"Duration", "0:0:0.2");
_containerCanvas.Resources.Add(_delayedActionStoryboard);
_delayedActionCompletedCookie =
_delayedActionStoryboard.AddEventListener(TimelineEvent.Completed,
new EventHandler(OnDelayedActionStoryboardCompleted));
}
private void OnDelayedActionStoryboardCompleted(object sender, EventArgs e) {
if (_delayedAction == "Stop") {
_updateStoryboard.Stop();
}
else {
_angleIncrement = AngleIncrement;
_updateStoryboard.Stop();
_updateStoryboard.Begin();
}
}
internal void SlowDown() {
_angleIncrement = AngleIncrement / 2;
_delayedAction = "Stop";
_delayedActionStoryboard.Begin();
}
internal void SpeedUp() {
_delayedAction = "Start";
_delayedActionStoryboard.Begin();
}
}
Step 5: Add the carousel control to the page
At this point we have our carousel ready to be consumed on the page. Again, like the last time I am going to use a scriptlet to write the code-behind for my HTML page.
public class CarouselScriptlet {
public static void Main(Dictionary arguments) {
ControlParameters controlParameters =
new ControlParameters((string)arguments["xaml"],
(DOMElement)arguments["xamlContainer"],
"carousel", null);
ControlFactory.CreateSilverlight(controlParameters, OnLoaded);
}
private static void OnLoaded(SilverlightControl control, object context) {
Carousel carousel = new Carousel((Canvas)control.Content.Root);
Application.Current.RegisterDisposableObject(carousel);
}
}
The scriptlet instantiates the Silverlight control, loads the specified XAML into it, and once that is loaded, it instantiates the carousel control we just wrote. Here is the corresponding HTML that defines where the carousel goes in the page, and the scriptlet server control that renders out the script includes and script to activate our code-behind in the page.
<div id="carouselContainer" />
<ssfx:Scriptlet runat="server" ID="scriptlet"
PrecompiledScriptlet="PhotoViewer.CarouselScriptlet">
<References>
<ssfx:AssemblyReference Name="sscorlib" />
<ssfx:AssemblyReference Name="ssagctrl" />
<ssfx:AssemblyReference Name="ssfx.Core" />
<ssfx:AssemblyReference Name="PhotoViewer" />
</References>
<Arguments>
<ssfx:StringLiteral Name="xaml" Value="Carousel.xaml" />
<ssfx:ElementReference Name="xamlContainer" ElementID="carouselContainer" />
</Arguments>
</ssfx:Scriptlet>
At this point we have a somewhat more meaningful example of a Silverlight control authored in Script#. Here is the screenshot of the resulting canvas control.

This control will really start to get interesting the next time around when it isn't hardcoded to display some colored squares. I however published this intermediate version as part 2 for primarily two reasons. First I followed this sequence when building the control myself - in other words, I wanted to get a functional carousel and get the animation working without getting bogged down with the actual photo viewer scenario. Secondly, the next part will introduce concepts like XAML templates, and this post is already quite long at this point!
For those of you want to download the code, here is the sample code. You'll need the latest version of script# installed in order to be able to use this. You can also just open the compiled script files if you're interested in seeing what the resulting script code looks like (since I've only been showing the C# code in the post).
If you have thoughts on this approach as well as on the specifics, please do let me know. Also, if you have any questions, feel free to post them. I'll either answer them as follow up comments, or try to incorporate answers in the summary post at the end.