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

Simple Metro Style Panorama Control for WPF

0.00/5 (No votes)
20 Oct 2013 8  
A simple Metro style Panorama control for WPF.

NOTES :

The alternative demo project and touch projects above were submitted to me by Jonathan Hodges.

Alternative demo : fixes a layout issue and also provides support for MouseWheel events to scroll. Thanks Jonathan, nice edits. 

Touch demo : works with the touch events for working with touch screens. 

Better new improved Panorama mouse handlers : Marc Jacobi 

Introduction

The attached code doesn't really do much so it doesn't need much jibber jabber describing what it does. What it does is pretty simple and can quite easily be summarized by the following few points:

  • Create a Metro style control. I say Style, as it may not be 100% in line with what the Win8 Metro tile interface does, or how it functions, but to be honest, it fitted my needs well enough and I figured it may be OK for others. So be warned, it is not like the best control ever, it is in fact pretty simple.
  • Allow it to be used in direct content mode/or use MVVM to drive the creation of UI elements, via DataBinding.
  • Allow users to customise the primary/complementary colors that should be used when generating tiles.
  • Allow single tile group snap back animations (you know when you have not dragged past 1/2 way ala iPhone UI experience, it snaps back to the previous tile group).
  • Works with WPF.

I wrote this control because I could not find one that seemed to work, I am aware of one in the Windows7 contrib/source code, but I wanted one for WPF, so I had to write this one.

So that is basically what it does in a nutshell, I guess it would be a nice thing to do at this point to show a screenshot or two, so let's do that.

Screenshots of Moving Parts

So here is how it all fits (you can click this image to get a bigger image):

The important thing to note here is that you are able to express how your tiles should look, that is entirely up to you. You have complete control over how your tiles are shown, this is typically done using a DataTemplate for your type of tile. We will see more on this later.

Deep Dive Into How It Works

There are really only a few controls that we need to worry about, which are described in full below:

PanoramaGroup

You can think of ParoramaGroup as being a logical group of tiles. Where the ParoramaGroup objects you supply need to look like this:

/// <summary>
/// Represents a groupig of tiles
/// </summary>
public class PanoramaGroup
{
    public PanoramaGroup(string header, ICollectionView tiles)
    {
        this.Header = header;
        this.Tiles = tiles;
    }

    public string Header { get; private set; }
    public ICollectionView Tiles { get; private set; }
}

Panorama

This is obviously where all the real action happens. So what does this control provide? It provides the following:

  • Templating ability via DataTemplates (in your own code...bonus)
  • It is a lookless control, as such template/style it as you see fit
  • It is easy to use
  • Two forms of scrolling, either snapback or using friction to current mouse co-ordinates

So those are the features.. what does the code look like? Well, here it is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Threading;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows.Media;

namespace PanoramaControl
{
    [TemplatePart(Name = "PART_ScrollViewer", Type = typeof(ScrollViewer))]
    public class Panorama : ItemsControl
    {
        #region Data
        private ScrollViewer sv;
        private Point scrollTarget;
        private Point scrollStartPoint;
        private Point scrollStartOffset;
        private Point previousPoint;
        private Vector velocity;
        private double friction;
        private DispatcherTimer animationTimer = new DispatcherTimer(DispatcherPriority.DataBind);
        private static int PixelsToMoveToBeConsideredScroll = 5;
        private static int PixelsToMoveToBeConsideredClick = 2;
        private Random rand = new Random(DateTime.Now.Millisecond);
        private bool _mouseDownFlag;
        private Cursor _savedCursor;
        #endregion

        #region Ctor
        public Panorama()
        {
            friction = 0.85;

            animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
            animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
            animationTimer.Start();

            TileColors = new Brush[] {
                new SolidColorBrush(Color.FromRgb((byte)111,(byte)189,(byte)69)),
                new SolidColorBrush(Color.FromRgb((byte)75,(byte)179,(byte)221)),
                new SolidColorBrush(Color.FromRgb((byte)65,(byte)100,(byte)165)),
                new SolidColorBrush(Color.FromRgb((byte)225,(byte)32,(byte)38)),
                new SolidColorBrush(Color.FromRgb((byte)128,(byte)0,(byte)128)),
                new SolidColorBrush(Color.FromRgb((byte)0,(byte)128,(byte)64)),
                new SolidColorBrush(Color.FromRgb((byte)0,(byte)148,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)0,(byte)199)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)135,(byte)15)),
                new SolidColorBrush(Color.FromRgb((byte)45,(byte)255,(byte)87)),
                new SolidColorBrush(Color.FromRgb((byte)127,(byte)0,(byte)55))
    
            };

            ComplimentaryTileColors = new Brush[] {
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255)),
                new SolidColorBrush(Color.FromRgb((byte)255,(byte)255,(byte)255))
            };

        }

        static Panorama()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Panorama), new FrameworkPropertyMetadata(typeof(Panorama)));
        }
        #endregion

        #region Properties
        public double Friction
        {
            get { return 1.0 - friction; }
            set { friction = Math.Min(Math.Max(1.0 - value, 0), 1.0); }
        }

        public List<Brush> TileColorPair
        {
            get 
            {
                int idx = rand.Next(TileColors.Length);
                return new List<Brush>() { TileColors[idx], ComplimentaryTileColors[idx] };
            }
        }

        #region DPs


        #region ItemBox

        public static readonly DependencyProperty ItemBoxProperty =
            DependencyProperty.Register("ItemHeight", typeof(double), typeof(Panorama),
                new FrameworkPropertyMetadata((double)120.0));


        public double ItemBox
        {
            get { return (double)GetValue(ItemBoxProperty); }
            set { SetValue(ItemBoxProperty, value); }
        }

        #endregion

        #region GroupHeight

        public static readonly DependencyProperty GroupHeightProperty =
            DependencyProperty.Register("GroupHeight", typeof(double), typeof(Panorama),
                new FrameworkPropertyMetadata((double)640.0));


        public double GroupHeight
        {
            get { return (double)GetValue(GroupHeightProperty); }
            set { SetValue(GroupHeightProperty, value); }
        }

        #endregion



        #region HeaderFontSize

        public static readonly DependencyProperty HeaderFontSizeProperty =
            DependencyProperty.Register("HeaderFontSize", typeof(double), typeof(Panorama),
                new FrameworkPropertyMetadata((double)30.0));

        public double HeaderFontSize
        {
            get { return (double)GetValue(HeaderFontSizeProperty); }
            set { SetValue(HeaderFontSizeProperty, value); }
        }

        #endregion


 
        #region HeaderFontColor

        public static readonly DependencyProperty HeaderFontColorProperty =
            DependencyProperty.Register("HeaderFontColor", typeof(Brush), typeof(Panorama),
                new FrameworkPropertyMetadata((Brush)Brushes.White));

        public Brush HeaderFontColor
        {
            get { return (Brush)GetValue(HeaderFontColorProperty); }
            set { SetValue(HeaderFontColorProperty, value); }
        }

        #endregion

        #region HeaderFontFamily

        public static readonly DependencyProperty HeaderFontFamilyProperty =
            DependencyProperty.Register("HeaderFontFamily", typeof(FontFamily), typeof(Panorama),
                new FrameworkPropertyMetadata((FontFamily)new FontFamily("Segoe UI")));

        public FontFamily HeaderFontFamily
        {
            get { return (FontFamily)GetValue(HeaderFontFamilyProperty); }
            set { SetValue(HeaderFontFamilyProperty, value); }
        }

        #endregion

        #region TileColors

        public static readonly DependencyProperty TileColorsProperty =
            DependencyProperty.Register("TileColors", typeof(Brush[]), typeof(Panorama),
                new FrameworkPropertyMetadata((Brush[])null));

        public Brush[] TileColors
        {
            get { return (Brush[])GetValue(TileColorsProperty); }
            set { SetValue(TileColorsProperty, value); }
        }

        #endregion

        #region ComplimentaryTileColors

        public static readonly DependencyProperty ComplimentaryTileColorsProperty =
            DependencyProperty.Register("ComplimentaryTileColors", typeof(Brush[]), typeof(Panorama),
                new FrameworkPropertyMetadata((Brush[])null));

        public Brush[] ComplimentaryTileColors
        {
            get { return (Brush[])GetValue(ComplimentaryTileColorsProperty); }
            set { SetValue(ComplimentaryTileColorsProperty, value); }
        }

        #endregion

        #region UseSnapBackScrolling

        public static readonly DependencyProperty UseSnapBackScrollingProperty =
            DependencyProperty.Register("UseSnapBackScrolling", typeof(bool), typeof(Panorama),
                new FrameworkPropertyMetadata((bool)true));

        public bool UseSnapBackScrolling
        {
            get { return (bool)GetValue(UseSnapBackScrollingProperty); }
            set { SetValue(UseSnapBackScrollingProperty, value); }
        }

        #endregion

        #endregion

        #endregion

        #region Private Methods

        private void DoStandardScrolling()
        {
            sv.ScrollToHorizontalOffset(scrollTarget.X);
            sv.ScrollToVerticalOffset(scrollTarget.Y);
            scrollTarget.X += velocity.X;
            scrollTarget.Y += velocity.Y;
            velocity *= friction;
        }


        private void HandleWorldTimerTick(object sender, EventArgs e)
        {
            var prop = DesignerProperties.IsInDesignModeProperty;
            bool isInDesignMode = (bool)DependencyPropertyDescriptor.FromProperty(prop,
                typeof(FrameworkElement)).Metadata.DefaultValue;

            if (isInDesignMode)
                return;


            if (IsMouseCaptured)
            {
                Point currentPoint = Mouse.GetPosition(this);
                velocity = previousPoint - currentPoint;
                previousPoint = currentPoint;
            }
            else
            {
                if (velocity.Length > 1)
                {
                    DoStandardScrolling();
                }
                else
                {
                    if (UseSnapBackScrolling)
                    {
                        int mx = (int)sv.HorizontalOffset % (int)ActualWidth;
                        if (mx == 0)
                            return;
                        int ix = (int)sv.HorizontalOffset / (int)ActualWidth;
                        double snapBackX = mx > ActualWidth / 2 ? (ix + 1) * ActualWidth : ix * ActualWidth;
                        sv.ScrollToHorizontalOffset(sv.HorizontalOffset + (snapBackX - sv.HorizontalOffset) / 4.0);
                    }
                    else
                    {
                        DoStandardScrolling();
                    }
                }
            }
        }
        #endregion

        #region Overrides


        public override void OnApplyTemplate()
        {
            sv = (ScrollViewer)Template.FindName("PART_ScrollViewer", this);
            base.OnApplyTemplate();
        }


        protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            if (sv.IsMouseOver)
            {
                _mouseDownFlag = true;

                // Save starting point, used later when determining how much to scroll.
                scrollStartPoint = e.GetPosition(this);
                scrollStartOffset.X = sv.HorizontalOffset;
                scrollStartOffset.Y = sv.VerticalOffset;
            }

            base.OnPreviewMouseLeftButtonDown(e);
        }

        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            if (_mouseDownFlag)
            {
                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) > PixelsToMoveToBeConsideredScroll ||
                    Math.Abs(delta.Y) > PixelsToMoveToBeConsideredScroll)
                {
                    scrollTarget.X = scrollStartOffset.X + delta.X;
                    scrollTarget.Y = scrollStartOffset.Y + delta.Y;

                    // Scroll to the new position.
                    sv.ScrollToHorizontalOffset(scrollTarget.X);
                    sv.ScrollToVerticalOffset(scrollTarget.Y);

                    if (!this.IsMouseCaptured)
                    {
                        if ((sv.ExtentWidth > sv.ViewportWidth) ||
                            (sv.ExtentHeight > sv.ViewportHeight))
                        {
                            _savedCursor = this.Cursor;
                            this.Cursor = Cursors.ScrollWE;
                        }

                        this.CaptureMouse();
                    }
                }
            }

            base.OnPreviewMouseMove(e);
        }

        protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            bool mouseDownFlag = _mouseDownFlag;
            // mouse move events may trigger while inside this handler.
            _mouseDownFlag = false;

            if (this.IsMouseCaptured)
            {
                // scroll action stopped
                this.Cursor = _savedCursor;
                this.ReleaseMouseCapture();
            }
            else if (mouseDownFlag)
            {
                // click action stopped
            }

            _savedCursor = null;

            base.OnPreviewMouseLeftButtonUp(e);
        }
        #endregion

    }
}

