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

A Phone Like Control

0.00/5 (No votes)
1 Nov 2010 1  
A control that partially emulates a popular phone.

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 BlockContainers 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:

  1. 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
  2. This control will not scroll when a ExpandedBlocksContainer is expanded.
  3. This control will also call a ExpandedBlock.DoWork() method if the control is deemed to be clicked.
  4. This control will also expand a GroupedBlocksControl as long as no other GroupedBlocksControls 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)
{
    ....
    ....
    ....
    //store Control if one was found, so we can call
    //its Expand() method/DoWork() on MouseUp()
    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)
        {
            ......
            ......
            ......
            ......
            //store Control if one was found, so we can
            //call its Expand() method/DoWork() on MouseUp()
            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);

            // Determine the new amount to scroll.
            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;


            // Scroll to the new position.
            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);

        // Determine the new amount to scroll.
        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 Blocks (either ExpandedBlockControl or GroupedBlockControl). The only other job it has is to respond when one of the GroupedBlockControls 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 StoryBoards 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);
        }

        /// <summary>
        /// Adjust show/hide storyboards for this controls current height
        /// and also create offsets for parent BlockContainer to that it
        /// can animate to correct positions in unison with this control
        /// </summary>
        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 Storyboards 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.

/// <summary>
/// Represents a single block that can be used inside of a <c>BlockContainer</c>
/// or a <c>ExpandedBlocksContainer</c>
/// </summary>
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
    /// <summary>
    /// Calls the Action delegate which allows the users of this control
    /// to do somework based on this control being clicked
    /// </summary>    
    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:

  1. 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).
  2. 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 BlockWorkItems or GroupedBlockItems, 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
    {
        //some randomness for simulated data
        private Random rand = new Random();

        //Setup images for Blocks
        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);
        }

        /// <summary>
        /// On Load create a simulation mixture of single/grouped blocks using
        /// the 2 helper methods CreateNewBlock() and CreateNewGroupBlock() and add
        /// these to the hosted ScrollContainer
        /// </summary>
        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)));
            }
            //add blocks to the ScrollContainer, and it will
            //create the actual Controls required
            //based on this incoming Blocks List<BlockItemBase>
            scrollContainer.Blocks = items;
        }


        /// <summary>
        /// Gets a random BitmapImage from the available images
        /// </summary>
        private BitmapImage GetRandomImagePath()
        {
            return new BitmapImage(
                new Uri(string.Format(
                    "pack://application:,,,/DemoApp;component/{0}", 
                        imageUrls[rand.Next(0, 6)])));
        }

        /// <summary>
        /// Creates a new grouped block data object, to be used as part
        /// of List<BlockItemBase> that will be used to pass to 
        /// ScrollContainer.Blocks property
        /// </summary>
        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); 
        }

        /// <summary>
        /// Creates a new single block data object, to be used as part
        /// of List<BlockItemBase> that will be used to pass to 
        /// ScrollContainer.Blocks property
        /// </summary>       
        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!

License

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

A list of licenses authors might use can be found here