In this post I want to demonstrate how easy it is to build a simple picture-browser application based on our RadDataBoundListBox and RadSlideView controls.
What’s more interesting in the demo:
Simulating Wrap Layout with RadDataBoundListBox
Building a custom control to be used for image holder
Using RadSlideView with SlideViewPictureSource
It is a very simple application with two pages – the main one that displays the list box with all the picture thumbnails. The second page contains a slide view instance and it is navigated to when a thumbnail image is tapped. Additional Tilt effect is used on each thumbnail to emphasize the user action. Embedded images are used on purpose, so that the application does not use your internet connection to download data. Still, the demo will work with any absolute or relative Uri.
Currently our list box supports stack layout only. Our plans are to extend the control with a virtualized wrap layout that will have the same performance as the stack one and will allow you implement scenarios (like the one in this blog) for your apps. Although this feature is not yet available, we can easily simulate a wrap layout using stack layout with three inner images in each listbox item.
Here is how our view model looks like:
public
class
WrapViewModel : BaseModel
{
private
Uri image1;
private
Uri image2;
private
Uri image3;
public
int
ItemIndex
{
get
;
set
;
}
public
Uri Image1
{
get
{
return
this
.image1;
}
set
{
this
.image1 = value;
this
.OnPropertyChanged(
"Image1"
);
}
}
public
Uri Image2
{
get
{
return
this
.image2;
}
set
{
this
.image2 = value;
this
.OnPropertyChanged(
"Image2"
);
}
}
public
Uri Image3
{
get
{
return
this
.image3;
}
set
{
this
.image3 = value;
this
.OnPropertyChanged(
"Image3"
);
}
}
}
We have three Uri properties per item and each property will be bound to an Image control. The item index property is used to tell the index of this item in the owning source collection. This index will be used to determine the index of the contained images. For example:
int
image1Index =
this
.ItemIndex * 3;
int
image2Index =
this
.ItemIndex * 3 + 1;
Now that we have the view model we need to setup the item template in the listbox. It will consist of a grid layout with three columns and three custom ImageLoaderControl instances within each column.
The control’s idea is to have two visual states – Loading and Loaded, indicating that an image is about to be displayed. Here is the control’s XAML:
<
Style
TargetType
=
"local:ImageLoaderControl"
>
<
Setter
Property
=
"Template"
>
<
Setter.Value
>
<
ControlTemplate
TargetType
=
"local:ImageLoaderControl"
>
<
Grid
Margin
=
"6"
>
<
VisualStateManager.VisualStateGroups
>
<
VisualStateGroup
x:Name
=
"CommonStates"
>
<
VisualState
x:Name
=
"Loading"
>
<
Storyboard
>
<
ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty
=
"Visibility"
Storyboard.TargetName
=
"loadingContent"
>
<
DiscreteObjectKeyFrame
KeyTime
=
"0"
Value
=
"Visible"
/>
</
ObjectAnimationUsingKeyFrames
>
<
ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty
=
"Visibility"
Storyboard.TargetName
=
"image"
>
<
DiscreteObjectKeyFrame
KeyTime
=
"0"
Value
=
"Collapsed"
/>
</
ObjectAnimationUsingKeyFrames
>
</
Storyboard
>
</
VisualState
>
<
VisualState
x:Name
=
"Loaded"
>
<
Storyboard
>
<
ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty
=
"Visibility"
Storyboard.TargetName
=
"loadingContent"
>
<
DiscreteObjectKeyFrame
KeyTime
=
"0"
Value
=
"Collapsed"
/>
</
ObjectAnimationUsingKeyFrames
>
<
ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty
=
"Visibility"
Storyboard.TargetName
=
"image"
>
<
DiscreteObjectKeyFrame
KeyTime
=
"0"
Value
=
"Visible"
/>
</
ObjectAnimationUsingKeyFrames
>
</
Storyboard
>
</
VisualState
>
</
VisualStateGroup
>
</
VisualStateManager.VisualStateGroups
>
<
Border
Background
=
"{StaticResource PhoneChromeBrush}"
x:Name
=
"loadingContent"
Visibility
=
"Collapsed"
>
<
TextBlock
Text
=
"Loading..."
HorizontalAlignment
=
"Center"
VerticalAlignment
=
"Center"
/>
</
Border
>
<
Image
Source
=
"{TemplateBinding Source}"
Stretch
=
"UniformToFill"
x:Name
=
"image"
Visibility
=
"Collapsed"
/>
</
Grid
>
</
ControlTemplate
>
</
Setter.Value
>
</
Setter
>
</
Style
>
The “Loading” state sets the “Loading…” text block’s visibility to Visible and the “Loaded” one hides the text block and displays the actual image. Of course this is a very simple scenario and you may implement more complex and visually appealing states to meet the requirements of you application.
And the code behind looks like:
public
class
ImageLoaderControl : Control
{
public
static
readonly
DependencyProperty SourceProperty =
DependencyProperty.Register(
"Source"
,
typeof
(ImageSource),
typeof
(ImageLoaderControl),
new
PropertyMetadata(
null
, OnSourceChanged));
public
static
readonly
DependencyProperty RowIndexProperty =
DependencyProperty.Register(
"RowIndex"
,
typeof
(
int
),
typeof
(ImageLoaderControl),
new
PropertyMetadata(-1));
public
static
readonly
DependencyProperty ColumnIndexProperty =
DependencyProperty.Register(
"Column"
,
typeof
(
int
),
typeof
(ImageLoaderControl),
new
PropertyMetadata(-1));
private
Image image;
private
Border loadingContent;
public
ImageLoaderControl()
{
this
.DefaultStyleKey =
typeof
(ImageLoaderControl);
InteractionEffectManager.AllowedTypes.Add(
typeof
(ImageLoaderControl));
}
public
int
RowIndex
{
get
{
return
(
int
)
this
.GetValue(RowIndexProperty);
}
set
{
this
.SetValue(RowIndexProperty, value);
}
}
public
int
ColumnIndex
{
get
{
return
(
int
)
this
.GetValue(ColumnIndexProperty);
}
set
{
this
.SetValue(ColumnIndexProperty, value);
}
}
public
ImageSource Source
{
get
{
return
this
.GetValue(SourceProperty)
as
ImageSource;
}
set
{
this
.SetValue(SourceProperty, value);
}
}
public
override
void
OnApplyTemplate()
{
base
.OnApplyTemplate();
this
.image =
this
.GetTemplateChild(
"image"
)
as
Image;
this
.loadingContent =
this
.GetTemplateChild(
"loadingContent"
)
as
Border;
VisualStateManager.GoToState(
this
,
"Loading"
,
false
);
}
private
static
void
OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ImageLoaderControl control = d
as
ImageLoaderControl;
VisualStateManager.GoToState(control,
"Loaded"
,
false
);
}
}
We have a Source property, used to specify the source of the inner Image instance, a RowIndex property to tell the index of the container RadDataBoundListBoxItem and a ColumnIndex to specify the column (0, 1, 2) the control is located at. Upon a change in the Source property we enter the “Loaded” visual state, which hides the text block and displays the image. You may perform more complex logic here to determine when the “Loaded” state actually occurs by handling the ImageOpened event of the image.
The XAML of the main page is quite simple – it uses the image loader to setup its item itemplate:
<
Grid
x:Name
=
"LayoutRoot"
Background
=
"Transparent"
Margin
=
"18,0,0,0"
telerikCore:InteractionEffectManager.IsInteractionEnabled
=
"True"
>
<
Grid.RowDefinitions
>
<
RowDefinition
Height
=
"Auto"
/>
<
RowDefinition
Height
=
"*"
/>
</
Grid.RowDefinitions
>
<
TextBlock
Text
=
"images"
FontSize
=
"{StaticResource PhoneFontSizeExtraExtraLarge}"
Margin
=
"12,24,0,48"
/>
<
telerikPrimitives:RadDataBoundListBox
x:Name
=
"listBox"
ItemsSource
=
"{Binding Items}"
Grid.Row
=
"1"
ItemTap
=
"OnImageTap"
>
<
telerikPrimitives:RadDataBoundListBox.ItemTemplate
>
<
DataTemplate
>
<
Grid
>
<
Grid.ColumnDefinitions
>
<
ColumnDefinition
Width
=
"148"
/>
<
ColumnDefinition
Width
=
"148"
/>
<
ColumnDefinition
Width
=
"148"
/>
</
Grid.ColumnDefinitions
>
<
StackPanel
>
<
local:ImageLoaderControl
Source
=
"{Binding Image1}"
RowIndex
=
"{Binding ItemIndex}"
ColumnIndex
=
"0"
Height
=
"148"
/>
<
TextBlock
Text
=
"{Binding Image1Description}"
Style
=
"{StaticResource itemText}"
/>
</
StackPanel
>
<
StackPanel
Grid.Column
=
"1"
>
<
local:ImageLoaderControl
Source
=
"{Binding Image2}"
RowIndex
=
"{Binding ItemIndex}"
ColumnIndex
=
"1"
Height
=
"148"
/>
<
TextBlock
Text
=
"{Binding Image2Description}"
Style
=
"{StaticResource itemText}"
/>
</
StackPanel
>
<
StackPanel
Grid.Column
=
"2"
>
<
local:ImageLoaderControl
Source
=
"{Binding Image3}"
RowIndex
=
"{Binding ItemIndex}"
ColumnIndex
=
"2"
Height
=
"148"
/>
<
TextBlock
Text
=
"{Binding Image3Description}"
Style
=
"{StaticResource itemText}"
/>
</
StackPanel
>
</
Grid
>
</
DataTemplate
>
</
telerikPrimitives:RadDataBoundListBox.ItemTemplate
>
</
telerikPrimitives:RadDataBoundListBox
>
</
Grid
>
Now we have the listbox with a simulate wrap layout and the fancy ImageLoaderControl to represent each image. Let’s see how to navigate to the slide view page upon a tap over an image:
private
void
OnImageTap(
object
sender, Telerik.Windows.Controls.ListBoxItemTapEventArgs e)
{
Image tappedImage = e.OriginalSource
as
Image;
if
(tappedImage ==
null
)
{
return
;
}
ImageLoaderControl ownerControl = ElementTreeHelper.FindVisualAncestor<ImageLoaderControl>(tappedImage);
if
(ownerControl !=
null
)
{
MainViewModel.Instance.NavigatoToImage(ownerControl.RowIndex, ownerControl.ColumnIndex);
}
}
public
void
NavigatoToImage(
int
rowIndex,
int
colIndex)
{
int
imageIndex = rowIndex * 3 + colIndex;
(Application.Current.RootVisual
as
PhoneApplicationFrame).Navigate(
new
Uri(
"/SlideViewPage.xaml?index="
+ imageIndex, UriKind.RelativeOrAbsolute));
}
We handle the ItemTap event of the listbox, find the owning ImageLoader control and navigate to the slide view page passing the currently selected index as a query string. And finally let’s see how the slide view page is setup.
The control’s usage is simple and straightforward:
<
Grid
x:Name
=
"LayoutRoot"
Background
=
"Transparent"
>
<
telerikPrimitives:RadSlideView
x:Name
=
"slideView"
Margin
=
"-6"
>
<
telerikPrimitives:RadSlideView.DataSource
>
<
telerikPrimitives:SlideViewPictureSource
ItemsSource
=
"{Binding Images}"
/>
</
telerikPrimitives:RadSlideView.DataSource
>
</
telerikPrimitives:RadSlideView
>
</
Grid
>
We specify the special SlideViewPictureSource as its DataSource. The data source itself will provide a default item template, so no need to specify our own one except we want a more complex one rather than a simple Image. We bind the data source to the Images property of the main view model, which is actually a collection with the URIs of all the loaded images. Currently the picture data source does not support an ImageSource instance directly but it will when the control exits its BETA state and you will have even better support for images. We also have plans to add an arbitrary option to load images directly from the IsolatedStorage, based on specified folder(s).
And the page’s code behind:
public
partial
class
SlideViewPage : PhoneApplicationPage
{
public
SlideViewPage()
{
InitializeComponent();
// update selected index after layout is updated
// this is a bug in the BETA of RadSlideView
this
.LayoutUpdated +=
this
.SlideViewPage_LayoutUpdated;
}
void
SlideViewPage_LayoutUpdated(
object
sender, EventArgs e)
{
this
.LayoutUpdated -=
this
.SlideViewPage_LayoutUpdated;
this
.slideView.SelectedIndex = Int32.Parse(
this
.NavigationContext.QueryString[
"index"
]);
}
}
Due to a bug that is currently present, the SelectedIndex property cannot be set before the control is added to the visual scene and its containers are created. That is why we handle the LayoutUpdated event of the page and specify the currently selected index of the control. The index is passed as a parameter in the page’s query string. Once the Service Pack of Q3 is out you will be able to bind the selected index to a property of the main view model.
Well, these are most of the more interesting parts of the demo app. I really hope that you will find it useful and learn some new tips and tricks from it. Do not hesitate to let me know for any suggestions or questions you may have.
Georgi worked at Progress Telerik to build a product that added the Progress value into the augmented and virtual reality development workflow.