Default Styles Applied

And as this is a lookless control. Just for completeness, here are the default Styles that get applied to the Panorama control

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:PanoramaControl">
    <local:PanoramaGroupWidthConverter x:Key="conv" />

    <Style x:Key="headerLabelStyle" TargetType="Label">
        <Setter Property="FontSize"
                Value="{Binding RelativeSource={RelativeSource 
                    AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontSize}" />
        <Setter Property="Foreground"
                Value="{Binding RelativeSource={RelativeSource 
                    AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontColor}" />
        <Setter Property="FontFamily"
                Value="{Binding RelativeSource={RelativeSource 
                    AncestorType={x:Type local:Panorama}, Mode=FindAncestor}, Path=HeaderFontFamily}" />
        <Setter Property="FontWeight"
                Value="Normal" />
        <Setter Property="HorizontalAlignment"
                Value="Left" />
        <Setter Property="HorizontalContentAlignment"
                Value="Left" />
        <Setter Property="VerticalAlignment"
                Value="Center" />
        <Setter Property="VerticalContentAlignment"
                Value="Center" />
        <Setter Property="Margin"
                Value="10,0,0,20" />
    </Style>

    <DataTemplate DataType="{x:Type local:PanoramaGroup}">
        <DataTemplate.Resources>
            <Style x:Key="transparentListBoxItemStyle"
                   TargetType="{x:Type ListBoxItem}">
                <Style.Resources>
                    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
                                     Color="Transparent" />
                </Style.Resources>
                <Setter Property="Padding"
                        Value="0" />
                <Setter Property="Margin"
                        Value="0" />
            </Style>
        </DataTemplate.Resources>

        <DockPanel LastChildFill="True" Background="Transparent">
            <Label Style="{StaticResource headerLabelStyle}"
                   Content="{Binding Header}"
                   DockPanel.Dock="Top" />
            <ListBox ItemsSource="{Binding Tiles}" 
                     SelectionMode="Single"
                     BorderThickness="0"
                     BorderBrush="Transparent"
                     Background="Transparent"
                     IsSynchronizedWithCurrentItem="True"
                     ItemContainerStyle="{StaticResource transparentListBoxItemStyle}">

                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Height="{Binding 
                                RelativeSource={RelativeSource 
                                AncestorType={x:Type local:Panorama}, 
                                Mode=FindAncestor}, 
                                Path=GroupHeight}">
                            <WrapPanel.Width>
                                <MultiBinding Converter="{StaticResource conv}">
                                    <Binding Path="ItemBox" 
                                             RelativeSource="{RelativeSource 
                                                    AncestorType={x:Type local:Panorama}, 
                                                    Mode=FindAncestor}" />
                                    <Binding Path="GroupHeight" 
                                             RelativeSource="{RelativeSource 
                                                    AncestorType={x:Type local:Panorama}, 
                                                    Mode=FindAncestor}" />
                                    <Binding RelativeSource="{RelativeSource 
                                                    AncestorType={x:Type ListBox}, 
                                                    Mode=FindAncestor}" />
                                </MultiBinding>
                            </WrapPanel.Width>
                        </WrapPanel>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ListBox>
        </DockPanel>
    </DataTemplate>


    <Style TargetType="{x:Type local:Panorama}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ItemsControl}">
                    <ControlTemplate.Resources>
                        <Style TargetType="{x:Type ScrollViewer}">
                            <Setter Property="HorizontalScrollBarVisibility"
                                    Value="Hidden" />
                            <Setter Property="VerticalScrollBarVisibility"
                                    Value="Hidden" />
                        </Style>
                    </ControlTemplate.Resources>

                    <ScrollViewer x:Name="PART_ScrollViewer"
                                  Background="{TemplateBinding Background}"
                                  Padding="{TemplateBinding Padding}"
                                  SnapsToDevicePixels="true">
                        <ItemsPresenter Margin="0" 
                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </ScrollViewer>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

