Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

Simple Metro Style Panorama Control for WPF

Rate me:
Please Sign up or sign in to vote.
4.99/5 (59 votes)
20 Oct 2013CPOL7 min read 295.4K   16.8K   136   110
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):

Image 1

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:

C#
/// <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:

C#
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

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

C#
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:

XML
<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.

C#
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:

XML
<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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionPanorama Picture Pin
neo86657-Nov-15 20:14
neo86657-Nov-15 20:14 
BugTouch pinch zoom in/out Pin
leyow1-May-14 14:22
professionalleyow1-May-14 14:22 
BugSystem Bug? Pin
Member 1036715628-Oct-13 23:40
Member 1036715628-Oct-13 23:40 
GeneralRe: System Bug? Pin
Sacha Barber29-Oct-13 3:34
Sacha Barber29-Oct-13 3:34 
GeneralRe: System Bug? Pin
Member 1036715631-Oct-13 17:17
Member 1036715631-Oct-13 17:17 
GeneralRe: System Bug? Pin
Member 1036715631-Oct-13 17:51
Member 1036715631-Oct-13 17:51 
GeneralRe: System Bug? Pin
Sacha Barber31-Oct-13 20:23
Sacha Barber31-Oct-13 20:23 
GeneralRe: System Bug? Pin
Member 1036715631-Oct-13 21:02
Member 1036715631-Oct-13 21:02 
GeneralRe: System Bug? Pin
Member 1036715631-Oct-13 23:04
Member 1036715631-Oct-13 23:04 
Questionvery nice Pin
BillW337-Oct-13 3:26
professionalBillW337-Oct-13 3:26 
AnswerRe: very nice Pin
Sacha Barber20-Oct-13 5:41
Sacha Barber20-Oct-13 5:41 
QuestionLoading big amount of tiles Pin
Florian Rosmann10-Sep-13 9:07
Florian Rosmann10-Sep-13 9:07 
AnswerRe: Loading big amount of tiles Pin
Sacha Barber10-Sep-13 10:30
Sacha Barber10-Sep-13 10:30 
GeneralRe: Loading big amount of tiles Pin
Florian Rosmann11-Sep-13 3:35
Florian Rosmann11-Sep-13 3:35 
QuestionExcellent work Pin
Andez20043-Jul-13 10:15
Andez20043-Jul-13 10:15 
Pete O'Hanlon was right. Keep up the great work.
SuggestionAlternative Mouse Handlers Pin
Obiwan Jacobi25-Feb-13 7:08
Obiwan Jacobi25-Feb-13 7:08 
GeneralRe: Alternative Mouse Handlers Pin
Sacha Barber25-Feb-13 10:02
Sacha Barber25-Feb-13 10:02 
GeneralRe: Alternative Mouse Handlers Pin
Sacha Barber2-Mar-13 5:29
Sacha Barber2-Mar-13 5:29 
GeneralRe: Alternative Mouse Handlers Pin
Obiwan Jacobi2-Mar-13 7:26
Obiwan Jacobi2-Mar-13 7:26 
GeneralRe: Alternative Mouse Handlers Pin
Sacha Barber2-Mar-13 12:14
Sacha Barber2-Mar-13 12:14 
QuestionVB version?? Pin
Santo Adzelito18-Feb-13 2:37
Santo Adzelito18-Feb-13 2:37 
AnswerRe: VB version?? Pin
Sacha Barber18-Feb-13 2:41
Sacha Barber18-Feb-13 2:41 
GeneralRe: VB version?? Pin
Santo Adzelito18-Feb-13 3:15
Santo Adzelito18-Feb-13 3:15 
BugExcellent work ,but using the demo project, I got a running "XamlParseException" by Vs2010, anything not correct? Pin
dicker15-Feb-13 17:06
dicker15-Feb-13 17:06 
GeneralRe: Excellent work ,but using the demo project, I got a running "XamlParseException" by Vs2010, anything not correct? Pin
Michael Röttges6-Mar-13 1:05
professionalMichael Röttges6-Mar-13 1:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.