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 Pat
hListBox. 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 LayoutPath
s 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>
<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>
<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>
<PathListBoxUtils:AdjustedOffsetToRadiusConverter
x:Key="AdjustedOffsetToRadiusConverter"/>
<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:
<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
:
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); }
}
private static void OnOpacityRangeChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((CarouselControl)d).OnOpacityRangeChanged(e);
}
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:
<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:
<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
:
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)
);
public int NumberOfItemsOnPath
{
get { return (int)GetValue(NumberOfItemsOnPathProperty); }
set { SetValue(NumberOfItemsOnPathProperty, value); }
}
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)
{
d.CoerceValue(MinNumberOfItemsOnPathProperty);
d.CoerceValue(MaxNumberOfItemsOnPathProperty);
CarouselControl depObj = (CarouselControl)d;
depObj.pathListBox.LayoutPaths[0].Capacity = (double)depObj.NumberOfItemsOnPath;
}
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));
public int MinNumberOfItemsOnPath
{
get { return (int)GetValue(MinNumberOfItemsOnPathProperty); }
set { SetValue(MinNumberOfItemsOnPathProperty, value); }
}
private static void OnMinNumberOfItemsOnPathChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(MaxNumberOfItemsOnPathProperty);
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;
}
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)
);
public int MaxNumberOfItemsOnPath
{
get { return (int)GetValue(MaxNumberOfItemsOnPathProperty); }
set { SetValue(MaxNumberOfItemsOnPathProperty, value); }
}
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)
{
d.CoerceValue(MinNumberOfItemsOnPathProperty);
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()));
}
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();
}
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);
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 (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.