Writing a custom control is a pretty good exercise if you're new to Silverlight or WPF. I originally wrote the LensPanel Flickr control for this blog in WPF and later ported it to Silverlight. In the process I delved into Storyboards (when to use them and more importantly when not to), Dependency Properties, layout, transforms, resources and everything in between. I recently refactored it with my brother in an international pairing exercise (he's in Ireland, I'm in Germany). It was slightly different from the traditional XP pairing process, but extremely effective none the less - we covered a lot of ground in a very short time. We both ended up with cosmetically different Controls (we weren't operating off the same source, just the same problem, with the same solutions applied).
Storyboards
Storyboards are used in two places, the fade up effect when an image loads (non-interactive), and for global animation when performing the manual calculations of size and position of the items based on the mouse position (also non-interactive, at least not in the traditional sense). The latter is the 'chained' Storyboard technique recommended for Game-type applications. Essentially it gives you a run loop.
Layout
Initially, I calculated the scale of each item and I let the base Panel class perform the layout pass, but this turns out to be a bad idea. What happens is the items to the left and right of the lens wobble slightly as you move across the control because of width changes in the items that are scaling under the influence of the lens. There's also an issue of centering the items - you don't always want to, like when the mouse is on the far left or right of the item group. It depends on how much you're willing to accept in terms of visible artifacts. For me, the artifacts were too jarring so I went in and calculated layout manually.
ItemsControl, Templates and ItemsSource
The ItemsControl is the glue that holds everything together, and takes care of injecting the control with items harvested from a custom provider, FlickrPhotoProvider, which is derived from ObservableCollection. It also allows specification of templates for the Panel itself and the Items that populate it. Providers are ideal for a control that gets populated based on, in this case, a dialogue with Flickr's API. Here's what the xaml looks like:
1: <UserControl x:Class="LensPanel.Page"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:LensPanel;assembly=LensPanel"
5: Height="135">
6: <UserControl.Resources>
7: <local:FlickrPhotoProvider x:Key="FlickrPhotoProvider" NumberOfItems="12" />
8: </UserControl.Resources>
9: <Grid>
10: <ItemsControl x:Name="icLensPanel" ItemsSource="{StaticResource FlickrPhotoProvider}">
11: <ItemsControl.ItemsPanel>
12: <ItemsPanelTemplate>
13: <local:LensPanel ElementWidth="50" ElementHeight="100" ElementRenderTransformOriginX="0" ElementRenderTransformOriginY=".5" ElementSelectableHeightFactor=".5">
14: <local:LensPanel.Background>
15: <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
16: <GradientStop Color="#FF333333" Offset=".54"/>
17: <GradientStop Color="#FF383838" Offset=".68"/>
18: <GradientStop Color="#FF303030" Offset=".70"/>
19: <GradientStop Color="#FF333333" Offset=".90"/>
20: </LinearGradientBrush>
21: </local:LensPanel.Background>
22: </local:LensPanel>
23: </ItemsPanelTemplate>
24: </ItemsControl.ItemsPanel>
25: <ItemsControl.ItemTemplate>
26: <DataTemplate>
27: <local:LensPanelReflectedItem ImageUri="{Binding ImageFarmUrl}" NavigateUri="{Binding ImagePageUrl}" />
28: </DataTemplate>
29: </ItemsControl.ItemTemplate>
30: </ItemsControl>
31: <TextBlock x:Name="uxError" Text="" Foreground="Silver" HorizontalAlignment="Center" VerticalAlignment="Bottom" />
32: </Grid>
33: </UserControl>
This gives you a clean separation of interests and dependency injection. The LensPanel and LensPanelRelfectedItem have no idea about any of the Flickr code, and vice versa.
Quick and Dirty Reflections
Silverlight doesn't have a VisualBrush. This makes typical effects a little more challenging in Silverlight, but not impossible.
There are two options. You can either attempt to procedurally create the reflections in code by cloning and flipping the elements, which is a lot of relatively heavy lifting on your part, or you can take the quick and dirty option and flesh it out in pure xaml.
I opted for the xaml approach as a quick way to prototype the method, but it worked so well, I left is as it is.
Each item in the LensPanel is a Grid with two rows. The Grid houses the Image and its' reflection underneath. In fact the Grid houses two Grid container objects, each of which contains an image. The reason for the Grid container is to provide an initial background, that the images can be faded onto when they load.
Here's a snapshot of the xaml for the LensPanelReflectedItem.
1: <UserControl x:Class="LensPanel.LensPanelReflectedItem"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:LensPanel;assembly=LensPanel">
5: <UserControl.Resources>
6: <Style x:Key="SubjectImageStyle" TargetType="Image">
7: <Setter Property="VerticalAlignment" Value="Center" />
8: <Setter Property="HorizontalAlignment" Value="Center" />
9: <Setter Property="Stretch" Value="UniformToFill" />
10: <Setter Property="Opacity" Value="0" />
11: </Style>
12: <Style x:Key="SubjectImageGridStyle" TargetType="Grid">
13: <Setter Property="Background" Value="#515151" />
14: </Style>
15: <Style x:Key="ReflectionImageGridStyle" TargetType="Grid">
16: <Setter Property="Background" Value="#515151" />
17: <Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
18: <Setter Property="RenderTransform">
19: <Setter.Value>
20: <TransformGroup>
21: <ScaleTransform ScaleY="-1"/>
22: </TransformGroup>
23: </Setter.Value>
24: </Setter>
25: <Setter Property="OpacityMask">
26: <Setter.Value>
27: <LinearGradientBrush EndPoint="0,0" StartPoint="0,1">
28: <LinearGradientBrush.GradientStops>
29: <GradientStop Color="#55FFFFFF" Offset="0"/>
30: <GradientStop Color="#00FFFFFF" Offset=".5"/>
31: </LinearGradientBrush.GradientStops>
32: </LinearGradientBrush>
33: </Setter.Value>
34: </Setter>
35: </Style>
36: <Storyboard x:Name="fadeUp" BeginTime="00:00:00.00" Duration="00:00:01.00" >
37: <DoubleAnimation Storyboard.TargetName="imgSubjectItem" Storyboard.TargetProperty="Opacity" To="1"/>
38: <DoubleAnimation Storyboard.TargetName="imgReflectionItem" Storyboard.TargetProperty="Opacity" To="1"/>
39: </Storyboard>
40: </UserControl.Resources>
41: <Grid Margin="1" >
42: <Grid.RowDefinitions>
43: <RowDefinition Height="*"/>
44: <RowDefinition Height="*"/>
45: </Grid.RowDefinitions>
46: <Grid Style="{StaticResource SubjectImageGridStyle}" MouseLeftButtonUp="LensPanelReflectedItem_MouseLeftButtonUp">
47: <Image x:Name="imgSubjectItem" Style="{StaticResource SubjectImageStyle}"/>
48: </Grid>
49: <Grid Grid.Row="1" Style="{StaticResource ReflectionImageGridStyle}">
50: <Image x:Name="imgReflectionItem" Style="{StaticResource SubjectImageStyle}"/>
51: </Grid>
52: </Grid>
53: </UserControl>
The upper Grid is pretty self explanatory, and holds the main image. The reflection Grid holds the same image, and has a render transform applied to flip it. The reflection Grid also has an Opacity Map which effectively fades the reflection out. At no point are the dimensions of the Item specified - it's up to the LensPanel Control to set the dimensions.
There's a little duplication in there. Right now, Silverlight doesn't support BasedOn styles, so there's not a huge amount you can do about it.
You can see the finished product on the top of this page, provided you're viewing on a system that Silverlight can run on. If the page detects it's on a non-Silverlight platform, it collapses the Silverlight HTML object, so you don't see the "Install Silverlight" button.