Programming Silverlight 1.0 with C# - Photo Carousel (Part 2)

Script# fully supports programming Silverlight 1.0. You can start writing your RIA and associated components and controls in C# rather than in JavaScript... today! This series of posts will build a Flickr photo viewer one step at a time to illustrate this approach. Part 2 starts building relatively more interesting functionality - specifically the carousel control.

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.

The carousel starts to take form...

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.

Posted on Sunday, 9/16/2007 @ 4:41 PM | #Silverlight


Comments

26 comments have been posted.

Mikkel

Posted on 9/17/2007 @ 1:29 AM
I really love your stuff!

I'm getting a parser error though: The file '/Site/ColorCarousel.aspx.cs' does not exist. Renaming it to just Carousel.aspx.cs doesn't really do it - it's giving me some JS-errors now.

Nikhil Kothari

Posted on 9/17/2007 @ 6:11 AM
Sample has been fixed... thanks for the heads up Mikkel.

yaip

Posted on 9/17/2007 @ 12:29 PM
I am a VB developer. Can I do this?

WooBoo

Posted on 9/17/2007 @ 6:59 PM
I guess you had a crash on your projects site and the latest Script# lib (0.4.3.0) isn't there anymore (yet?).
I managed to walk around the lack of new cool stuff you added (CreateFromXaml from UIFactory) but there is still a bug in converting numbers.
All float/double variables converted from C# to JS has "," as a decimal symbol instead of ".".
Well... My regional settings says so, but i don't think that it has any influence on JS language standards.

Sam

Posted on 9/19/2007 @ 8:15 PM
I do not see Script# 0.4.3.0. WooBoo how did you work around the UIFactory issue?

Nikhil Kothari

Posted on 9/20/2007 @ 12:42 AM
WooBoo, Sam - OK, I just uploaded 0.4.3.0 with various bug fixes + the UIFactory feature. I also fixed the generation of float/double literals.

Yaip - sorry script# only supports c#.

Rob

Posted on 9/20/2007 @ 3:02 AM
Great sample, but can only get to run if i set enabledebugging to true in the scriptlet control.
I observe flickering as the carousel rotates - I assume due to stopping and restarting the storyboards. Is there anything we can do to lessen the effect of the flickering?

Rob

Posted on 9/20/2007 @ 3:07 AM
Forget the comment about having to set enabledebugging to true - i was working with the original sample before it was fixed.

Nikhil Kothari

Posted on 9/20/2007 @ 6:52 AM
Rob, I don't think the flickering is because of starting/stopping storyboards. Stopping/starting is already done by slowing down/speeding up rather than just abruptly stopping/starting. I've seen the flickering happen on occasion when the CPU is already busy, and while the carousel is animating.

The idea I have which I haven't tried yet is to turn the animation computation to be based on elapsed time. Right now on every frame the carousel animates by a fixed amount. This depends on frame rate, which may go up/down based on CPU cycles. Instead if on every frame update the animation looks at time elapsed and computes the angle to animate by, then it might lead to the desired result. This is certainly possible. In fact the javascript animation framework in script# is already architected around the concept elapsed-time, and the same concepts could be applied here.

Glenn Block

Posted on 10/1/2007 @ 1:57 AM
Great sample Nikhil. Using Script# makes you forget you aren't using Silverlight 1.1. Now all we need is Script# + Silverlight + Facebook ;)

Jan

Posted on 10/5/2007 @ 1:12 AM
Another question,

I noticed it isn't possible to import other projects (because apperently those aren't script assemblies) that's a big bummer because my DAL is in that project.

Is there a work around so I can still acces that proejct? I already tried to use a webservice but no luck either, you can't import system.web ..

Hope to hear from you soon.

TIA!

Nikhil Kothari

Posted on 10/5/2007 @ 8:39 AM
Jan - its likely the Loaded event has already fired by the time you're subscribing to it.

On the other question, thats not happening. Script# isn't meant to convert arbitrary code into script. Its instead designed to create efficient script from code intended to be run in a script engine. In your particular scenario, you'd have your DAL on the server, create the right sort of service operations to access it, and you'd use HTTPRequest to issue requests to those service endpoints.

Jan

Posted on 10/7/2007 @ 11:41 PM
Oh ok, np, I've started the animation by changing my xaml code (putting the storyboard in the event.triggers part of my code).

And I've succeeded to use my data from my DAL! Just add the projects to the main website, and give your data to the scriptlet..

TADA! :D

John D

Posted on 11/20/2007 @ 10:06 AM
Hi Nikhil.

This has been a very useful tool for us migrating a project from Silverlight 1.1 to Silverlight 1.0, and allowing most of the software design to be preserved even if the details changed.

A new UIFactory method to make clipping shapes, particularly rectangles, would be great.

Many thanks.

wmwebtr ödüllü seo yarışması

Posted on 12/18/2007 @ 12:18 PM
SEO (Search Engine Optimization) yarışmasınının genel amacı "wmwebtr ödüllü seo yarışması" kelimesinde google da ilk üçe girebilmektir. İlk üçe girenlere sürpriz hediyeler var.

prmakinesi

Posted on 12/24/2007 @ 10:31 AM
thanks, nice

osmanli-tarih

Posted on 12/24/2007 @ 10:32 AM
wow, thans very much

Eksis kurutma

Posted on 12/26/2007 @ 12:08 PM
thank you my friend for your help.

Ashim

Posted on 1/16/2008 @ 10:40 PM
I am doing project in VS.net 2003 [asp.net web applications]..... I am not able to call the event MouseEnter and MouseLeave and all... wat to do????? I need it very urgently to complete my project....please help me...

Dijital Fotoğraf Makinesi

Posted on 2/11/2008 @ 9:58 AM
Rob, I don't think the flickering is because of starting/stopping storyboards. Stopping/starting is already done by slowing down/speeding up rather than just abruptly stopping/starting. I've seen the flickering happen on occasion when the CPU is already busy, and while the carousel is animating.

The idea I have which I haven't tried yet is to turn the animation computation to be based on elapsed time. Right now on every frame the carousel animates by a fixed amount. This depends on frame rate, which may go up/down based on CPU cycles. Instead if on every frame update the animation looks at time elapsed and computes the angle to animate by, then it might lead to the desired result. This is certainly possible. In fact the javascript animation framework in script# is already architected around the concept elapsed-time, and the same concepts could be applied here.

Eksis Kurutma Sistemleri

Posted on 2/13/2008 @ 8:59 AM
Thank you
www.kurutma.net

youtube

Posted on 3/4/2008 @ 4:22 PM
I am doing project in VS.net 2003 [asp.net web applications]..... I am not able to call the event MouseEnter and MouseLeave and all... wat to do????? I need it very urgently to complete my project..

ŞİİRLER

Posted on 3/4/2008 @ 4:23 PM

Jon

Posted on 3/17/2008 @ 1:39 PM
I am sure I'm overlooking it, but where can I slow the carousel down? Thx

Samuel

Posted on 4/25/2008 @ 6:15 AM
Hello,

when I download your project and I try to compile it, I have one error: the UIFactory can't be found. However, I have the Script# version 0.5.0.0 (latest).

Could you give me some help or tell me where I could download the version 0.4.3.0 ?

Thanks,

Sam

Jorge

Posted on 5/31/2008 @ 8:29 AM
Hello, I'm having the same problem as Samuel, does anyone know how can i fix this,
probably the problem would be resolved in version 0.4.3.0, but i'm already working
on VS 2008 so it would be nice to have it fixed here

PS: Great Application, by the way
The discussion on this post has been closed. Please use my contact form to provide comments.