Introduction
I have been away from writing CodeProject articles for a little while now, but that does not mean I am idle. Far from it, I am working on a large ASP.NET MVC based project in my spare time that I hope will be a useful tool to most developers. Thing is, the web is not really where my heart lies, and I was getting fed up writing all this web stuff on the train and messing around with all the cross browser weirdness (still can't believe we have to care about that in this day and age.....grrr). Long and short of it is that I needed a bit of a break, so I set myself a spare week's worth of train journey (I work 50 miles away, so get a hour or two per day to play on train) time to get back to where my roots lay, which is in the land of WPF and honey. At around that time, my work colleague also got a new version of a well known phone that had a funky navigation and grouping system, and I thought, ah maybe I could have a go at something like that. This article represents a WPF control library that emulates part of the well known phone navigation and grouping system. I should just mention that it does not do as much as the well known phone, and I am now done with this, as I am now ready to get back to my web site project, and I would also like to say Kudos to the well known phone manufacturer for creating such a sterling system in the first place; it rocks, and to emulate in its entirety would be some undertaking actually.
So to recap, this article only provides a portion of what is available in the well known phone vendor's system, but I still think there is something of merit that could be useful in this article for you good folk.
Video Demo
As this is quite a visual thing, I think the best way to demo it is to show you a small video; click the image below to view a video of the finished control:
What is the Overall Structure of the App
Now that you have seen the video, let's have a look at a few diagrams which may help further reinforce how the control suite is constructed.
Let's start with this diagram:
I think this figure is a good starting point. What you can see in the figure above is that there is a hosting Window that hosts what appears to be a scrollable area that only shows a portion of an entire canvas full of blocks. That sentence is pretty close to the truth of how the code works actually. There is indeed a scrollable area which is a control called ScrollContainer
, which holds a number of BlockContainer
controls. Each BlockContainer
also holds a number of either GroupedBlockControl
(s) or ExpandedBlockControl
(s).
Oh, and you can also expand a GroupedBlockControl
(providing there is none other expanded already), which will look like this:
To see how all these control types relate to the figure above, consider the following figure which I have annotated to show the different control types:
So How Does it All Work?
The following subsections will outline how the relevant parts of these five controls work.
ScrollContainer
Description of what this control does
At the heart of this article is a custom ScrollContainer
which is a control that simply contains a specialized ScrollViewer
that supports friction, which I have used in a number of my articles, so I will not bore you with how that all works; have a look at the code.
What I think is a better thing to do is to talk about what the ScrollContainer
does as a whole and then show you some trimmed down code.
The ScrollContainer
acts as a container for a number of BlockContainer
controls, which are created when the ScrollContainer
's Blocks
property is assigned a new List<BlockItemBase>
. Essentially, what happens is that the incoming List<BlockItemBase>
of blocks is examined, and if the item is a GroupedBlockItem
, a new GroupedBlockControl
is created and added to the current BlockContainer
. If the item is a BlockWorkItem
, a new ExpandedBlockControl
is created and added to the current BlockContainer
.
Notice how I talk about a current BlockContainer
? Well, what exactly do we mean by current?
The ScrollContainer
class holds a variable which dictates how many blocks should fit within a single BlockContainer
, so it's just a question of creating enough BlockContainer
s to accommodate the number of items in the ScrollViewer.Blocks
property.
What else does this control do? Well, roughly speaking, this control's job is to carry out the following tasks:
- This control will friction scroll even after the user has lifted their mouse. This control handles snapping back into the previous position if the friction was not enough or the user let go of the mouse whilst not more than 1/2 way into the new block
- This control will not scroll when a
ExpandedBlocksContainer
is expanded.
- This control will also call a
ExpandedBlock.DoWork()
method if the control is deemed to be clicked.
- This control will also expand a
GroupedBlocksControl
as long as no other GroupedBlocksControl
s are expanded
Now, this may sound well and good, but when you have a scrollable control that should be scrollable all the time except sometimes, you have to use a few tricks, and what about determining if a control has been clicked? Easier said than done actually. How do we do these things?
To scroll or not to scroll is easily achieved using a simple ViewState
enum that has BlockExpanded
/BlockCollapsed
values. We only allow scrolling when the ViewState
of the ScrollContainer
is BlockExpanded
.
How about determining if a control has been clicked? Well, that's a two stage thing: first, we need to see if there is a control under the mouse when we firsst MouseDown
; this is achieved as follows:
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
....
....
....
groupedBlockControl =
TreeHelper.TryFindFromPoint<GroupedBlockControl>(this, scrollStartPoint);
expandedBlockControl =
TreeHelper.TryFindFromPoint<ExpandedBlockControl>(this, scrollStartPoint);
....
....
....
}
How we can use the VisualTree (via the TreeHelper
class in the demo app) to find out if we have a Point
that is over a GroupedBlockControl
or a ExpandedBlockControl
? Basically, later on, if we have deemed to have clicked a GroupedBlockControl
, it will be expanded (as long as no other GroupedBlockControl
is already expanded), and if we have deemed to have clicked a ExpandedBlockControl
, its BlockWorkItemClickedWork
callback Action<BlockWorkItem>
delegate will be called.
But, before any of that happens, we need to find if something has been clicked; otherwise, we are just scrolling wonderfully. The determining of a click is done by measuring the amount of pixels moved, and if they are within a certain limit, the original control we stored on MouseDown
is deemed to be clicked. The amount of pixels is stored within a ScrollContainer
variable called PixelsToMoveToBeConsideredClick
.
Anyway, without further ado, here is the most relevant code from the ScrollContainer
; the XAML is nothing, a ScrollViewer
and that is it.
public partial class ScrollContainer : UserControl, IScrollContainer
{
#region Data
....
....
private GroupedBlockControl groupedBlockControl;
private ExpandedBlockControl expandedBlockControl;
private List<BlockItemBase> blocks;
#endregion
#region Ctor
public ScrollContainer()
{
InitializeComponent();
friction = 0.85;
....
....
....
ContainerViewState = ViewState.BlockCollapsed;
}
#endregion
#region Public Properties
public ViewState ContainerViewState { get; set; }
public List<BlockItemBase> Blocks
{
get { return blocks; }
set
{
blocks = value;
CreateItems();
}
}
#endregion
#region Overrides
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
if (scroller.IsMouseOver)
{
......
......
......
......
groupedBlockControl =
TreeHelper.TryFindFromPoint<GroupedBlockControl>(this, scrollStartPoint);
expandedBlockControl =
TreeHelper.TryFindFromPoint<ExpandedBlockControl>(this, scrollStartPoint);
this.CaptureMouse();
}
base.OnPreviewMouseDown(e);
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (this.IsMouseCaptured)
{
Point currentPoint = e.GetPosition(this);
Point delta = new Point(scrollStartPoint.X -
currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
if (Math.Abs(delta.X) < ScrollContainer.PixelsToMoveToBeConsideredScroll &&
Math.Abs(delta.Y) < ScrollContainer.PixelsToMoveToBeConsideredScroll)
return;
scrollTarget.X = scrollStartOffset.X + delta.X;
scrollTarget.Y = scrollStartOffset.Y + delta.Y;
if (ContainerViewState == ViewState.BlockExpanded)
return;
scroller.ScrollToHorizontalOffset(scrollTarget.X);
scroller.ScrollToVerticalOffset(scrollTarget.Y);
}
base.OnPreviewMouseMove(e);
}
protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
{
if (this.IsMouseCaptured)
{
this.Cursor = Cursors.Arrow;
this.ReleaseMouseCapture();
}
Point currentPoint = e.GetPosition(this);
Point delta = new Point(scrollStartPoint.X -
currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
if (Math.Abs(delta.X) < ScrollContainer.PixelsToMoveToBeConsideredClick &&
Math.Abs(delta.Y) < ScrollContainer.PixelsToMoveToBeConsideredClick)
{
switch (ContainerViewState)
{
case ViewState.BlockExpanded:
if (expandedBlockControl == null)
{
blockContainers.ForEach((x) => x.CollapseAll());
ContainerViewState = ViewState.BlockCollapsed;
}
else
{
if (expandedBlockControl.IsPartOfGroup)
expandedBlockControl.DoWork();
else
{
blockContainers.ForEach((x) => x.CollapseAll());
ContainerViewState = ViewState.BlockCollapsed;
}
}
break;
case ViewState.BlockCollapsed:
if (groupedBlockControl != null)
groupedBlockControl.ExpandItem();
if (expandedBlockControl != null)
expandedBlockControl.DoWork();
break;
}
}
base.OnPreviewMouseUp(e);
}
#endregion
}
BlockContainer
Description of what this control does
This is actually a very simple control, in that its main purpose is just to show a group of Block
s (either ExpandedBlockControl
or GroupedBlockControl
). The only other job it has is to respond when one of the GroupedBlockControl
s is expanded, where this control will simply create some room to show the expanded GroupedBlockControl
by running a standard StoryBoard
animation. The only strange thing is that the BlockContainer
needs to be told by the expanding ExpandedBlocksContainer
how much room is required, and when it is told that, the BlockContainer
adjusts both its Show/Hide StoryBoard
s to make sure it grows/shrinks by the required amount for the expanding control. The BlockContainer
also tells its parent ScrollContainer
that its new state should be Expanded, which will stop the user from being able to scroll around, until they have closed the currently expanded GroupedBlockControl
.
It can be seen in the figure below that the ScrollContainer
can contain a number of BlockContainer
objects. This is dictated by the number of source items and the ScrollContainer blocksInBlocksContainer
variable.
The most important part of the BlockContainer
class is shown below. The XAML is nothing worth mentioning.
public partial class BlockContainer : UserControl, IBlockContainer
{
#region Data
private List<BlockItemBase> blocks;
private IScrollContainer scrollContainer;
private ExpandedBlocksContainer expanderToExpand;
private ExpandedBlocksContainer expanderToCollapse;
#endregion
#region Ctor
public BlockContainer(IScrollContainer scrollContainer)
{
InitializeComponent();
showStory = this.Resources["OnShow"] as Storyboard;
hideStory = this.Resources["OnHide"] as Storyboard;
this.scrollContainer = scrollContainer;
}
#endregion
#region Public Methods
public void StartExpandAnimation()
{
showStory.Begin();
}
public void StartCollapseAnimation()
{
hideStory.Begin();
}
public void CollapseAll()
{
expanderToExpand = null;
expanderToCollapse = null;
isFirstTime = true;
foreach (Object child in mainStack.Children)
{
if (child is ExpandedBlocksContainer)
{
ExpandedBlocksContainer temp = (ExpandedBlocksContainer)child;
temp.Hide();
}
}
SetOpacityForAllGroups(1.0);
}
public void ExpandAndShowBlocks(GroupedBlockControl controlToExpand)
{
if (expanderToExpand != null)
expanderToCollapse = expanderToExpand;
int rowOfExpandedGroup = controlToExpand.Row;
int columnOfExpandedGroup = controlToExpand.Column;
expanderToExpand=null;
foreach (Object child in mainStack.Children)
{
if (child is ExpandedBlocksContainer)
{
ExpandedBlocksContainer temp = (ExpandedBlocksContainer)child;
if ((int)temp.RowNumber == (rowOfExpandedGroup + 1))
{
expanderToExpand = (ExpandedBlocksContainer)child;
break;
}
}
}
expanderToExpand.GroupedBlockItem = controlToExpand.GroupedBlockItem;
expanderToExpand.ColumnBeingShown = columnOfExpandedGroup + 1;
if (expanderToCollapse != null)
{
expanderToCollapse.Hide();
}
if (isFirstTime)
{
isFirstTime = false;
expanderToExpand.Show();
scrollContainer.ContainerViewState = ViewState.BlockExpanded;
SetOpacityForAllGroups(lowerOpacity);
}
}
public List<BlockItemBase> Blocks
{
get { return blocks; }
set
{
blocks = value;
CreateItems();
}
}
#endregion
}
GroupBlockControl
Description of what this control does
Represents a grouping of items which are represented by small images and arranged in rows/columns in a grid. This control is also able to detect when a mouse click has been issued to it, and when that happens, providing no other group is expanded, it will signal to the parent BlockContainer
that the ExpandedBlocksContainer
that shows an expanded row for the items in this control should happen. Basically, all the real work for expanding is done in the parent BlockContainer
, which we discussed above.
The most relevant parts of this class are shown below:
public partial class GroupedBlockControl : UserControl
{
#region Ctor
public GroupedBlockControl(IBlockContainer blockContainer,
GroupedBlockItem groupedBlockItem,
int rowPositionInParent, int columnPositionInParent)
{
this.blockContainer = blockContainer;
this.groupedBlockItem = groupedBlockItem;
this.rowPositionInParent = rowPositionInParent;
this.columnPositionInParent = columnPositionInParent;
InitializeComponent();
....
....
....
}
....
....
....
#endregion
#region Public Methods
public void ExpandItem()
{
blockContainer.ExpandAndShowBlocks(this);
}
#endregion
}
The only thing of real note here is how the parent BlockContainer
is called in the ExpandItem()
method shown above, where the parent BlockContainer.ExpandAndShowBlocks()
method is called with the this
object. This allows the parent BlockContainer
to expand this newly clicked GroupedBlockControl
object, a.k.a. the GroupedBlockControl
that called the parent BlockContainer
. This expansion mechanism was explained earlier when we discussed the BlockContainer
control.
The XAML for this control is pretty basic; there is a Grid
with rows/columns that host images for the grouped items, and there is also a label. The finished thing looks like this:
ExpandedBlocksContainer
Description of what this control does
This control hosts a number of ExpandedBlockControl
items which are created by iterating through the IEnumerable<BlockWorkItem>
that are supplied to this control on the BlockItems
property by the parent BlockContainer.
The parent BlockContainer
will supply these items in response to a request to Expand a GroupedBlockControl
that the BlockContainer
also owns.
Basically, if you consider a single row within a GroupedBlockControl
, it could contain a mixture of GroupedBlockControl
and ExpandedBlockControl
, and when a GroupedBlockControl
is clicked and successfully expanded, the parent BlockContainer
control will establish which GroupedBlockItem
out of the original list of items relate to the GroupedBlockControl
that was expanded, and this GroupedBlockItem
will be used to populate the GroupedBlockItem
property of a ExpandedBlocksContainer
for the GroupedBlockControl
which is being asked to expand.
And then the ExpandedBlocksContainer
will be asked to expand, and when the expansion StoryBoard
completes, the ExpandedBlocksContainer
will create all the individual ExpandedBlockControl
items (one for each BlockWorkItem
within the IEnumerable<BlockWorkItem>
inside the supplied GroupedBlockItem
). It should also be noted that when the ExpandedBlocksContainer
expands/hides, it will also start a expand/hide StoryBoard
in the parent BlockContainer
to create enough room for the newly expanded ExpandedBlocksContainer
.
There really is not too much going on in the XAML, but I think I will show this for completeness this time. Here it is in its entirety. There are only a few things to note here, which are:
- The use of 2 x
StoryBoard
, these deal with the expand/collapse animations. These are adjusted using code-behind, once all ExpandedBlockControl
items have been added, and we know how much space is required.
- The use of a
Grid
(blocksContainerGrid
), which is used to host the ExpandedBlockControl
items that are added.
- The use of Expression Blend: Microsoft.Expression.Drawing.Dll, to allow us to use the native shapes, such as
RegularPolygon
below.
<UserControl x:Class="PhoneLikeScrollControl.ExpandedBlocksContainer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300" Margin="0">
<UserControl.Resources>
<Storyboard x:Key="OnShow" Duration="0:0:0.3"
Completed="OnShowStoryboard_Completed">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(FrameworkElement.Height)"
Storyboard.TargetName="bord">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="60"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="OnHide" Duration="0:0:0.1"
Completed="OnHideStoryboard_Completed">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(FrameworkElement.Height)"
Storyboard.TargetName="bord">
<EasingDoubleKeyFrame KeyTime="0" Value="60"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<StackPanel x:Name="blocksStack"
Orientation="Vertical"
Background="Transparent">
<StackPanel.Effect>
<DropShadowEffect Color="White"
Opacity="0.2" Direction="270"
ShadowDepth="5" BlurRadius="12" />
</StackPanel.Effect>
<ed:RegularPolygon x:Name="polygon"
HorizontalAlignment="Left"
VerticalAlignment="Center"
InnerRadius="1"
PointCount="3"
Stretch="Fill"
Stroke="White"
Fill="White"
Width="10"
Height="5"
Visibility="Collapsed"
StrokeThickness="1" />
<Border BorderThickness="0,2,0,2"
BorderBrush="White" x:Name="bord"
Margin="-1,0,-1,-6"
Visibility="Collapsed"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Border.RenderTransform>
<Grid Background="Black">
<StackPanel Orientation="Vertical">
<Canvas x:Name="canv"
HorizontalAlignment="Stretch" Height="20"
VerticalAlignment="Bottom"
Margin="0,0,0,0">
<Canvas.Background>
<LinearGradientBrush EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0.66"/>
<GradientStop Color="#FF686868"/>
</LinearGradientBrush>
</Canvas.Background>
<Label x:Name="lblGroup" Foreground="White"
FontFamily="Arial" FontWeight="Bold"
FontSize="18" Width="auto" Margin="0,-3,0,0"
VerticalAlignment="Center" VerticalContentAlignment="Center"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center">
</Label>
</Canvas>
<Grid x:Name="blocksContainerGrid" Margin="6,0,0,0"/>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</UserControl>
Now, let's consider the most important parts of the code-behind, which are as follows:
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;
using System.Windows.Media.Animation;
namespace PhoneLikeScrollControl
{
public partial class ExpandedBlocksContainer : UserControl
{
#region Data
private GroupedBlockItem groupedBlockItem;
private Storyboard showStory;
private Storyboard hideStory;
private bool isExpanded = false;
private IBlockContainer blockContainer;
#endregion
#region Ctor
public ExpandedBlocksContainer(IBlockContainer
int adjustedRowNumber, int rowNumber)
{
InitializeComponent();
this.RowNumber = rowNumber;
this.AdjustedRowNumber = adjustedRowNumber;
this.blockContainer = blockContainer;
showStory = this.Resources["OnShow"] as Storyboard;
hideStory = this.Resources["OnHide"] as Storyboard;
}
#endregion
#region Public Properties
public bool IsExpanded
{
get { return isExpanded; }
}
public GroupedBlockItem GroupedBlockItem
{
get { return groupedBlockItem; }
set
{
groupedBlockItem = value;
AdjustHeightForItems();
SetupGrid();
CreateAnimations();
}
}
#endregion
#region Public Methods
public void Show()
{
this.Visibility = Visibility.Visible;
bord.Visibility = Visibility.Visible;
blockContainer.StartExpandAnimation();
showStory.Begin();
}
public void Hide()
{
polygon.Visibility = Visibility.Collapsed;
blockContainer.StartCollapseAnimation();
hideStory.Begin();
}
public event EventHandler<EventArgs> ShowCompleted;
public event EventHandler<EventArgs> HideCompleted;
protected virtual void OnShowCompleted(EventArgs e)
{
polygon.Visibility = Visibility.Visible;
CreateItems();
if (ShowCompleted != null)
{
ShowCompleted(this, e);
}
}
protected virtual void OnHideCompleted(EventArgs e)
{
bord.Visibility = Visibility.Collapsed;
if (HideCompleted != null)
{
this.Visibility = Visibility.Collapsed;
HideCompleted(this, e);
}
}
#endregion
#region Private Methods
private void OnShowStoryboard_Completed(object sender, EventArgs e)
{
isExpanded = true;
OnShowCompleted(e);
}
private void OnHideStoryboard_Completed(object sender, EventArgs e)
{
isExpanded = false;
OnHideCompleted(e);
}
private void CreateAnimations()
{
....
....
....
....
}
private void CreateItems()
{
lblGroup.Content = groupedBlockItem.BlockName;
int row = 0;
int col = 0;
foreach (BlockWorkItem blockWorkItem in groupedBlockItem.BlockItems)
{
ExpandedBlockControl singleBlock =
new ExpandedBlockControl(blockWorkItem, true,false);
singleBlock.SetValue(Grid.RowProperty, row);
singleBlock.SetValue(Grid.ColumnProperty, col++);
blocksContainerGrid.Children.Add(singleBlock);
if (col == ScrollContainer.BlocksPerRow)
{
row++;
col = 0;
}
}
}
#endregion
}
}
I think this was mainly described at the beginning of this control's description, but the main parts are:
- When the
GroupedBlockItem
property is set, the height is adjusted to accommodate the items, and the 2 x Storyboard
s are adjusted for the required height for all items.
- When the
Show()
method is called, it will trigger the show StoryBoard
and will also trigger a StoryBoard
in the parent BlockContainer
to expand at the same time that this control is expanding.
- When the show
StoryBoard
is completed, all the ExpandedBlockControl
items are created and assigned to Grid
row/columns.
- When the
Hide()
method is called, it will trigger the hide StoryBoard
and will also trigger a StoryBoard
in the parent BlockContainer
to hide at the same time that this control is expanding.
ExpandedBlockControl
Description of what this control does
This control simply accepts an incoming constructor parameter of type BlockWorkItem
which contains a callback delegate (Action<T>
), which is called when this control is clicked, either when no expanded group is expanded (when it is part of a BlocksContainer
directly) or when it is part of an expanded group (when it is used as part of a ExpandedBlocksControl
).
From the figure above, it can be seen that this control is used in both a BlockContainer
and also as part of a ExpandedBlocksControl
group.
This is the most simple of the five controls that make up this article's source code. The XAML for this control merely comprises a Border
and a Image
, nothing much to speak of.
Here is most of the code for the ExpandedBlockControl
, where the only part of note is the DoWork()
method, that simply calls the original BlockWorkItem.BlockWorkItemClickedWorkcallback Action<BlockWorkItem>
delegate. This is how the hosting application is able to react to a block being clicked and do something useful with it. Basically, the hosting app should provide a payload for the callback delegate when it creates the initial List<BlockItemBase> ScrollableContainer.Blocks
property values.
public partial class ExpandedBlockControl : UserControl
{
#region Data
private BlockWorkItem blockWorkItem;
private bool isPartOfGroup = false;
#endregion
#region Ctor
public ExpandedBlockControl(BlockWorkItem blockWorkItem,
bool isPartOfGroup, bool addOnLabelHeight)
{
InitializeComponent();
....
....
this.blockWorkItem = blockWorkItem;
}
#endregion
#region Public Methods
public void DoWork()
{
blockWorkItem.BlockWorkItemClickedWork(this.blockWorkItem);
}
#endregion
#region Public Properties
public bool IsPartOfGroup
{
get { return isPartOfGroup; }
}
#endregion
}
How Could I Use it in My Own App?
To use this set of controls is very easy, all you have to do really is:
Step 1: Get Some Images
Add some images to your own app that you want to use for the blocks. In the demo app attached, these are in the DemoApp/Images folder.
Step 2: Host the ScrollContainer
Host the ScrollableContainer
in your own app. In the demo app, this is done by hosting the ScrollableContainer
in a host Window
. This is shown below:
<Window x:Class="PhoneLikeScrollControl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PhoneLikeScrollControl;assembly=PhoneLikeScrollControl">
<local:ScrollContainer x:Name="scrollContainer"
MaxHeight="400" MaxWidth="290"
HorizontalAlignment="Left"/>
</Window>
Step 3: Create Some Items for the ScrollContainer
Now that you have some images, and you have a hosted ScrollableContainer
, all you have to do is create some blocks to pass to the ScrollableContainer.Blocks
property. As previously mentioned in the article's text, there are two types of block controls: ones that are grouped (GroupedBlockControl
), and ones that are single block (ExpandedBlockControl
). These two different types of controls are internal objects that are created by the ScrollableContainer
; you will never create these controls yourself.
The ScrollableContainer
creates these internal controls based on examining the incoming List<BlockItemBase>
, which should be supplied to the ScrollableContainer.Blocks
property. The trick is that BlockItemBase
is an abstract
class that is implemented by two other classes:
BlockWorkItem
(represents a single block), which has a BitmapImage
and an Action<BlockWorkItem>
delegate that is called when the ExpandedBlockControl
is clicked. The only thing I am doing when the ExpandedBlockControl
is clicked is call the Action<BlockWorkItem>
delegate, passing in the original BlockWorkItem
and showing a MessageBox
, but I am sure you lot can think of some better things to do in your own app (hint: perhaps launch some app, or navigation pane). Where the ExpandedBlockControl
is created by the ScrollContainer
when it processes its Blocks
property (remember, that is a list of List<BlockItemBase>
which can hold BlockWorkItem
and GroupedBlockItem
items).
GroupedBlockItem
(represents a grouping of blocks between 1 - GroupedBlockControl.BlocksPerGroup
), where each block is represented by a single BlockWorkItem
(see item 1 directly above).
So all you really need to worry about is creating a List<BlockItemBase>
, which contains any mixture of BlockWorkItem
s or GroupedBlockItem
s, that fulfill your requirements.
A complete example of how to create some grouped/non grouped blocks with images is shown below, but please note this is just dummy data, where your actual data would more than likely come from a database or some other source.
This is the entire simulation code from the demo app's Window1.xaml.cs:
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;
using System.Windows.Threading;
using System.Diagnostics;
namespace PhoneLikeScrollControl
{
public partial class Window1 : Window
{
private Random rand = new Random();
private string[] imageUrls = new string[]
{
"/Images/square1.png",
"/Images/square2.png",
"/Images/square3.png",
"/Images/square4.png",
"/Images/square5.png",
"/Images/square6.png",
"/Images/square7.png",
"/Images/square8.png",
"/Images/square9.png",
"/Images/square10.png",
"/Images/square11.png",
"/Images/square12.png",
"/Images/square13.png",
"/Images/square14.png",
"/Images/square15.png",
"/Images/square16.png"
};
public Window1()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Window1_Loaded);
}
private void Window1_Loaded(object sender, RoutedEventArgs e)
{
List<BlockItemBase> items = new List<BlockItemBase>();
for (int i = 0; i < 72; i++)
{
if (rand.NextDouble() > 0.5)
items.Add(CreateNewGroupBlock((i+1)));
else
items.Add(CreateNewBlock((i + 1)));
}
scrollContainer.Blocks = items;
}
private BitmapImage GetRandomImagePath()
{
return new BitmapImage(
new Uri(string.Format(
"pack://application:,,,/DemoApp;component/{0}",
imageUrls[rand.Next(0, 6)])));
}
private GroupedBlockItem CreateNewGroupBlock(int blockNum)
{
List<BlockWorkItem> blockItems = new List<BlockWorkItem>();
for (int i = 0; i < rand.Next(1, GroupedBlockControl.BlocksPerGroup); i++)
{
string blockName = string.Format("title_{0}", (i + 1).ToString());
blockItems.Add(new BlockWorkItem(blockName, (x) =>
{
MessageBox.Show(string.Format("you clicked : {0}", x.BlockName));
},
GetRandomImagePath()));
}
return new GroupedBlockItem(string.Format("group_{0}", blockNum), blockItems);
}
private BlockWorkItem CreateNewBlock(int blockNum)
{
string blockName = string.Format("title_{0}", blockNum.ToString());
return new BlockWorkItem(blockName, (x) =>
{
MessageBox.Show(string.Format("you clicked : {0}", x.BlockName));
},
GetRandomImagePath());
}
}
}
That's it
As usual, I would just like to ask, if you enjoyed this article, and value the articles I write, could you please leave a vote and/or comment? It is always nice to hear if people find your articles useful. Cheers!