Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF: Carousel Control

0.00/5 (No votes)
18 Apr 2011 1  
Highly customisable WPF Carousel control.

Introduction

A while ago (around Xmas) Marcelo Ricardo de Oliveira and I were working on a joint article to create a custom Carousel, you know the sort of thing you see a lot in popular component manufacturer's control sets. Thing was, we very nearly finished our own hand cranked solution, and then I remembered the PathListBox which I had played with in the past.

So I did a bit of Googling to see what people had done with the PathListBox and to my surprise, there was a CodePlex offering from Microsoft that seemed to get you half way to being able to implement a Carousel using the PathListBox. There are a number of classes that Microsoft has come out with to help us lowly developers, which come in the form of a bunch of DLLs which are known as the PathListBoxUtils, which are part of the Expression Blend samples over at CodePlex: http://expressionblend.codeplex.com/releases/view/44915.

The PathListBoxUtils includes the following classes that can be used:

  • PathListBoxScrollBehavior
  • PathListBoxItemTransformer
  • AdjustedOffsetToRadiusConverter
  • PathListBoxExtensions

This is all well and good, but what Marcelo and I wanted to do was to offer the end user as much customization as possible, you know allow things like:

  • Be able to use one of several inbuilt paths, or pick your own animation path (by drawing a path in Blend)
  • Be able to change the navigation button Styles/Positions
  • Be able to use different DataTemplates
  • Be able to use different Easing animation functions

Basically, we wanted to be able to come up with a "Carousel control", which simply exposed the relevant customization properties that the user could set.

Like I said, the PathListBoxUtils gets you some of the way there, but not all the way, so what was called for is a wrapper. This article is essentially a wrapper for a PathListBox which allows it to be treated as a "Carousel control", that Marcelo and I dreamt of and worked so hard to achieve, even though we have now dismissed our code in favour of this Microsoft vetted (are more than likely more universally accepted) code.

It was a good journey while it lasted, hey Marcelo, nothing ventured nothing gained, and all that.

One Thing of Note Before We Start

I just wanted to note one thing before we start diving right into the nitty gritty, which in this case is the fact that since I am pretty much just supplying a wrapper, where the original animation code is as supplied by the Expression Blend team, any queries regarding animation will need to directed at the Blend team and more specifically the PathListBoxUtils CodePlex site. Which I have to say I do not like, I normally like to get the blame for my work, it's just this time it seemed to be, there was a solution out there which did what needed to be done, and just needed generalizing to make it more end user friendly.

Ok now that, that is out of the way lets proceed.

Table of Contents

Anyway, what I am going to cover in this article is as follows:

Video

I think the best way to see what is possible is to view the video using the link below:

Click the image above to play the video

Images

But if you just can't be bothered to watch the video (or can't at work), here are some screenshots:

Using a custom animation path

Using inbuilt "Ring" animation path

Using inbuilt "Diagonal" animation path

Adjust number of items of path

Supply different template style

Swap out the default navigation button styles for your own (you can swap left and right independent of each other).

So there we go, that gives you a taste of what this wrapper allows. Now, let's carry on to see how it all works.

How it Works

As I stated throughout this article, most of the clever stuff is actually done via the use of the PathListBoxUtils DLLs. As such, I think one of the best places to start is to explain all of the PathListBoxUtils classes. I know of no better way than to use the author's original text which is shown below.

PathListBoxScrollBehavior

This behavior provides smooth scrolling for your PathListBox. It exposes three commands that apply a smooth scroll while changing the StartItemIndex of a PathListBox control.

You can either scroll a certain number of items with the Increment and Decrement commands, or you can scroll the selected item to a relative position on your path with the ScrollSelected command.

Commands

IncrementCommand

Gets a command that increments the StartItemIndex by Amount.

DecrementCommand

Gets a command that decrements the StartItemIndex by Amount.

ScrollSelectedCommand

Gets a command that scrolls the selected item to the same position as the item that is closest to the DesiredOffset.

Properties

Amount

Gets or sets the amount to increment or decrement the StartItemIndex.

Duration

Gets or sets the duration of the scroll.

Ease

Gets or sets the easing function to use when scrolling.

DesiredOffset

Gets or sets the offset from 0 to 1 along the layout paths associated with this PathListBox to scroll the selected item.

HideEnteringItem

Gets or sets whether to hide the item that will be newly arranged on the path when the StartItemIndex is changed. Use this when scrolling on an open path.

PathListBoxItemTransformer

This is a custom content control that allows you to modify the opacity, scale, and rotation of a PathListBoxItem based on where it is positioned on the layout path of the PathListBox. Set ScaleRange, OpacityRange, and AngleRange to the minimum and maximum values you would like applied. The final values are adjusted based on the settings of the Ease, Shift, and IsCentered properties. If Ease is set to None, a linear interpolation is used. To adjust where the effective start and end occurs along the path, adjust the Shift property. Enabling the IsCentered property will adjust the offset evenly from the middle of the path, as shown in the example above. If your PathListBox has multiple LayoutPaths assigned, you can enable the UseGlobalOffset property to indicate that the values should be computed across all paths instead of per individual path.

Properties

OpacityRange

Gets or sets the range of values to use for the opacity.

ScaleRange

Gets or sets the range of values to use for the scale.

AngleRange

Gets or sets the range of angle values to add to the rotation.

Ease

Gets or sets the easing function that is used to adjust the offset.

Shift

Gets or sets the amount to shift the offset from the beginning of the path.

IsCentered

Gets or sets whether to adjust the offset evenly from the center of the path.

UseGlobalOffset

Gets or sets whether to adjust the offset across all paths by using GlobalOffset instead of LocalOffset as the starting offset for the item.

AdjustedOffsetToRadiusConverter

This converter can be used to data bind the AdjustedOffset property of a PathListItemTransformer to the Radius value for a Blur effect. The parameter can be used as a scaling factor to control the maximum level of blur. This is the value computed from the offset adjusted by the IsCentered, Ease, and Shift properties. In the carousel example above, it is used as the converter for data binding the Radius property of a Blur effect on the ContentPresenter in the ItemContainerStyle of the PathListBox.

PathListBoxExtensions

This class exposes several extension methods to a PathListBox control.

int GetItemsArrangedCount(int layoutPathIndex)

Determines the number of items currently arranged on the specified layout path.

int GetFirstArrangedIndex(int layoutPathIndex)

Finds the item that is laid out at the beginning of the specified layout path.

int GetLastArrangedIndex(int layoutPathIndex)

Finds the item that is laid out at the end of the specified layout path.

-- http://expressionblend.codeplex.com/wikipage?title=PathListBoxUtils up on date 14/04/2011.

So that is how the PathListBoxUtils classes work, but what does that look like in XAML, how do we use it all together? Well, this is basically all the XAML for the CarouselControl:

<UserControl x:Class="Carousel.CarouselControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ec="http://schemas.microsoft.com/expression/2010/controls" 
        xmlns:PathListBoxUtils="clr-namespace:Expression.Samples.PathListBoxUtils;
        assembly=Expression.Samples.PathListBoxUtils" 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:local="clr-namespace:Carousel"
        mc:Ignorable="d"
        d:DesignHeight="300" d:DesignWidth="300">


<UserControl.Resources>

    <!-- Defualt Previous Button style-->
    <Style x:Key="navigatorPreviousButtonStyle" TargetType="{x:Type Button}">
        <Setter Property="Padding" Value="0"/>
        <Setter Property="Margin" Value="5"/>
        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Viewbox  Width="40"
                          Height="40">
                        <Image x:Name="img" Source="Images/previous.png"
                           Margin="{TemplateBinding Padding}" Opacity="0.5"
                           Stretch="Uniform">
                        </Image>
                    </Viewbox>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver"
                             Value="True">
                            <Setter Property="Effect">
                                <Setter.Value>
                                    <DropShadowEffect Color="Red"
                                                  ShadowDepth="2"
                                                  Direction="315"
                                                  Opacity="0.5" />
                                </Setter.Value>
                            </Setter>
                            <Setter TargetName="img"
                                Property="Opacity"
                                Value="1.0" />
                        </Trigger>
                        <Trigger Property="IsEnabled"
                             Value="False">
                            <Setter TargetName="img"
                                Property="Opacity"
                                Value="0.3" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- Defualt Next Button style-->
    <Style x:Key="navigatorNextButtonStyle" TargetType="{x:Type Button}">
        <Setter Property="Padding" Value="0"/>
        <Setter Property="Margin" Value="5"/>
        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Viewbox  Width="40"
                          Height="40">
                        <Image x:Name="img" Source="Images/next.png"
                           Margin="{TemplateBinding Padding}" Opacity="0.5"
                           Stretch="Uniform">
                        </Image>
                    </Viewbox>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver"
                             Value="True">
                            <Setter Property="Effect">
                                <Setter.Value>
                                    <DropShadowEffect Color="Red"
                                                  ShadowDepth="2"
                                                  Direction="315"
                                                  Opacity="0.5" />
                                </Setter.Value>
                            </Setter>
                            <Setter TargetName="img"
                                Property="Opacity"
                                Value="1.0" />
                        </Trigger>
                        <Trigger Property="IsEnabled"
                             Value="False">
                            <Setter TargetName="img"
                                Property="Opacity"
                                Value="0.3" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- PathlistBox Paath Converter-->
    <PathListBoxUtils:AdjustedOffsetToRadiusConverter 
        x:Key="AdjustedOffsetToRadiusConverter"/>

    <!-- PathListBox Style-->
    <Style x:Key="PathListBoxItemStyle" TargetType="{x:Type ec:PathListBoxItem}">
        <Setter Property="HorizontalContentAlignment" Value="Left"/>
        <Setter Property="VerticalContentAlignment" Value="Top"/>
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ec:PathListBoxItem}">
                    <Grid Background="{TemplateBinding Background}" 
                            RenderTransformOrigin="0.5,0.5">
                        <Grid.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform>
                                    <ScaleTransform.ScaleY>
                                        <Binding Path="IsArranged" 
                                              RelativeSource="{RelativeSource TemplatedParent}">
                                            <Binding.Converter>
                                                <ec:IsArrangedToScaleConverter/>
                                            </Binding.Converter>
                                        </Binding>
                                    </ScaleTransform.ScaleY>
                                    <ScaleTransform.ScaleX>
                                        <Binding Path="IsArranged" 
                                              RelativeSource="{RelativeSource TemplatedParent}">
                                            <Binding.Converter>
                                                <ec:IsArrangedToScaleConverter/>
                                            </Binding.Converter>
                                        </Binding>
                                    </ScaleTransform.ScaleX>
                                </ScaleTransform>
                                <SkewTransform/>
                                <RotateTransform Angle="{Binding OrientationAngle, 
                                    RelativeSource={RelativeSource TemplatedParent}}"/>
                                <TranslateTransform/>
                            </TransformGroup>
                        </Grid.RenderTransform>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="MouseOver"/>
                                <VisualState x:Name="Disabled"/>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="SelectionStates">
                                <VisualState x:Name="Unselected"/>
                                <VisualState x:Name="Selected"/>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="FocusStates">
                                <VisualState x:Name="Focused"/>
                                <VisualState x:Name="Unfocused"/>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <PathListBoxUtils:PathListBoxItemTransformer 
                                x:Name="pathListBoxItemTransformer"
                                Loaded="PathListBoxItemTransformer_Loaded"
                                VerticalAlignment="Top" 
                                d:LayoutOverrides="Width" 
                                IsCentered="True">
                            <PathListBoxUtils:PathListBoxItemTransformer.Ease>
                                <SineEase EasingMode="EaseIn"/>
                            </PathListBoxUtils:PathListBoxItemTransformer.Ease>
                            <Grid x:Name="TransformerParentGrid" Height="Auto">
                                <Rectangle x:Name="FocusVisualElement" RadiusY="1" 
                                           RadiusX="1" Stroke="#FF6DBDD1" 
                                           StrokeThickness="1" 
                                           Visibility="Collapsed"/>
                                <ContentPresenter x:Name="contentPresenter" 
                                      ContentTemplate="{TemplateBinding ContentTemplate}" 
                                      Content="{TemplateBinding Content}" 
                                      HorizontalAlignment=
                                        "{TemplateBinding HorizontalContentAlignment}" 
                                      Margin="{TemplateBinding Padding}">
                                </ContentPresenter>
                                <Rectangle x:Name="fillColor" Fill="#FFBADDE9" 
                                           IsHitTestVisible="False" 
                                           Opacity="0" RadiusY="1" 
                                           RadiusX="1"/>
                                <Rectangle x:Name="fillColor2" Fill="#FFBADDE9" 
                                           IsHitTestVisible="False" 
                                           Opacity="0" RadiusY="1" 
                                           RadiusX="1"/>
                            </Grid>
                        </PathListBoxUtils:PathListBoxItemTransformer>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
        
</UserControl.Resources>

<Grid Margin="20">
    <ec:PathListBox x:Name="pathListBox" Margin="0" WrapItems="True"
                SelectionMode="Single"
                ItemContainerStyle="{DynamicResource PathListBoxItemStyle}">
        <ec:PathListBox.LayoutPaths>
            <ec:LayoutPath SourceElement="{Binding ElementName=ell}" 
                           Distribution="Even" Capacity="7" Start="0.01" 
                           FillBehavior="NoOverlap"/>
        </ec:PathListBox.LayoutPaths>
        <i:Interaction.Behaviors>
            <PathListBoxUtils:PathListBoxScrollBehavior 
                    DesiredOffset="0.5" 
                    HideEnteringItem="False">
                <PathListBoxUtils:PathListBoxScrollBehavior.Ease>
                    <SineEase EasingMode="EaseOut" />
                </PathListBoxUtils:PathListBoxScrollBehavior.Ease>
                <i:Interaction.Triggers>
                    <i:EventTrigger SourceName="pathListBox" 
                                    SourceObject="{Binding ElementName=previousButton}" 
                                    EventName="Click">
                        <i:InvokeCommandAction CommandName="DecrementCommand"/>
                    </i:EventTrigger>
                    <i:EventTrigger SourceName="pathListBox" 
                                    SourceObject="{Binding ElementName=nextButton}" 
                                    EventName="Click">
                        <i:InvokeCommandAction CommandName="IncrementCommand"/>
                    </i:EventTrigger>
                    <i:EventTrigger SourceName="pathListBox" 
                                    EventName="SelectionChanged">
                        <i:InvokeCommandAction CommandName="ScrollSelectedCommand"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </PathListBoxUtils:PathListBoxScrollBehavior>
        </i:Interaction.Behaviors>
    </ec:PathListBox>
        
    <StackPanel x:Name="spButtons" Orientation="Horizontal" 
                HorizontalAlignment="Center" 
                VerticalAlignment="Bottom" Margin="5">
        <Button x:Name="previousButton" Content="<" 
                Style="{StaticResource navigatorPreviousButtonStyle}" 
                Click="PreviousButton_Click"/>
        <Button x:Name="nextButton" Content=">" 
                Style="{StaticResource navigatorNextButtonStyle}" 
                Click="NextButton_Click"/>
    </StackPanel>

    <Grid x:Name="gridForKnownPaths" HorizontalAlignment="Stretch" 
                VerticalAlignment="Stretch">
        <Path x:Name="wavePath" 
           Data="M-45,335 C59,230 149,187......." 
           Stretch="Fill" Stroke="Transparent" 
           StrokeThickness="1"/>
        <Path x:Name="diagonalPath" 
          Data="M-43,120 L249,245.........." 
          Margin="-44,79,14,-31" Stretch="Fill" 
          Stroke="Transparent" StrokeThickness="1"/>
        <Path x:Name="zigzagPath" 
           Data="M-38,425 C74,254 -20.5,......" 
           Margin="-38,32.5,-26.5,16" Stretch="Fill" 
           Stroke="Transparent"  StrokeThickness="1"/>
        <Ellipse x:Name="ellipsePath" 
           Margin="40,32.5,82.5,106" 
           Stroke="Transparent" StrokeThickness="1"/>
    </Grid>

</Grid>

</UserControl>

There is a lot there, but we will be going through a lot of this in the rest of the article,

Customisation

Customisation is pretty much the heart of this article. As I said, what I am essentially presenting in this article is a wrapper for the PathListBox that makes use of the PathListBoxUtils helper classes. I also said that I wanted to make it as easy as possible for developers to create new and elegant Carousels by just setting a few properties, rather than digging right into the XAML. So to this end, a custom control CarouselControl was born that adds the following properties which allow customisation, which we will go through one by one below:

CarouselControl Dependency Property Dependency Property Type Description
CustomPathElement FrameworkElement A FrameworkElement which will be used as a custom path
NavigationButtonPosition ButtonPosition The position of the navigation buttons
PreviousButtonStyle Style The style for the previous navigation button
NextButtonStyle Style The style for the next navigation button
PathType PathType The PathType which is one of the PathType enum values
AnimationEaseIn EasingFunctionBase The easing in animation type to use
AnimationEaseOut EasingFunctionBase The easing out animation type to use
OpacityRange Point The opacity range for the items on the path
ScaleRange Point The scale range for the items on the path
AngleRange Point The angle range for the items on the path
DataTemplateToUse DataTemplate The DataTemplate to use to display the items
SelectedItem Object Sets the SelectedItem (there is one issue with this when used in MVVM fashion, see Important Notice About Animating to Bound SelectedItem details near the bottom of this article)
ItemsSource IEnumerable The source items collection to use
NumberOfItemsOnPath int The number of items to show on the path
MinNumberOfItemsOnPath int The minimum number of items to be allowed on path (bounds checking value for NumberOfItemsOnPath)
MaxNumberOfItemsOnPath int The maximum number of items to be allowed on path (bounds checking value for NumberOfItemsOnPath)

There is an entire demo dedicated to showcasing the customisation, which you will find in the attached code.

CustomPathElement

CarouselControl has a number of inbuilt paths that you can use, but you may also supply your own, which you typically do by having a path either in the same XAML where you use the CarouselControl or in a ResourceDictionary. Either way, you must have a Path element somewhere that you may use to set the CustomPath DependencyProperty of the CarouselControl, which internally looks like this:

/// <summary>
/// The FrameworkElement to use as custom path for Carousel
/// </summary>
public static readonly DependencyProperty CustomPathElementProperty =
    DependencyProperty.Register("CustomPathElement", 
        typeof(FrameworkElement), typeof(CarouselControl),
        new FrameworkPropertyMetadata((FrameworkElement)null,
            new PropertyChangedCallback(OnCustomPathElementChanged)));

public FrameworkElement CustomPathElement
{
    get { return (FrameworkElement)GetValue(CustomPathElementProperty); }
    set { SetValue(CustomPathElementProperty, value); }
}

private static void OnCustomPathElementChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
{
    ((CarouselControl)d).OnCustomPathElementChanged(e);
}

protected virtual void OnCustomPathElementChanged(DependencyPropertyChangedEventArgs e)
{
    if (e.NewValue != null)
        SetVisibilityForPath(PathType.Custom);
}

Where I have some XAML that looks like this (see the custom Path element there):

<Path x:Name="customPath" Data="M12,547.5........" 
      Stretch="Fill" Stroke="Transparent"/>
<Carousel:CarouselControl x:Name="CarouselControl" 
                        CustomPathElement="{Binding ElementName=customPath}"
                        SelectionChanged="SelectionChanged"/>

Which I can then set simply using a Binding, or grab it from a ResourceDictionary, or I could even (if I were brave) dynamically create a Path using some crazy Path generation math done in code.. Which will eventually fire this code internally in the CarouselControl, where the PathType will be set to Custom, thanks to the CarouselControl.CustomPathElement DepenendencyProperty changes callback calling it.

private void SetVisibilityForPath(PathType pathType)
{
    foreach (UIElement uiElement in gridForKnownPaths.Children)
    {
        uiElement.Visibility = Visibility.Collapsed;
    }

    switch (pathType)
    {
        case PathType.Ellipse:
            this.ellipsePath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.ellipsePath;
            break;
        case PathType.Wave:
            this.wavePath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.wavePath;
            break;
        case PathType.Diagonal:
            this.diagonalPath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.diagonalPath;
            break;
        case PathType.ZigZag:
            this.zigzagPath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.zigzagPath;
            break;
        case PathType.Custom:
            pathListBox.LayoutPaths[0].SourceElement = CustomPathElement;
            break;

    }
}

NavigationButtonPosition

Setting this property will locate the navigation buttons at various positions within the CarouselControl. You just need to set the CarouselControl to one of the following enum values:

public enum ButtonPosition
{
    TopLeft, TopCenter, TopRight, LeftCenter,
    RightCenter, BottomLeft, BottomCenter, BottomRight
};

Here is an example (note this could also be done via a Binding):

CarouselControl.NavigationButtonPosition = ButtonPosition.BottomCenter;

This will internally call this code inside the CarouselControl:

private void SetButtonPosition(ButtonPosition buttonPosition)
{
    switch (buttonPosition)
    {
        case ButtonPosition.TopLeft:
            spButtons.VerticalAlignment = VerticalAlignment.Top;
            spButtons.HorizontalAlignment = HorizontalAlignment.Left;
            spButtons.Orientation = System.Windows.Controls.Orientation.Horizontal;
            break;
        case ButtonPosition.TopCenter:
            spButtons.VerticalAlignment = VerticalAlignment.Top;
            spButtons.HorizontalAlignment = HorizontalAlignment.Center;
            spButtons.Orientation = System.Windows.Controls.Orientation.Horizontal;
            break;
        case ButtonPosition.TopRight:
            spButtons.VerticalAlignment = VerticalAlignment.Top;
            spButtons.HorizontalAlignment = HorizontalAlignment.Right;
            spButtons.Orientation = System.Windows.Controls.Orientation.Horizontal;
            break;
        case ButtonPosition.LeftCenter:
            spButtons.VerticalAlignment = VerticalAlignment.Center;
            spButtons.HorizontalAlignment = HorizontalAlignment.Left;
            spButtons.Orientation = System.Windows.Controls.Orientation.Vertical;
            break;
        case ButtonPosition.RightCenter:
            spButtons.VerticalAlignment = VerticalAlignment.Center;
            spButtons.HorizontalAlignment = HorizontalAlignment.Right;
            spButtons.Orientation = System.Windows.Controls.Orientation.Vertical;
            break;
        case ButtonPosition.BottomLeft:
            spButtons.VerticalAlignment = VerticalAlignment.Bottom;
            spButtons.HorizontalAlignment = HorizontalAlignment.Left;
            spButtons.Orientation = System.Windows.Controls.Orientation.Horizontal;
            break;
        case ButtonPosition.BottomCenter:
            spButtons.VerticalAlignment = VerticalAlignment.Bottom;
            spButtons.HorizontalAlignment = HorizontalAlignment.Center;
            spButtons.Orientation = System.Windows.Controls.Orientation.Horizontal;
            break;
        case ButtonPosition.BottomRight:
            spButtons.VerticalAlignment = VerticalAlignment.Bottom;
            spButtons.HorizontalAlignment = HorizontalAlignment.Right;
            spButtons.Orientation = System.Windows.Controls.Orientation.Horizontal;
            break;

    }
}

