- Links
- Refcardz
- Guides
- About
- Post
- Article
- Link
- Code Snippet
- Announcement
Like this piece? Share it with your friends:
In the previous article in the series I talked about how it is possible to load the main gallery images, as well as how to display a small subset of those on the main page. Today I am going to talk about a way to add more images in the ListBox, loading them as the user needs them, as well as about a way to view image details if the user taps on one of the items.
So let's begin with the ListBox. Currently, when the main page is loaded, I am checking whether there is anything in the HomeImages container - that's where all active images are stored. There is also an additional container - DeserializedHomeImages, that contains the image references, but not the images themselves.
Looking at the code we have, you can see that currently I am loading only 10 images:
ImageDownloadHelper.DownloadImages(MainPageViewModel.Instance.DeserializedHomeImages, 0, 10, (image) => { MainPageViewModel.Instance.HomeImages.Add(image); Debug.WriteLine(string.Format("[{0}] Image added to HomeImages in MainPageViewModel.", image.Type)); });
Which are then displayed in the core view:
Perfect, but 10 images are not exactly what we're looking for, since there are many more out there in the reference container (DeserializedHomeImages). So how do we keep adding more to the bound collection?
We need to make sure that we can detect when the user scrolled to the bottom of the list. There is no default event handler, but we can surely implement a custom control that notifies the application when the user reached the end of the current set. The inspiration behind my implementation for this was the control documented by Eric here. I ported it from VB to C#.
I create a new Controls folder in the project and added an InfiniteListBox new class. This class inherits from a standard ListBox, but also provides the proper hooks to let the developer known when a compression, therefore list finalization, event occurs.
Here are the contents of the file:
using System; using System.Collections; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace Imagine.Controls { public class InfiniteListBox : ListBox { public delegate void OnCompression(object sender, CompressionEventArgs args); public event OnCompression CompressionOccured; private bool scrollEventsHooked = false; public InfiniteListBox() { this.Loaded += InfiniteListBox_Loaded; } void InfiniteListBox_Loaded(object sender, RoutedEventArgs e) { PrepareCompressionTracking(); } private void PrepareCompressionTracking() { ScrollViewer scrollViewer = null; if (scrollEventsHooked) return; scrollViewer = FindFirstElement(this, typeof(ScrollViewer)) as ScrollViewer; if (scrollViewer != null) { FrameworkElement element = VisualTreeHelper.GetChild(scrollViewer, 0) as FrameworkElement; if (element != null) { var verticalStateGroup = FindVisualStateGroup(element, "VerticalCompression"); var horizontalStateGroup = FindVisualStateGroup(element, "HorizontalCompression"); if (verticalStateGroup != null) { verticalStateGroup.CurrentStateChanging += verticalStateGroup_CurrentStateChanging; } if (horizontalStateGroup != null) { horizontalStateGroup.CurrentStateChanging += horizontalStateGroup_CurrentStateChanging; } } } scrollEventsHooked = true; } void horizontalStateGroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e) { if (e.NewState.Name == "CompressionLeft") { CompressionOccured(this, new CompressionEventArgs(CompressionType.Left)); } else if (e.NewState.Name == "CompressionRight") { CompressionOccured(this, new CompressionEventArgs(CompressionType.Right)); } } void verticalStateGroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e) { if (e.NewState.Name == "CompressionTop") { CompressionOccured(this, new CompressionEventArgs(CompressionType.Top)); } else if (e.NewState.Name == "CompressionBottom") { CompressionOccured(this, new CompressionEventArgs(CompressionType.Bottom)); } } private VisualStateGroup FindVisualStateGroup(FrameworkElement parent, string name) { if (parent == null) return null; IList groups = VisualStateManager.GetVisualStateGroups(parent); foreach (VisualStateGroup group in groups) { if (group.Name == name) return group; } return null; } private UIElement FindFirstElement(FrameworkElement parent, Type targetType) { int childCount = VisualTreeHelper.GetChildrenCount(parent); UIElement returnedElement = null; if (childCount > 0) { for (int i = 0; i < childCount; i++) { var element = VisualTreeHelper.GetChild(parent, i); if (element.GetType().Equals(targetType)) { returnedElement = (UIElement)element; break; } } } return returnedElement; } } public class CompressionEventArgs : EventArgs { CompressionType _type; public CompressionType Type { get { return _type; } set { _type = value; } } public CompressionEventArgs(CompressionType type) { _type = type; } } public enum CompressionType { Top, Bottom, Left, Right } }There is not much going on in the backend other than the visual tree being traversed to find the ScrollViewer and the associated VisualStateGroup instances for horizontal and vertical compression.
<Style TargetType="ScrollViewer"> <Setter Property="VerticalScrollBarVisibility" Value="Auto"/> <Setter Property="HorizontalScrollBarVisibility" Value="Auto"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="Padding" Value="0"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderBrush" Value="Transparent"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ScrollViewer"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" > <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="ScrollStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="00:00:00.5"/> </VisualStateGroup.Transitions> <VisualState x:Name="Scrolling"> <Storyboard> <DoubleAnimation Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="Opacity" To="1" Duration="0"/> <DoubleAnimation Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="Opacity" To="1" Duration="0"/> </Storyboard> </VisualState> <VisualState x:Name="NotScrolling"/> </VisualStateGroup> <VisualStateGroup x:Name="VerticalCompression"> <VisualState x:Name="NoVerticalCompression"/> <VisualState x:Name="CompressionTop"/> <VisualState x:Name="CompressionBottom"/> </VisualStateGroup> <VisualStateGroup x:Name="HorizontalCompression"> <VisualState x:Name="NoHorizontalCompression"/> <VisualState x:Name="CompressionLeft"/> <VisualState x:Name="CompressionRight"/> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid Margin="{TemplateBinding Padding}"> <ScrollContentPresenter x:Name="ScrollContentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/> <ScrollBar x:Name="VerticalScrollBar" IsHitTestVisible="False" HorizontalAlignment="Right" VerticalAlignment="Stretch" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Value="{TemplateBinding VerticalOffset}" Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" /> <ScrollBar x:Name="HorizontalScrollBar" IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Value="{TemplateBinding HorizontalOffset}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" /> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>I would recommend including it in App.xaml, in order for it to be an app-wide accessible resource. Going back to MainPage.xaml, where the contents are being rendered, I am replacing the ListBox with InfiniteListBox:
<controls:InfiniteListBox x:Name="mainList" CompressionOccured="mainList_CompressionOccured_1" ItemsSource="{Binding Path=Instance.HomeImages, Source={StaticResource MainPageViewModel}}"> <controls:InfiniteListBox.ItemTemplate> <DataTemplate> <Image Stretch="UniformToFill" Source="{Binding Image}"></Image> </DataTemplate> </controls:InfiniteListBox.ItemTemplate> <controls:InfiniteListBox.ItemsPanel> <ItemsPanelTemplate> <toolkit:WrapPanel Item Item/> </ItemsPanelTemplate> </controls:InfiniteListBox.ItemsPanel> </controls:InfiniteListBox>The controls namespace was declared in the XAML file header as:
xmlns:controls="clr-namespace:Imagine.Controls"Notice that the item and item panel template declarations do not change. But there is now a hooked CompressionOccured event:
private void mainList_CompressionOccured_1(object sender, Controls.CompressionEventArgs args) { Debug.WriteLine(args.Type.ToString()); }For testing purposes, I am simply writing in the output console that the list has reached a specific compression state. And it sure works: