Introduction
I recently set myself the task of developing two versions of a simple coverflow control - one in Silverlight and one in JavaScript. In this article I will give a high level overview of the two implementations and discuss some of the similarities and differences I experienced while developing with the two technologies. It won't go into a lot of the details of the code. If you want to delve further into the code feel free to download the complete source for both implementations. The project is also hosted on github.
You can see the two controls in action here:
A caveat: The decision to develop the JavaScript version only for WebKit browsers enabled some short cuts which perhaps makes any comparison a little unfair. Obviously, to complete a more cross-browser friendly version requires more effort, whereas the Silverlight version should work in any browser that has an appropriate plug-in available. This needs to be kept in mind while comparing the code.
Contents
Implementation Overview
Both implementations take similar approaches. They start out with a collection of images laid out horizontally.
The "current" item is then scaled to bring it to the forefront, and its neighbouring items are scaled and rotated as shown below. All other items are hidden.
A reflection is added to give the impression that the items are standing on a glass surface, rather than floating in air!
The items are then moved horizontally to keep the current item central.
Finally, all of the above scaling, rotating and horizontal adjustment is animated to give the feeling of flipping through the items as the "current" item changes.
The Basic Layout
At the core of each implementation is a collection of items which is used to build up the UI.
JavaScript Version
For the JavaScript application I took the jQuery plugin approach. The collection of items is passed in to the plugin as an array in settings.items
.
$.fn.coverFlow = function (settings) {
settings = $.extend({}, $.fn.coverFlow.defaultSettings, settings || {});
return this.each(function () {
$.tmpl(settings.template, settings).appendTo(this);
$("#coverFlowItems").width(settings.itemSize * settings.items.length);
...
});
};
It uses jQuery templates to render the items. $.fn.coverFlow.defaultSettings
provides defaults for the template
and itemSize
, so these can be omitted when calling the plugin or added to the settings
object to override the defaults.
The default template renders the items as an unordered list, as shown below.
<ul id='coverFlowItems'>
{{each $data.items}}
<li>
<a href='${$value.url}' tabindex='-1'>
<img src="${$value.image}"
width='${$data.itemSize}'
height='${$data.itemSize}' />
</a>
</li>
{{/each}}
</ul>
Then it just needs some css to lay out the items horizontally...
#coverFlowItems > li {
display: inline-block;
...
}</
Silverlight Version
In the Silverlight version, the layout of the items is provided by an ItemsControl
configured to use a horizontal StackPanel
for its layout panel. The display of each item is delegated to a DataTemplate
containing a UserControl
of type CoverFlowItemView
.
...
<ItemsControl ItemsSource="{Binding CoverFlowItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:CoverFlowItemView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
...
Using the Model-View-ViewModel pattern, the DataContext
of the control is set to a view model object holding the collection of items in an ObservableCollection
property called CoverFlowItems
. The ItemsSource
of the ItemsControl
is bound to this property.
The CoverFlowItemView
user control defines the display of each item in a similar way to the jQuery template - an image with a hyperlink.
<UserControl x:Class="CoverFlow.View.CoverFlowItemView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
...
</UserControl.Resources>
...
<HyperlinkButton NavigateUri="{Binding Url}">
<Image Source="{Binding Image}"
Style="{StaticResource coverFlowImageStyle}"/>
</HyperlinkButton>
...
</UserControl>
Comparisons & Observations
An interesting aspect of the code I've shown so far is the impact of the differing type systems on the two implementations.
The jQuery plugin makes use of JavaScript's 'Duck Typing' - if the items passed in 'walk like a coverflow item and swim like a coverflow item and quack like a coverflow item, we can call them coverflow items!' In other words, the plugin will be happy with any objects passed in as long as they have the expected image
and url
properties. In the Silverlight version, this requirement is explicitly defined in the CoverFlowItemViewModel
type with its properties for Image
and Url
.
public class CoverFlowItemViewModel : INotifyPropertyChanged
{
...
private readonly string url;
private readonly string image;
public CoverFlowItemViewModel(string url, string image)
{
this.url = url;
this.image = image;
}
public string Url { get { return url; } }
public string Image { get { return image; } }
...
}
The relative advantages & disadvantages of static vs. dynamic type systems have been hotly debated. In practice, it comes down to a choice based on trade-offs as to which best suits the kind of application being developed.
In general, statically typed languages can provide better tooling support like auto-completion, code navigation, refactorings, etc., which can be a big productivity boost during development. Also, compile time error messages can help catch mistakes sooner.
On the other hand, the reduced edit-compile-test-debug cycle with dynamic languages can provide a more streamlined feedback & debugging experience.
For an exercise of this nature, the static type checks and better tooling support available when developing the Silverlight version didn't seem to have as big an impact as they might on a larger more complex project. However, the streamlined feedback & debugging experience of the JavaScript development did make a big difference.
Being able to change some JavaScript and simply refresh the browser to see the effect of the changes is a big advantage. Also, using the in-browser debugging tools to examine the state of elements and experiment with different values for their attributes while the UI updates 'on the fly' helped to quickly understand & solve many problems.
I ended up using this to experiment with the appearance of the items in the JavaScript version and then use what I learned to make the same appearance in the Silverlight version.
Transforming the items
Once we've got the collection of items in a basic layout, the bulk of the rest of the code is concerned with transforming those items into the coverflow formation and animating this as the current item changes.
JavaScript Version
In the JavaScript version, the responsibility of the code is mainly to add and remove class names to the relevant elements in response to the current item changing. In the following code snippet _items
is a collection of jQuery wrapped objects representing the item DOM elements, i.e., $("#coverFlowItems li")
.
$(this._items[oldIndex - 2]).removeClass("left-2");
$(this._items[oldIndex - 1]).removeClass("left-1");
$(this._items[oldIndex]).removeClass("active");
$(this._items[oldIndex + 1]).removeClass("right-1");
$(this._items[oldIndex + 2]).removeClass("right-2");
$(this._items[this._currentIndex - 2]).addClass("left-2");
$(this._items[this._currentIndex - 1]).addClass("left-1");
$(this._items[this._currentIndex]).addClass("active");
$(this._items[this._currentIndex + 1]).addClass("right-1");
$(this._items[this._currentIndex + 2]).addClass("right-2");
The transforms themselves are defined in css.
#coverFlowItems > li.active {
z-index: 2;
opacity: 1;
-webkit-transform: scale(2);
}
#coverFlowItems > li.left-1 {
z-index: 1;
opacity: 1;
-webkit-transform: scale(1.75) rotateY(60deg);
}
#coverFlowItems > li.right-1 {
z-index: 1;
opacity: 1;
-webkit-transform: scale(1.75) rotateY(-60deg);
}
#coverFlowItems > li.left-2 {
opacity: 0.8;
-webkit-transform: scale(1.5) rotateY(60deg);
}
#coverFlowItems > li.right-2 {
opacity: 0.8;
-webkit-transform: scale(1.5) rotateY(-60deg);
}
The animation is also defined in the css.
#coverFlowItems > li {
...
-webkit-transition: all 0.5s;
}
Silverlight Version
To create the same effect in the
Silverlight version I made use of the VisualState class to define the appearance of the items in each specific state.
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Left_2">
<Storyboard TargetName="itemPanel">
<DoubleAnimation
To="0.8"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.Opacity)" />
<DoubleAnimation
To="-60"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.Projection).(RotationY)" />
<DoubleAnimation
To="-0"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.Projection)
.(CenterOfRotationX)" />
<DoubleAnimation
To="1.5"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.RenderTransform)
.Children[0].ScaleX" />
<DoubleAnimation
To="1.5"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.RenderTransform)
.Children[0].ScaleY" />
</Storyboard>
</VisualState>
<VisualState x:Name="Left_1">
...
</VisualState>
<VisualState x:Name="Current">
...
</VisualState>
<VisualState x:Name="Right_1">
...
</VisualState>
<VisualState x:Name="Right_2">
...
</VisualState>
<VisualState x:Name="Hidden">
...
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
I created an attached property to allow the visual state of each item to be bound to a view model property.
public static readonly DependencyProperty VisualStateProperty =
DependencyProperty.RegisterAttached("VisualState", typeof(string),
typeof(CoverFlowItemView), new PropertyMetadata(VisualStateChanged));
public static string GetVisualState(DependencyObject target)
{
return (string)target.GetValue(VisualStateProperty);
}
public static void SetVisualState(DependencyObject target, string value)
{
target.SetValue(VisualStateProperty, value);
}
private static void VisualStateChanged(object sender,
DependencyPropertyChangedEventArgs args)
{
var newState = (string)args.NewValue;
if (!string.IsNullOrWhiteSpace(newState))
{
...
VisualStateManager.GoToState(control, newState, true);
}
}
The binding between the CoverFlowItemView
and the CoverFlowItemViewModel
looks like this...
<UserControl x:Class="CoverFlow.View.CoverFlowItemView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CoverFlow.View"
local:CoverFlowItemView.VisualState="{Binding VisualState}">
public class CoverFlowItemViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
...
private string visualState = "Hidden";
public string VisualState
{
get { return visualState; }
set
{
if (VisualState == value)
{
return;
}
visualState = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("VisualState"));
}
}
}
}
So when the current item changes the items can be transformed by updating their VisualState appropriately, in a similar way to the JavaScript version, for example...
public void NextItem()
{
SetItemVisualState(CurrentItemIndex - 2, "Hidden");
SetItemVisualState(CurrentItemIndex - 1, "Left_2");
SetItemVisualState(CurrentItemIndex, "Left_1");
SetItemVisualState(CurrentItemIndex + 1, "Current");
SetItemVisualState(CurrentItemIndex + 2, "Right_1");
SetItemVisualState(CurrentItemIndex + 3, "Right_2");
CurrentItemIndex++;
}
private void SetItemVisualState(int itemIndex, string visualState)
{
if (IsValidIndex(itemIndex))
{
CoverFlowItems[itemIndex].VisualState = visualState;
}
}
Comparisons & Observations
Conceptually the two implementations are quite similar. They are both based around a separation between the logic that determines the current state of each item and the definition of what that state should look like.
There can be no denying though that the Silverlight implementation is more verbose. Also, the paths in those Storyboard.TargetProperty
settings (like the one shown below) took quite a bit of trial and error to get right and are tricky to debug when they're not working as expected!
Storyboard.TargetProperty="(UIElement.RenderTransform).Children[0].ScaleX"
As mentioned at the beginning of the article though, it is largely down to those transform definitions in the css that make the JavaScript version WebKit specific.
-webkit-transform: scale(1.75) rotateY(60deg);
A lot of the vendor specific properties have equivalents in each of the main browsers, but they're not standard and not guaranteed to work the same way across browsers. (In fact, they're not guaranteed to work the same way in future releases of the same browser!)
A quick test in Firefox after replacing all -webkit-
prefixes with -moz-
, didn't quite work as expected so clearly there's more work required than just repeating the properties with different vendor prefixes!
Some welcome these vendor-specific properties as accelerating CSS development; others consider them to be harmful to web standards. Either way, an overriding advantage of the Silverlight version is that, having gone to the effort of implementing that more verbose solution, we can expect it to work in any browser that has a Silverlight plug-in available.
The Silverlight version runs in the major browsers...
Conclusion
I have given quite a high-level overview here and discussed a couple of areas where there were interesting similarities or differences between the two implementations. There are some details in both versions that have been ignored for the sake of the article. The complete source is available if you want to look at the code in more detail.
If I was backed into a corner and forced to draw a conclusion from these comparisons I would have to say that, for this exercise, it seems the flexibility and dynamism of the JavaScript language led to a more succinct solution than the Silverlight version, and the in-browser debugging tools were a big time saver. The static type checks and other tooling available with Silverlight development (and the lack of them in the JavaScript development environment) didn't seem to make as big an impact as I expected. However, we can't ignore the fact that the Silverlight version provides a much wider browser coverage for no extra effort whereas there is still some work to be done in that respect with the JavaScript version.
Of course, it isn't difficult to imagine a scenario where the impact of these differences would be reversed - i.e., where the type checking and tooling available with Silverlight development would become much more important.
A better way to look at the differences is to think of them as choices based on trade-offs and considered in the context of the type of application being developed, rather than an overall 'which is best' decision.
Finally, I can recommend trying this kind of exercise yourself (developing a simple application in two technologies and comparing the experience). It can be a useful tool to help understand these trade-offs and gain a better idea of the impact they might have on the kinds of applications you work with.