PreviousButtonStyle

The CarouselControl comes with a pre made Style for the two navigation buttons, but you can swap each of these out with your own Style just by setting the relevant CarouselControl DependencyProperty.

Here is an example (note: this could also be done via a Binding):

private Style blackPreviousButtonStyle =
    (Application.Current as App).Resources["BlackPreviousButtonStyle"] as Style;

CarouselControl.PreviousButtonStyle = blackPreviousButtonStyle;

Where we have a Style declared in a ResourceDictionary as follows:

<!-- Black Previous Button style-->
<Style x:Key="BlackPreviousButtonStyle" TargetType="{x:Type Button}">
    <Setter Property="Padding" Value="0"/>
    <Setter Property="Margin" Value="5"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Viewbox  Width="60"
                            Height="60">
                    <Image x:Name="img" 
                            Source="../Images/BlackArrowPrevious.png"
                            Margin="{TemplateBinding Padding}" 
                            Opacity="0.5"
                            Stretch="Uniform">
                    </Image>
                </Viewbox>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver"
                                Value="True">
                        <Setter Property="Effect">
                            <Setter.Value>
                                <DropShadowEffect ShadowDepth="0" 
                                    Color="White" BlurRadius="15" />
                            </Setter.Value>
                        </Setter>
                        <Setter TargetName="img"
                                Property="Opacity"
                                Value="1.0" />
                    </Trigger>
                    <Trigger Property="IsEnabled"
                                Value="False">
                        <Setter TargetName="img"
                                Property="Opacity"
                                Value="0.3" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

NextButtonStyle

As above, but this time, we would use the CarouselControl.NextButtonStyle DependencyProperty.

PathType

The CarouselControl has a number of inbuilt Paths that can be used such as Wave/Diagonal/ZigZag/Ellipse, and these can be used simply by setting the PathType to the matching enum value from the PathType enum shown below.

public enum PathType { Ellipse, Wave, Diagonal, ZigZag, Custom };

We already discussed what happens with the Custom one, but what about the other inbuilt ones? Well, what happens is that the following code is run internally to the CarouselControl which works out what Path to use based on what enum value was set on the PathType DependencyProperty.

private void SetVisibilityForPath(PathType pathType)
{
    foreach (UIElement uiElement in gridForKnownPaths.Children)
    {
        uiElement.Visibility = Visibility.Collapsed;
    }

    switch (pathType)
    {
        case PathType.Ellipse:
            this.ellipsePath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.ellipsePath;
            break;
        case PathType.Wave:
            this.wavePath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.wavePath;
            break;
        case PathType.Diagonal:
            this.diagonalPath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.diagonalPath;
            break;
        case PathType.ZigZag:
            this.zigzagPath.Visibility = Visibility.Visible;
            pathListBox.LayoutPaths[0].SourceElement = this.zigzagPath;
            break;
        case PathType.Custom:
            pathListBox.LayoutPaths[0].SourceElement = CustomPathElement;
            break;
    }
}

AnimationEaseIn

It is possible to set the type of animation easing that is used when easing new items in. This is achieved by using the CarouselControl.AnimationEaseIn DependencyProperty which can be set to any of the animation easing classes which derive from the easing base class EasingFunctionBase.

You can set the property to any one of the following EasingFunctionBase derived classes:

AnimationEaseOut

It is possible to set the type of animation easing that is used when easing old items out. This is achieved by using the CarouselControl.AnimationEaseOut DependencyProperty which can be set to any of the animation easing classes which derive from the easing base class EasingFunctionBase, as we just saw above.

OpacityRange

The items on the animation path can have an Opacity applied to them. The PathListBoxUtils allows this to be changed such that you could have outer items very see through (Opacity near to 0) and inner items very visible (Opacity near to 1). This is achieved using a Point struct. Basically, all I do is pass this value straight to the internal PathListBoxUtils PathListBoxItemTransformer object.

Here is what happens internally in the CarouselControl:

/// <summary>
/// OpacityRange to use for PathListBoxItemTransformer
/// </summary>
public static readonly DependencyProperty OpacityRangeProperty =
    DependencyProperty.Register("OpacityRange", typeof(Point), typeof(CarouselControl),
        new FrameworkPropertyMetadata((Point)new Point(0.7,1.0),
            new PropertyChangedCallback(OnOpacityRangeChanged)));

public Point OpacityRange
{
    get { return (Point)GetValue(OpacityRangeProperty); }
    set { SetValue(OpacityRangeProperty, value); }
}

/// <summary>
/// Handles changes to the OpacityRange property.
/// </summary>
private static void OnOpacityRangeChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
{
    ((CarouselControl)d).OnOpacityRangeChanged(e);
}

/// <summary>
/// Provides derived classes an opportunity to handle changes to the OpacityRange property.
/// </summary>
protected virtual void OnOpacityRangeChanged(DependencyPropertyChangedEventArgs e)
{
    foreach (PathListBoxItemTransformer pathListBoxItemTransformer in transformers)
    {
        pathListBoxItemTransformer.OpacityRange = (Point)e.NewValue;
    }
}

Which you can set from your code as follows:

CarouselControl.OpacityRange = new Point(0.4,1.0);

Or from a Binding:

<Carousel:CarouselControl OpacityRange="0.4,1.0"/>

ScaleRange

This works in much the same way as the OpacityRange, except this time we are talking the scaling of each item instead.

Which you can set from your code as follows:

CarouselControl.ScaleRange = new Point(0.4,1.0);

Or from a Binding:

<Carousel:CarouselControl ScaleRange="0.4,1.0"/>

AngleRange

This works in much the same way as the OpacityRange, except this time we are talking about the angles of each item instead.

Which you can set from your code as follows:

CarouselControl.AngleRange = new Point(10,45);

Or from a Binding:

<Carousel:CarouselControl ScaleRange="10,45"/>

DataTemplateToUse

The DataTemplateToUse DependencyProperty simply allows the internal PathListBox.ItemTemplate. Although this is a pretty bulk standard property, it does have a big impact on how your CarouselControl, by setting a new DataTemplate, will change how your items are displayed. For example:

Robot DataTemplate

Here is the Robot DataTemplate:

<!-- Robot DataTemplate -->
<DataTemplate x:Key="robotDataTemplate">
    <StackPanel Orientation="Vertical">
        <Image x:Name="img"
                HorizontalAlignment="Center" 
                VerticalAlignment="Center"
                Source="{Binding ImageUrl}" 
                Width="50" Height="50" 
                ToolTip="{Binding RobotName}" />
    </StackPanel>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding 
        RelativeSource={RelativeSource Mode=FindAncestor,
                AncestorType={x:Type ListBoxItem}},Path=IsSelected}" 
        Value="True">
            <Setter TargetName="img" Property="Effect">
                <Setter.Value>
                    <DropShadowEffect ShadowDepth="0" 
            Color="White" BlurRadius="15" />
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

Where the Robot data model looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CarouselWPFTestApp
{
    public class RobotData : INPCBase
    {
        private string robotName;
        private string imageUrl;

        public RobotData(string robotName, string imageUrl)
        {
            this.robotName = robotName;
            this.imageUrl = imageUrl;
        }


        public string RobotName
        {
            get { return robotName; }
            set
            {
                if (robotName != value)
                {
                    robotName = value;
                    NotifyPropertyChanged("RobotName");
                }
            }
        }

        public string ImageUrl
        {
            get { return imageUrl; }
            set
            {
                if (imageUrl != value)
                {
                    imageUrl = value;
                    NotifyPropertyChanged("ImageUrl");
                }
            }
        }

    }
}

Which looks like this:

Person DataTemplate

Here is the Person DataTemplate:

<!-- Person DataTemplate -->
<DataTemplate x:Key="personDataTemplate">

    <Border BorderBrush="#FFFF3204" x:Name="bord" 
           BorderThickness="2" CornerRadius="10">
        <Border.Background>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="Black" Offset="1"/>
                <GradientStop Color="#FF4E4E4E"/>
            </LinearGradientBrush>
        </Border.Background>
        <Grid>
            <StackPanel Margin="0">
                <StackPanel Orientation="Horizontal" 
                        HorizontalAlignment="Right">
                    <Label Content="{Binding Salutation}" 
                        FontSize="9" Foreground="Orange"
                        HorizontalAlignment="Right"
                        HorizontalContentAlignment="Right"
                        Style="{StaticResource personLabel}"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <Label Content="FirstName" 
                        Style="{StaticResource personLabel}"/>
                    <Label Content="{Binding FirstName}" 
                        Style="{StaticResource personLabel}"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <Label Content="LastName" 
                        Style="{StaticResource personLabel}"/>
                    <Label Content="{Binding LastName}" 
                        Style="{StaticResource personLabel}"/>
                </StackPanel>
            </StackPanel>
            <Ellipse Width="30" Height="30" Fill="Black" 
                        HorizontalAlignment="Left" Margin="-10,-10,0,0" 
                        VerticalAlignment="Top" 
                        Stroke="#FFFF3204" StrokeThickness="4"/>
            <Image x:Name="img" Margin="-5,-5,0,0" 
                HorizontalAlignment="Left" 
                VerticalAlignment="Top"
                Source="../Images/male.png" 
                Width="20" Height="20" 
                ToolTip="{Binding RobotName}" />
        </Grid>
    </Border>


    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Path=IsMale}" Value="False">
            <Setter TargetName="img" Property="Source" 
                  Value="../Images/female.png"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
                AncestorType={x:Type ListBoxItem}},Path=IsSelected}" Value="True">
            <Setter TargetName="bord" Property="Effect">
                <Setter.Value>
                    <DropShadowEffect ShadowDepth="0" 
                          Color="White" BlurRadius="15" />
                </Setter.Value>
            </Setter>
        </DataTrigger>            
    </DataTemplate.Triggers>
</DataTemplate>

Where the Robot data model looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CarouselWPFTestApp
{
    public class PersonData : INPCBase
    {
        private string firstName;
        private string lastName;
        private string salutation;
        private bool isMale;

        public PersonData(string firstName, string lastName, 
            string salutation, bool isMale)
        {
            this.firstName = firstName;
            this.lastName = lastName;
            this.salutation = salutation;
            this.isMale = isMale;
        }

        public string FirstName
        {
            get { return firstName; }
            set
            {
                if (firstName != value)
                {
                    firstName = value;
                    NotifyPropertyChanged("FirstName");
                    NotifyPropertyChanged("FullName");
                }
            }
        }

        public string LastName
        {
            get { return lastName; }
            set
            {
                if (lastName != value)
                {
                    lastName = value;
                    NotifyPropertyChanged("LastName");
                    NotifyPropertyChanged("FullName");
                }
            }
        }

        public string Salutation
        {
            get { return salutation; }
            set
            {
                if (salutation != value)
                {
                    salutation = value;
                    NotifyPropertyChanged("Salutation");
                }
            }
        }

        public bool IsMale
        {
            get { return isMale; }
            set
            {
                if (isMale != value)
                {
                    isMale = value;
                    NotifyPropertyChanged("IsMale");
                }
            }
        }

        public string FullName
        {
            get
            {
                return String.Format("{0} {1} {2}", Salutation, FirstName, LastName);
            }
        }
    }
}

Which looks like this:

SelectedItem

By using the Carousel.SelectedItem DependencyProperty, you are able to set the SelectedItem that will be set in the wrapped PathListBox.

It should be noted that the PathListBoxUtils has some form of virtualization where it will not attempt to navigate to an item that is not shown on screen at that time, so setting the Carousel.SelectedItem DependencyProperty to a non visible item will not cause any animation to the item to occur at all.

This is the only area where I must hold my hand up and say it works a bit strangely if you are using MVVM, and if you are, you should read this section of the article carefully: Important Notice About Animating to Bound SelectedItem.

ItemsSource

The ItemsSource DependencyProperty simply allows the internal PathListBox.ItemsSource to be set to an IEnumerable. This could be any IEnumerable, where an example may be as follows (which is actually how the robot items are applied to the demo):

carouselControl.ItemsSource = GetRobotData();

private IEnumerable GetRobotData()
{
    Random rand = new Random();
    List<RobotData> robotData = new List<RobotData>();

    for (int i = 0; i < 15; i++)
    {
        int idx = rand.Next(1, 9);
        robotData.Add(new RobotData(
            string.Format("Robot{0}", idx),
            string.Format("../Images/robot{0}.png", idx)));
    }

    return robotData;
}

NumberOfItemsOnPath

Sets the required number of items that should appear on the animation path. You should obviously only set this to a value which is less than the total number of items that were supplied to the CarouselControl.ItemSource DependencyProperty. This is not something the CarouselControl does for you, this is in your hands.

One thing the CarouselControl does do for you is, it will coerce the value you set on this CarouselControl.NumberOfItemsOnPath DependencyProperty using the CarouselControl.MinNumberOfItemsOnPath and CarouselControl.MaxNumberOfItemsOnPath properties.

This is all done using the following code in the CarouselControl:

/// <summary>
/// NumberOfItemsOnPath
/// </summary>
public static readonly DependencyProperty NumberOfItemsOnPathProperty = 
DependencyProperty.Register(
    "NumberOfItemsOnPath",
    typeof(int),
    typeof(CarouselControl),
    new FrameworkPropertyMetadata(
        7,
        FrameworkPropertyMetadataOptions.None,
        new PropertyChangedCallback(OnNumberOfItemsOnPathChanged),
        new CoerceValueCallback(CoerceNumberOfItemsOnPath)
    ),
    new ValidateValueCallback(IsValidNumberOfItemsOnPath)
);

//property accessors
public int NumberOfItemsOnPath
{
    get { return (int)GetValue(NumberOfItemsOnPathProperty); }
    set { SetValue(NumberOfItemsOnPathProperty, value); }
}

/// <summary>
/// Coerce NumberOfItemsOnPath value if not within limits
/// </summary>
private static object CoerceNumberOfItemsOnPath(DependencyObject d, object value)
{
    CarouselControl depObj = (CarouselControl)d;
    int current = (int)value;
    if (current < depObj.MinNumberOfItemsOnPath) current = depObj.MinNumberOfItemsOnPath;
    if (current > depObj.MaxNumberOfItemsOnPath) current = depObj.MaxNumberOfItemsOnPath;
    return current;
}

private static void OnNumberOfItemsOnPathChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    //invokes the CoerceValueCallback delegate ("CoerceMinNumberOfItemsOnPath")
    d.CoerceValue(MinNumberOfItemsOnPathProperty);  
    //invokes the CoerceValueCallback delegate ("CoerceMaxNumberOfItemsOnPath")
    d.CoerceValue(MaxNumberOfItemsOnPathProperty);  
    CarouselControl depObj = (CarouselControl)d;
    depObj.pathListBox.LayoutPaths[0].Capacity = (double)depObj.NumberOfItemsOnPath;
}

/// <summary>
/// MinNumberOfItemsOnPath DP
/// </summary>
public static readonly DependencyProperty MinNumberOfItemsOnPathProperty = 
    DependencyProperty.Register(
    "MinNumberOfItemsOnPath",
    typeof(int),
    typeof(CarouselControl),
    new FrameworkPropertyMetadata(
        3,
        FrameworkPropertyMetadataOptions.None,
        new PropertyChangedCallback(OnMinNumberOfItemsOnPathChanged),
        new CoerceValueCallback(CoerceMinNumberOfItemsOnPath)
    ),
    new ValidateValueCallback(IsValidNumberOfItemsOnPath));

//property accessors
public int MinNumberOfItemsOnPath
{
    get { return (int)GetValue(MinNumberOfItemsOnPathProperty); }
    set { SetValue(MinNumberOfItemsOnPathProperty, value); }
}

/// <summary>
/// Coerce MinNumberOfItemsOnPath value if not within limits
/// </summary>
private static void OnMinNumberOfItemsOnPathChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    //invokes the CoerceValueCallback delegate ("CoerceMaxNumberOfItemsOnPath")
    d.CoerceValue(MaxNumberOfItemsOnPathProperty);
    //invokes the CoerceValueCallback delegate ("CoerceNumberOfItemsOnPath")  
    d.CoerceValue(NumberOfItemsOnPathProperty);  
}

private static object CoerceMinNumberOfItemsOnPath(DependencyObject d, object value)
{
    CarouselControl depObj = (CarouselControl)d;
    int min = (int)value;
    if (min > depObj.MaxNumberOfItemsOnPath) min = depObj.MaxNumberOfItemsOnPath;
    return min;
}

/// <summary>
/// MaxNumberOfItemsOnPath
/// </summary>
public static readonly DependencyProperty MaxNumberOfItemsOnPathProperty = 
DependencyProperty.Register(
    "MaxNumberOfItemsOnPath",
    typeof(int),
    typeof(CarouselControl),
    new FrameworkPropertyMetadata(
        10,
        FrameworkPropertyMetadataOptions.None,
        new PropertyChangedCallback(OnMaxNumberOfItemsOnPathChanged),
        new CoerceValueCallback(CoerceMaxNumberOfItemsOnPath)
    ),
    new ValidateValueCallback(IsValidNumberOfItemsOnPath)
);

//property accessors
public int MaxNumberOfItemsOnPath
{
    get { return (int)GetValue(MaxNumberOfItemsOnPathProperty); }
    set { SetValue(MaxNumberOfItemsOnPathProperty, value); }
}

/// <summary>
/// Coerce MaxNumberOfItemsOnPath value if not within limits
/// </summary>
private static object CoerceMaxNumberOfItemsOnPath(DependencyObject d, object value)
{
    CarouselControl depObj = (CarouselControl)d;
    int max = (int)value;
    if (max < depObj.MinNumberOfItemsOnPath) max = depObj.MinNumberOfItemsOnPath;
    return max;
}

private static void OnMaxNumberOfItemsOnPathChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    //invokes the CoerceValueCallback delegate ("CoerceMinNumberOfItemsOnPath")
    d.CoerceValue(MinNumberOfItemsOnPathProperty);  
    
    //invokes the CoerceValueCallback delegate ("CoerceNumberOfItemsOnPath")
    d.CoerceValue(NumberOfItemsOnPathProperty);  
}

MinNumberOfItemsOnPath

Sets the minimum number of items that should appear on the animation path.

This CarouselControl DependencyProperty is really only used to coerce the CarouselControl.NumberOfItemsOnPath if the value supplied was less than this value. This coercing was shown above.

MaxNumberOfItemsOnPath

Sets the maximum number of items that should appear on the animation path.

This CarouselControl DependencyProperty is really only used to coerce the CarouselControl.NumberOfItemsOnPath if the value supplied is greater than this value. This coercing was shown above.

Demos

There are 2 demos supplied that make use of the attached Carousel control, one is there to show of the customisation, and the other is there to show you how you might use it in a MVVM environment.

Customsation Demo

Demo Project Name: CarouselWPFTestApp

This demo is really just there to show the customisation features of the CarouselControl that we have already discussed.

Here is the complete code behind for the customisation window that is affecting a CarouselControl instance. I would think, from this code, it is pretty clear how you could customise the CarouselControl in your own code by just setting the relevant CarouselControl properties.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Carousel;
using System.Collections;
using System.Windows.Media.Animation;

namespace CarouselWPFTestApp
{
    public partial class CustomiseWindow : Window
    {
        private CarouselControl carouselControl;
        private DataTemplate robotDataTemplate = 
            (Application.Current as App).Resources["robotDataTemplate"] as DataTemplate;
        private DataTemplate personDataTemplate =
            (Application.Current as App).Resources["personDataTemplate"] as DataTemplate;
        private Style blackPreviousButtonStyle =
            (Application.Current as App).Resources["BlackPreviousButtonStyle"] as Style;
        private Style blackNextButtonStyle =
            (Application.Current as App).Resources["BlackNextButtonStyle"] as Style;
        private PowerEase powerEaseIn;
        private PowerEase powerEaseOut;
        private BounceEase bounceEaseIn;
        private BounceEase bounceEaseOut;
        private SineEase sineEaseIn;
        private SineEase sineEaseOut;

        public CustomiseWindow()
        {
            InitializeComponent();
            powerEaseIn = new PowerEase() { Power = 6, EasingMode = EasingMode.EaseIn };
            powerEaseOut = new PowerEase() { Power = 6, EasingMode = EasingMode.EaseOut };
            bounceEaseIn = new BounceEase() { Bounces = 1, Bounciness = 4, 
                           EasingMode = EasingMode.EaseIn };
            bounceEaseOut = new BounceEase() { Bounces = 1, Bounciness = 4, 
                            EasingMode = EasingMode.EaseOut };
            sineEaseIn = new SineEase() { EasingMode = EasingMode.EaseIn };
            sineEaseOut = new SineEase() { EasingMode = EasingMode.EaseOut };
        }

        public CarouselControl CarouselControl
        {
            get { return carouselControl; }
            set
            {
                carouselControl = value;
                carouselControl.ItemsSource = GetRobotData();
                carouselControl.DataTemplateToUse = robotDataTemplate;
                MainWindow.CurrentDemoDataTemplateType = DemoDataTemplateType.Robot;
                slider.Minimum = carouselControl.MinNumberOfItemsOnPath;
                slider.Maximum = carouselControl.MaxNumberOfItemsOnPath;
                slider.Value = carouselControl.NumberOfItemsOnPath;
            }
        }

        private IEnumerable GetRobotData()
        {
            Random rand = new Random();
            List<RobotData> robotData = new List<RobotData>();

            for (int i = 0; i < 15; i++)
            {
                int idx = rand.Next(1, 9);
                robotData.Add(new RobotData(
                    string.Format("Robot{0}", idx),
                    string.Format("../Images/robot{0}.png", idx)));
            }

            return robotData;
        }

        private IEnumerable GetPersonData()
        {
            List<PersonData> personData = new List<PersonData>();
            personData.Add(new PersonData("Steve", "Soloman", "Mr", true));
            personData.Add(new PersonData("Ryan", "Worseley", "Mr", true));
            personData.Add(new PersonData("Sacha", "Barber", "Mr", true));
            personData.Add(new PersonData("Amy", "Amer", "Mrs", false));
            personData.Add(new PersonData("Samar", "Bou-Antoine", "Mrs", false));
            personData.Add(new PersonData("Fredrik", "Bornander", "Mr", true));
            personData.Add(new PersonData("Richard", "King", "Mr", true));
            personData.Add(new PersonData("Henry", "McKeon", "Mr", true));
            personData.Add(new PersonData("Debbie", "Doyle", "Mrs", false));
            personData.Add(new PersonData("Sarah", "Burns", "Mrs", false));
            personData.Add(new PersonData("Hank", "Dales", "Mr", true));
            personData.Add(new PersonData("Daniel", "Jones", "Mr", true));
            personData.Add(new PersonData("Lisa", "Dove", "Mrs", true));
            personData.Add(new PersonData("Rena", "Sams", "Mrs", false));
            personData.Add(new PersonData("Sarah", "Gray", "Mrs", false));
            
            return personData;
        }
        
        private void CmbPath_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (cmbPath.SelectedItem != null && CarouselControl != null)
            {
                CarouselControl.PathType = (PathType)Enum.Parse(typeof(PathType),
                    ((ComboBoxItem)cmbPath.SelectedItem).Tag.ToString());
            }
        }

        private void CmbNavigationButtonLocation_SelectionChanged(
                object sender, SelectionChangedEventArgs e)
        {
            if (cmbNavigationButtonLocation.SelectedItem != null && 
                CarouselControl != null)
            {
                CarouselControl.NavigationButtonPosition = 
                 (ButtonPosition)Enum.Parse(typeof(ButtonPosition),
                 ((ComboBoxItem)cmbNavigationButtonLocation.SelectedItem).Tag.ToString());
            }
        }

        private void CmbEasing_SelectionChanged(object sender, 
                     SelectionChangedEventArgs e)
        {
            if (cmbEasing.SelectedItem != null && CarouselControl != null)
            {
                String animationSelected = 
                  ((ComboBoxItem)cmbEasing.SelectedItem).Tag.ToString();

                switch (animationSelected)
                {
                    case "PowerEase":
                        CarouselControl.AnimationEaseIn = powerEaseIn;
                        CarouselControl.AnimationEaseOut = powerEaseOut;
                        break;
                    case "BounceEase":
                        CarouselControl.AnimationEaseIn = bounceEaseIn;
                        CarouselControl.AnimationEaseOut = bounceEaseOut;
                        break;
                    case "SineEase":
                        CarouselControl.AnimationEaseIn = sineEaseIn;
                        CarouselControl.AnimationEaseOut = sineEaseOut;
                        break;
                }
            }
        }

        private void ChangeButtonStyle_Click(object sender, RoutedEventArgs e)
        {
            CarouselControl.PreviousButtonStyle = blackPreviousButtonStyle;
            CarouselControl.NextButtonStyle = blackNextButtonStyle;
        }

        private void CmbDataTemplate_SelectionChanged(object sender, 
                     SelectionChangedEventArgs e)
        {
            if (cmbDataTemplate.SelectedItem != null && CarouselControl != null)
            {
                String templateSelected = 
                  ((ComboBoxItem)cmbDataTemplate.SelectedItem).Tag.ToString();

                switch (templateSelected)
                {
                    case "Robot":
                        carouselControl.ItemsSource = GetRobotData();
                        carouselControl.DataTemplateToUse = robotDataTemplate;
                        MainWindow.CurrentDemoDataTemplateType = DemoDataTemplateType.Robot;
                        break;
                    case "Person":
                        carouselControl.ItemsSource = GetPersonData();
                        carouselControl.DataTemplateToUse = personDataTemplate;
                        MainWindow.CurrentDemoDataTemplateType = DemoDataTemplateType.Person;
                        break;
                }
            }
        }

        private void Slider_ValueChanged(object sender, 
                RoutedPropertyChangedEventArgs<double> e)
        {
            if(CarouselControl != null)
                CarouselControl.NumberOfItemsOnPath = (int)slider.Value;
        }
    }
}

MVVM Demo

Demo Project Name: CarouselMVVM

I have created a small demo project that demonstrates how you might use the CarouselControl in an MVVM like manner.

So starting with a ViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;

namespace CarouselMVVM
{
    public class MainWindowViewModel : INPCBase
    {
        private List<String> data;
        private string currentData;
        private int currentPos = 0;

        public MainWindowViewModel()
        {
            data = new List<string>();
            for (int i = 0; i < 20; i++)
            {
                data.Add(String.Format("Item_{0}",i.ToString()));
            }

            //commands
            DecrementCommand = new SimpleCommand<Object, Object>(ExecuteDecrementCommand);
            IncrementCommand = new SimpleCommand<Object, Object>(ExecuteIncrementCommand);
        }

        private void ExecuteDecrementCommand(Object parameter)
        {
            if (currentPos > 0)
            {
                --currentPos;
                CurrentData = data[currentPos];
            }
        }

        private void ExecuteIncrementCommand(Object parameter)
        {
            if (currentPos < data.Count -1)
            {
                ++currentPos;
                CurrentData = data[currentPos];
            }
        }

        public ICommand DecrementCommand { get; private set; }
        public ICommand IncrementCommand { get; private set; }

        public List<String> Data
        {
            get { return data; }
        }

        public string CurrentData
        {
            get { return currentData; }
            set
            {
                if (currentData != value)
                {
                    currentData = value;
                    currentPos = data.IndexOf(currentData);
                    NotifyPropertyChanged("CurrentData");
                }
            }
        }
    }
}

Where we would have a View something like this:

<Window x:Class="CarouselMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Carousel="clr-namespace:Carousel;assembly=Carousel"
        Title="MVVM Carousel Demo"
        WindowStyle="ToolWindow"
        WindowStartupLocation="Manual"
        Left="0"
        Top="0"
        Background="Black"
        Width="800" Height="600">

    <Window.Resources>
        <Style x:Key="labelStyle" TargetType="{x:Type Label}">
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="HorizontalContentAlignment" Value="Left"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="FontFamily" Value="Verdana"/>
            <Setter Property="FontSize" Value="10"/>
            <Setter Property="Height" Value="25"/>
            <Setter Property="Width" Value="Auto"/>
            <Setter Property="Margin" Value="5"/>
        </Style>

        <Style x:Key="buttonStyle" TargetType="{x:Type Button}">
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="FontFamily" Value="Verdana"/>
            <Setter Property="FontSize" Value="10"/>
            <Setter Property="Height" Value="25"/>
            <Setter Property="Width" Value="Auto"/>
            <Setter Property="Margin" Value="5"/>
        </Style>

    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="80"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>


        <Grid Grid.Row="0" Background="LightSteelBlue">
            <StackPanel Orientation="Horizontal">
                <Label Style="{StaticResource labelStyle}" 
                    Content="Pick Current Item"/>
                <ComboBox  x:Name="cmb" ItemsSource="{Binding Data}" 
                           SelectedItem="{Binding CurrentData}"
                           Margin="5" Height="25" Width="100"/>
                <Button Width="25" Height="25" 
                   Margin="5" Content="<" 
                   Command="{Binding DecrementCommand}"/>
                <Button Width="25" Height="25" 
                   Margin="5" Content=">" 
                   Command="{Binding IncrementCommand}"/>
            </StackPanel>
        </Grid>

        <Grid Grid.Row="1" Background="LightSteelBlue">
            <Label Content="Selected Item:" 
                HorizontalAlignment="Left" Margin="0" 
                Width="202" FontFamily="Impact" 
                FontSize="32" VerticalAlignment="Center"/>
            <Label x:Name="lblSelectedItem" 
                Margin="195,0,0,0" FontFamily="Impact" 
                FontSize="32" VerticalAlignment="Center" 
                Foreground="#FFFF3204"/>
        </Grid>

        <Grid Grid.Row="2">

            <Path x:Name="customPath" Data="M12,547.5 C42.162511,513.31582............. 
                Margin="12,62.168,-25.5,18.5" 
                Stretch="Fill" Stroke="Transparent"/>
            <Carousel:CarouselControl x:Name="CarouselControl"
                        ItemsSource="{Binding Data}"
                        SelectedItem="{Binding CurrentData,Mode=TwoWay}"
                        SelectionChanged="CarouselControl_SelectionChanged"
                        CustomPathElement="{Binding ElementName=customPath}">
                <Carousel:CarouselControl.DataTemplateToUse>
                    <DataTemplate>
                        <Border BorderBrush="White" BorderThickness="2" 
                                CornerRadius="5" Background="DarkGray">
                            <Label VerticalAlignment="Center" 
                               VerticalContentAlignment="Center"
                               HorizontalAlignment="Center" 
                               HorizontalContentAlignment="Center"
                               Content="{Binding}" Foreground="White"/>
                        </Border>
                    </DataTemplate>
                </Carousel:CarouselControl.DataTemplateToUse>
               
            </Carousel:CarouselControl>
        </Grid>
    </Grid>
</Window>

And code-behind (see Shortcomings below for the why) like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CarouselMVVM
{
    public partial class MainWindow : Window
    {
        MainWindowViewModel vm = new MainWindowViewModel();
        public MainWindow()
        {
            this.DataContext = vm;
            InitializeComponent();
        }

        /// <summary>
        /// This should not be nessecary, but could
        /// not get wrapper to update Binding when
        /// SelectedItem changed...Tried lots of stuff,
        /// this is hack, but ran out of steam.
        /// </summary>
        private void CarouselControl_SelectionChanged(object sender, 
                     SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count > 0)
            {
                lblSelectedItem.Content = e.AddedItems[0].ToString();
                vm.CurrentData = (string)e.AddedItems[0];
            }
        }
    }
}

Important Notice About Animating to Bound SelectedItem

There is one very important thing to note when using an MVVM like app with the CarouselControl, which is when you set the SelectedItem, and expect the CarouselControl to navigate to that item. Sometimes it will navigate to the item you select and sometimes it will not, the reason for this is a form of virtualization that the PathListBoxUtils performs, in that if you ask to select an item that is currently not shown on screen, the internal PathListBox will not be animated. If however you ask set a SelectedItem (say via a Binding) where the item is viewable in the CarouselControl, then that will be fine and will be animated to.

If you feel this is behaviour you just can't abide by, you can change this by downloading the actual PathListBoxUtils source and commenting out the following lines of code in PathListBoxScrollBehavior (but beware, doing this may be dangerous if you have a lot of items; this code is there to provide some level of protection against having to wait to scroll through loads of items, so edit the code at your peril).

To be honest, it is things like this that make the PathListBoxUtils not that suitable for MVVM in my opinion.

private void ScrollSelected()
{
    PathListBox pathListBox = this.AssociatedObject as PathListBox;
    if (pathListBox == null)
    {
        return;
    }
    PathListBoxItem newItem = (PathListBoxItem) pathListBox.ItemContainerGenerator
                    .ContainerFromItem(pathListBox.SelectedItem);
            
    // find the item on the path that is closest to the position
    PathListBoxItem closestItem = null;
    PathListBoxItem pathListBoxItem;
    for (int i = 0; i < pathListBox.Items.Count; i++)
    {
        pathListBoxItem = (PathListBoxItem)
          pathListBox.ItemContainerGenerator.ContainerFromIndex(i);
        if (pathListBoxItem != null && pathListBoxItem.IsArranged)
        {
            if (closestItem == null)
            {
                closestItem = pathListBoxItem;
            }
            else if (Math.Abs(pathListBoxItem.LocalOffset - this.DesiredOffset) 
                < Math.Abs(closestItem.LocalOffset - this.DesiredOffset))
            {
                closestItem = pathListBoxItem;
            }
        }
    }

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //If you always want to scroll to the item, comment these lines        
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    if (closestItem == null || newItem == null || !newItem.IsArranged || 
        !closestItem.IsArranged)
    {
        return;
    }

    int increment = 0;

    if (newItem.GlobalOffset < closestItem.GlobalOffset && 
        newItem.GlobalIndex > closestItem.GlobalIndex)
    {
        increment = -(pathListBox.Items.Count - 
          newItem.GlobalIndex + closestItem.GlobalIndex);
    }
    else if (newItem.GlobalOffset > closestItem.GlobalOffset && 
             newItem.GlobalIndex < closestItem.GlobalIndex)
    {
        increment = (pathListBox.Items.Count - closestItem.GlobalIndex + 
                     newItem.GlobalIndex);
    }
    else
    {
        increment = newItem.GlobalIndex - closestItem.GlobalIndex;
    }

    bool hideEnteringItem = this.HideEnteringItem;
    this.HideEnteringItem = false;

    Scroll(increment);

    this.HideEnteringItem = hideEnteringItem;

}

Shortcomings

I could not get some of the usual things to work, such as IsSynchronizedWithCurrentItem and even the SelectedItem is somewhat hinky, so I feel I just need to go into this a bit.

It would have been nice to be able to fully support ICollectionView and IsSynchronizedWithCurrentItem and allow a new item to be set from the ViewModel and be done with it. Unfortunately, that did not seem to work out for me, so I have ended up in some 1/2 way house, where by you expose an item in your ViewModel that you want to use as the current item in the CarouselControl, but you must also hook up the SelectionChanged event of the CarouselControl in the view and write back to the ViewModel, as shown in the code snippets above.

OK, this could be wrapped up into a new Behaviour or something, for the zero code behind nazis.....but there you go.

I could not fix this (and to be frank, I am sick of trying; I know, I know, what a bad attitude I have...but it is what it is), someone may find out why, and they can let me know all about it.

That's it For Now

Messing around with this has taken me longer than I would care to admit, and I am now going to get back to my last TPL article. So hopefully see you then.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here