It really isn't that difficult actually. Essentially the Panorama control is a specialized ItemsControl that hosts its items (PanoramaGroup) inside a ScrollViewer, and each PanoramaGroup also hosts a number of custom tiles (which you would declare as ViewModels say, and implement a DataTemplate for) inside a ListBox which is part of the PanoramaGroup default style. The look and feel of the ViewModel for the tile is down to you and can be applied via your own DataTemplate.

As for the scrolling, that is all handled inside of the Panorama control and it is all mouse manipulation code. The only point worth mentioning is that you may pick your scrolling type by setting the UseSnapBackScrolling which you can set on the Panorama control.

How To Use It In Your Own Applications

There are really only a couple of things you need to do to use it in your own applications, these are outlined below. There is also a demo app attached which shows how to create the Panorama as shown in this article.

1. Create A Custom ViewModel 

This is really easy and you have pretty much full control over what your implementing class looks like.  

Here is a really simple tile example:

public class PanoramaTileViewModel : INPCBase
{
    private IMessageBoxService messageBoxService;
    private Timer liveUpdateTileTimer = new Timer();

    public PanoramaTileViewModel(IMessageBoxService messageBoxService, string text, string imageUrl, bool isDoubleWidth)
    {
        if (isDoubleWidth)
        {
            liveUpdateTileTimer.Interval = 1000;
            liveUpdateTileTimer.Elapsed += LiveUpdateTileTimer_Elapsed;
            liveUpdateTileTimer.Start();
        }


        this.messageBoxService = messageBoxService;
        this.Text = text;
        this.ImageUrl = imageUrl;
        this.IsDoubleWidth = isDoubleWidth;
        this.TileClickedCommand = new SimpleCommand<object, object>(ExecuteTileClickedCommand);
    }

    void LiveUpdateTileTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if (Counter < 10)
            Counter++;
        else
            Counter = 0;
        NotifyPropertyChanged("Counter");
    }

    public int Counter { get; set; }
    public string Text { get; private set; }
    public string ImageUrl { get; private set; }
    public bool IsDoubleWidth { get; private set; }
    public ICommand TileClickedCommand { get; private set; }

    public void ExecuteTileClickedCommand(object parameter)
    {
        messageBoxService.ShowMessage(string.Format("you clicked {0}", this.Text));
    }
}

This simple demo ViewModel class provides the ability to:

  • Show an image
  • Stretch to being double width tile
  • Show some text (if you want to, this demo shows this text as a ToolTip)
  • Respond to being clicked  
  • Allow live updates to tiles via INotifyPropertyChanged bindings (the way I am doing it in the demo app is not that typical, but this is not how you would normally do things anyway, you would be hitting web services / databases / WCF services etc., not using a Timer, it is just to demonstrate that you can live update the tiles anyway you want via INotifyPropertyChanged bindings)

You will note that my tile example here is fairly dumb for brevity, but you could have it animating/streaming data, whatever you like really, and the Panorama control would just show that data. Basically it is all standard XAML, so anything XAML does should work.

2. Define a DataTemplate To Suit Your ViewModel Class

The next thing you need to do is create your own DataTemplate. This must be what you want to see, I can't help you there, but as you have just created your own ViewModel class, it's really just a question of designing the look and feel of what you want to see for a given tile based on its current data value.

Here is a DataTemplate for the demo app's PanoramaTileViewModel class:

<DataTemplate DataType="{x:Type local:PanoramaTileViewModel}">
    <Border x:Name="bord" 
            BorderThickness="2"
            BorderBrush="{Binding RelativeSource={RelativeSource Mode=Self}, 
                Path=Background}"
            Background="{Binding RelativeSource={RelativeSource 
                AncestorType={x:Type pan:Panorama}, 
                Mode=FindAncestor}, 
                Path=TileColorPair[0]}"
            Width="120" Height="120" Margin="0">
        <StackPanel Orientation="Horizontal">

            <Button Command="{Binding TileClickedCommand}">
                <Button.Template>
                    <ControlTemplate>
                        <Image x:Name="img"
                            Source="{Binding ImageUrl}"
                            Width="100"
                            Height="100"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"
                            ToolTip="{Binding Text}" >
                        </Image>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding IsDoubleWidth}" Value="True">
                                <Setter TargetName="img"
                                    Property="HorizontalAlignment"
                                    Value="Left" />
                                <Setter TargetName="img"
                                    Property="Margin"
                                    Value="10,0,0,0" />
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Button.Template>
            </Button>
                    

            <Grid  Margin="30,0,0,0"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Center">
                        
            <Ellipse Stroke="White"
                        StrokeThickness="2"
                        Width="50"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Height="50" Fill="Transparent"/>
                    
                <Label x:Name="liveUpdate"
                    Content="{Binding Counter}"
                    Visibility="Collapsed"
                    HorizontalAlignment="Center"
                    HorizontalContentAlignment="Center"
                    VerticalAlignment="Center"
                    VerticalContentAlignment="Center"
                    Foreground="White"
                    FontFamily="Segoe UI"
                    FontSize="30"
                    FontWeight="DemiBold"/>
            </Grid>

        </StackPanel>

    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource 
                        AncestorType={x:Type ListBoxItem}, Mode=FindAncestor}, 
                        Path=IsSelected}"
                        Value="True">
            <Setter TargetName="bord"
                    Property="BorderBrush"
                    Value="{Binding RelativeSource={RelativeSource 
                        AncestorType={x:Type pan:Panorama}, Mode=FindAncestor}, 
                        Path=TileColorPair[1]}"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding IsDoubleWidth}"
                        Value="True">
            <Setter TargetName="bord"
                    Property="Width"
                    Value="240" />
            <Setter TargetName="liveUpdate"
                    Property="Visibility"
                    Value="Visible" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

3. Create an IEnumerable<PanoramaGroup> And Use That With the Panorama Control

Believe it or not, that is almost enough to get you there, all you need to do now is create either some manual UI elements (boo no one wants that, we want MVVM to do that for us), or create a ViewModel to supply items to an instance of a Panorama control. Let's see a dead simple demo ViewModel which provides the Items for a Panorama control.

public class MainWindowViewModel : INPCBase
{
    private Random rand = new Random(DateTime.Now.Millisecond);
    private List<DummyTileData> dummyData = new List<DummyTileData>();
    private IMessageBoxService messageBoxService;



    public MainWindowViewModel(IMessageBoxService messageBoxService)
    {
        this.messageBoxService = messageBoxService;

        //create some dummy data
        dummyData.Add(new DummyTileData("Add", @"Images/Add.png"));
        dummyData.Add(new DummyTileData("Adobe", @"Images/Adobe.png"));
        dummyData.Add(new DummyTileData("Android", @"Images/Android.png"));
        dummyData.Add(new DummyTileData("Author", @"Images/Author.png"));
        dummyData.Add(new DummyTileData("Blogger", @"Images/Blogger.png"));
        dummyData.Add(new DummyTileData("Copy", @"Images/Copy.png"));
        dummyData.Add(new DummyTileData("Delete", @"Images/Delete.png"));
        dummyData.Add(new DummyTileData("Digg", @"Images/Digg.png"));
        dummyData.Add(new DummyTileData("Edit", @"Images/Edit.png"));
        dummyData.Add(new DummyTileData("Facebook", @"Images/Facebook.png"));
        dummyData.Add(new DummyTileData("GMail", @"Images/GMail.png"));
        dummyData.Add(new DummyTileData("RSS", @"Images/RSS.png"));
        dummyData.Add(new DummyTileData("Save", @"Images/Save.png"));
        dummyData.Add(new DummyTileData("Search", @"Images/Search.png"));
        dummyData.Add(new DummyTileData("Trash", @"Images/Trash.png"));
        dummyData.Add(new DummyTileData("Twitter", @"Images/Twitter.png"));
        dummyData.Add(new DummyTileData("VisualStudio", @"Images/VisualStudio.png"));
        dummyData.Add(new DummyTileData("Wordpress", @"Images/Wordpress.png"));
        dummyData.Add(new DummyTileData("Yahoo", @"Images/Yahoo.png"));
        dummyData.Add(new DummyTileData("YouTube", @"Images/YouTube.png"));

        //Great some dummy groups
        List<PanoramaGroup> data = new List<PanoramaGroup>();
        List<PanoramaTileViewModel> tiles = new List<PanoramaTileViewModel>();

        for (int i = 0; i < 4; i++)
        {
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(true));

            tiles.Add(CreateTile(true));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));

            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));

            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
            tiles.Add(CreateTile(false));
        }

        data.Add(new PanoramaGroup("Settings",
            CollectionViewSource.GetDefaultView(tiles)));

        PanoramaItems = data;

    }


    private PanoramaTileViewModel CreateTile(bool isDoubleWidth)
    {
        DummyTileData dummyTileData = dummyData[rand.Next(dummyData.Count)];
        return new PanoramaTileViewModel(messageBoxService, 
            dummyTileData.Text, dummyTileData.ImageUrl, isDoubleWidth);
    }


    private IEnumerable<PanoramaGroup> panoramaItems;

    public IEnumerable<PanoramaGroup> PanoramaItems
    {
        get { return this.panoramaItems; }

        set
        {
            if (value != this.panoramaItems)
            {
                this.panoramaItems = value;
                NotifyPropertyChanged("CompanyName");
            }
        }
    }
}




public class DummyTileData
{
    public string Text { get; private set; }
    public string ImageUrl { get; private set; }

    public DummyTileData(string text, string imageUrl)
    {
        this.Text = text;
        this.ImageUrl = imageUrl;
    }
}

And here is an example of it being used in XAML using this demo ViewModel:

<Controls:MetroWindow
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:pan="clr-namespace:PanoramaControl;assembly=PanoramaControl"
    Width="960" MinWidth="960" Height="540">
    
        <pan:Panorama Grid.Row="1" x:Name="pan"
                      UseSnapBackScrolling="{Binding ElementName=chkUseSNapBackScrolling, 
				Path=IsChecked, Mode=OneWay}"
                      ItemsSource="{Binding PanoramaItems}"
                      ItemBox="120"
                      GroupHeight="360"
                      Background="Transparent" />
	
</Controls:MetroWindow>

You can see that the Panorama control requires very little setup at all. One thing that you may need to do when using it is to set your own Brush arrays, to specify your primary and complimentary tile colors (of course, you may disregard this altogether if you want to as well, at the end of the day the tile generation/color/look and feel is up to you).

But if you want to work with what this control offers, there are two DependencyProperty values that can be used to set an array of Brush objects which can be used for the tile and complimentary color brushes. These are available using the following Panorama control DPs, where these are expected to be of type Brush[]:

  • TileColors
  • ComplimentaryTileColors (this can be used inside your DataTemplate to color borders etc., should you wish to use them, which as I say is up to you)

Credit Where Credit Is Due

For the overall Window Style I am using some code found here: http://mahapps.com/MahApps.Metro/ which strangely enough also offers a Panorama control, which looks great, but did not seem to work very well in terms of scrolling when I tried it. Perhaps I was just doing it wrong, but I couldn't get how it did its scrolling at all.

The initial code had code that captured the mouse and as such did not allow input controls such as Button to be part of the tiles  DataTemplate, but thanks to some better mouse handlers supplied by Marc Jacobi, Buttons inside Tile are no issues at all, as can be seen from the new demo code DataTemplate 

 

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